diff --git a/native/expo-module/android/CMakeLists.txt b/android/CMakeLists.txt similarity index 72% rename from native/expo-module/android/CMakeLists.txt rename to android/CMakeLists.txt index 900774d..d27ed8a 100644 --- a/native/expo-module/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -10,17 +10,18 @@ find_package(ReactAndroid REQUIRED CONFIG) # Pre-built Rust .so add_library(salvium_crypto SHARED IMPORTED) set_target_properties(salvium_crypto PROPERTIES - IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/../../lib/android/${ANDROID_ABI}/libsalvium_crypto.so" + IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/../prebuilt/android/${ANDROID_ABI}/libsalvium_crypto.so" ) # JSI bridge module add_library(${PROJECT_NAME} SHARED - ../../cpp/SalviumCryptoModule.cpp + ../cpp/SalviumCryptoModule.cpp + ../cpp/OnLoad.cpp ) target_include_directories(${PROJECT_NAME} PRIVATE - ../../cpp - ../../../../crates/salvium-crypto/include + ../cpp + ../crates/salvium-crypto/include ) target_link_libraries(${PROJECT_NAME} diff --git a/native/expo-module/android/build.gradle b/android/build.gradle similarity index 91% rename from native/expo-module/android/build.gradle rename to android/build.gradle index a88b65a..a906d38 100644 --- a/native/expo-module/android/build.gradle +++ b/android/build.gradle @@ -39,10 +39,9 @@ android { } } - // Pre-built Rust .so files sourceSets { main { - jniLibs.srcDirs = ["../../lib/android"] + jniLibs.srcDirs = ["../prebuilt/android"] } } } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0eab87a --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/android/src/main/java/com/salvium/crypto/ExpoSalviumCryptoModule.java b/android/src/main/java/com/salvium/crypto/ExpoSalviumCryptoModule.java new file mode 100644 index 0000000..50f980b --- /dev/null +++ b/android/src/main/java/com/salvium/crypto/ExpoSalviumCryptoModule.java @@ -0,0 +1,53 @@ +package com.salvium.crypto; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.module.annotations.ReactModule; + +/** + * React Native module that installs the JSI crypto bindings. + * + * The actual crypto functions are exposed via JSI (C++ -> Rust FFI), + * not through the React Native bridge. This Java module only handles + * loading the native library and calling the C++ install function. + */ +@ReactModule(name = ExpoSalviumCryptoModule.NAME) +public class ExpoSalviumCryptoModule extends ReactContextBaseJavaModule { + public static final String NAME = "ExpoSalviumCrypto"; + + static { + System.loadLibrary("ExpoSalviumCrypto"); + System.loadLibrary("salvium_crypto"); + } + + public ExpoSalviumCryptoModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return NAME; + } + + /** + * Called from JS to install the JSI bindings. + * Must be called after the JSI runtime is available. + */ + @ReactMethod(isBlockingSynchronousMethod = true) + public boolean install() { + try { + ReactApplicationContext context = getReactApplicationContext(); + long jsiRuntimePtr = context.getJavaScriptContextHolder().get(); + if (jsiRuntimePtr == 0) { + return false; + } + nativeInstall(jsiRuntimePtr); + return true; + } catch (Exception e) { + return false; + } + } + + private native void nativeInstall(long jsiRuntimePtr); +} diff --git a/android/src/main/java/com/salvium/crypto/ExpoSalviumCryptoPackage.java b/android/src/main/java/com/salvium/crypto/ExpoSalviumCryptoPackage.java new file mode 100644 index 0000000..050e1b8 --- /dev/null +++ b/android/src/main/java/com/salvium/crypto/ExpoSalviumCryptoPackage.java @@ -0,0 +1,24 @@ +package com.salvium.crypto; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ExpoSalviumCryptoPackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new ExpoSalviumCryptoModule(reactContext)); + return modules; + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/cpp/OnLoad.cpp b/cpp/OnLoad.cpp new file mode 100644 index 0000000..c71f8ad --- /dev/null +++ b/cpp/OnLoad.cpp @@ -0,0 +1,21 @@ +/** + * OnLoad.cpp — Android JNI entry point + * + * Registers the JSI install function so Java can call it + * during React Native bridge initialization. + */ + +#include +#include +#include + +#include "SalviumCryptoModule.h" + +extern "C" JNIEXPORT void JNICALL +Java_com_salvium_crypto_ExpoSalviumCryptoModule_nativeInstall( + JNIEnv *env, jobject thiz, jlong jsiRuntimePtr) { + auto *rt = reinterpret_cast(jsiRuntimePtr); + if (rt) { + salvium::install(*rt); + } +} diff --git a/native/cpp/SalviumCryptoModule.cpp b/cpp/SalviumCryptoModule.cpp similarity index 99% rename from native/cpp/SalviumCryptoModule.cpp rename to cpp/SalviumCryptoModule.cpp index 6ef8db9..814edb2 100644 --- a/native/cpp/SalviumCryptoModule.cpp +++ b/cpp/SalviumCryptoModule.cpp @@ -8,7 +8,7 @@ */ #include "SalviumCryptoModule.h" -#include "../../crates/salvium-crypto/include/salvium_crypto.h" +#include "salvium_crypto.h" #include #include diff --git a/native/cpp/SalviumCryptoModule.h b/cpp/SalviumCryptoModule.h similarity index 100% rename from native/cpp/SalviumCryptoModule.h rename to cpp/SalviumCryptoModule.h diff --git a/native/expo-module/ExpoSalviumCrypto.podspec b/ios/ExpoSalviumCrypto.podspec similarity index 73% rename from native/expo-module/ExpoSalviumCrypto.podspec rename to ios/ExpoSalviumCrypto.podspec index 84715d8..d5b57ed 100644 --- a/native/expo-module/ExpoSalviumCrypto.podspec +++ b/ios/ExpoSalviumCrypto.podspec @@ -1,5 +1,3 @@ -require 'json' - Pod::Spec.new do |s| s.name = "ExpoSalviumCrypto" s.version = "0.1.0" @@ -11,17 +9,17 @@ Pod::Spec.new do |s| s.platforms = { :ios => "13.0" } s.source = { :git => "https://github.com/salvium/salvium-js.git", :tag => s.version.to_s } - # C++ JSI source + # C++ JSI source (shared between iOS and Android) s.source_files = "../cpp/SalviumCryptoModule.{h,cpp}" s.header_dir = "SalviumCrypto" - # Rust static library - s.vendored_libraries = "../lib/ios/libsalvium_crypto.a" + # Rust static library (prebuilt universal binary) + s.vendored_libraries = "../prebuilt/ios/libsalvium_crypto.a" # C header search path for salvium_crypto.h - s.preserve_paths = "../../crates/salvium-crypto/include/**" + s.preserve_paths = "../crates/salvium-crypto/include/**" s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/../../crates/salvium-crypto/include\"", + "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/../crates/salvium-crypto/include\"", "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", "OTHER_LDFLAGS" => "-lsalvium_crypto" } diff --git a/native/build-android.sh b/scripts/build-android.sh similarity index 86% rename from native/build-android.sh rename to scripts/build-android.sh index e804ba9..a20539d 100755 --- a/native/build-android.sh +++ b/scripts/build-android.sh @@ -6,15 +6,16 @@ # Set ANDROID_NDK_HOME to your NDK path (e.g. ~/Android/Sdk/ndk/26.1.10909125) # # Produces: -# native/lib/android/arm64-v8a/libsalvium_crypto.so -# native/lib/android/armeabi-v7a/libsalvium_crypto.so -# native/lib/android/x86_64/libsalvium_crypto.so +# prebuilt/android/arm64-v8a/libsalvium_crypto.so +# prebuilt/android/armeabi-v7a/libsalvium_crypto.so +# prebuilt/android/x86_64/libsalvium_crypto.so set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -CRATE_DIR="$SCRIPT_DIR/../crates/salvium-crypto" -OUT_DIR="$SCRIPT_DIR/lib/android" +ROOT_DIR="$SCRIPT_DIR/.." +CRATE_DIR="$ROOT_DIR/crates/salvium-crypto" +OUT_DIR="$ROOT_DIR/prebuilt/android" if [ -z "${ANDROID_NDK_HOME:-}" ]; then echo "Error: ANDROID_NDK_HOME is not set." @@ -65,7 +66,7 @@ for target in "${!TARGET_ABI[@]}"; do cargo build --release --target "$target" --manifest-path "$CRATE_DIR/Cargo.toml" mkdir -p "$OUT_DIR/$abi" - cp "$CRATE_DIR/../../target/$target/release/libsalvium_crypto.so" \ + cp "$CRATE_DIR/target/$target/release/libsalvium_crypto.so" \ "$OUT_DIR/$abi/libsalvium_crypto.so" done diff --git a/native/build-ios.sh b/scripts/build-ios.sh similarity index 78% rename from native/build-ios.sh rename to scripts/build-ios.sh index fb99675..4cddd85 100755 --- a/native/build-ios.sh +++ b/scripts/build-ios.sh @@ -4,13 +4,14 @@ # Prerequisites: # rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios # -# Produces: native/lib/ios/libsalvium_crypto.a (universal fat binary via lipo) +# Produces: prebuilt/ios/libsalvium_crypto.a (universal fat binary via lipo) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -CRATE_DIR="$SCRIPT_DIR/../crates/salvium-crypto" -OUT_DIR="$SCRIPT_DIR/lib/ios" +ROOT_DIR="$SCRIPT_DIR/.." +CRATE_DIR="$ROOT_DIR/crates/salvium-crypto" +OUT_DIR="$ROOT_DIR/prebuilt/ios" TARGETS=( aarch64-apple-ios # Device (arm64) @@ -31,7 +32,7 @@ mkdir -p "$OUT_DIR" # Collect all .a paths LIBS=() for target in "${TARGETS[@]}"; do - LIBS+=("$CRATE_DIR/../../target/$target/release/libsalvium_crypto.a") + LIBS+=("$CRATE_DIR/target/$target/release/libsalvium_crypto.a") done lipo -create "${LIBS[@]}" -output "$OUT_DIR/libsalvium_crypto.a" diff --git a/src/bulletproofs_plus.js b/src/bulletproofs_plus.js index 1090985..87f0c43 100644 --- a/src/bulletproofs_plus.js +++ b/src/bulletproofs_plus.js @@ -1082,14 +1082,19 @@ export function serializeProof(proof) { const { V, A, A1, B, r1, s1, d1, L, R } = proof; // Monero/Salvium binary format for BulletproofPlus: + // varint(V.length), V[0..n] (32 bytes each) // A (32), A1 (32), B (32) // r1 (32), s1 (32), d1 (32) // varint(L.length), L[0..n] (32 bytes each) // varint(R.length), R[0..n] (32 bytes each) - // NOTE: V (commitments) are NOT serialized — they're restored via outPk + // Must match parseProof format for roundtrip compatibility. const chunks = []; + // V (commitments) + chunks.push(_encodeVarint(V.length)); + for (const v of V) chunks.push(v.toBytes()); + // A, A1, B (points) chunks.push(A.toBytes()); chunks.push(A1.toBytes()); diff --git a/test/bulletproofs_plus.test.js b/test/bulletproofs_plus.test.js index b83276d..5188129 100644 --- a/test/bulletproofs_plus.test.js +++ b/test/bulletproofs_plus.test.js @@ -9,7 +9,7 @@ import { scalarToBytes, bytesToPoint, hashToScalar, - hashToPoint, + hashToPointMonero, initGenerators, initTranscript, parseProof, @@ -152,9 +152,9 @@ test('hashToScalar produces different output for different input', () => { assertTrue(scalar1 !== scalar2, 'Different inputs should produce different outputs'); }); -test('hashToPoint produces valid point', () => { +test('hashToPointMonero produces valid point', () => { const data = new TextEncoder().encode('test data'); - const point = hashToPoint(data); + const point = hashToPointMonero(data); assertExists(point); // Point should not be identity assertTrue(!point.equals(Point.ZERO), 'Should not be identity point'); diff --git a/test/transaction-builder.test.js b/test/transaction-builder.test.js index 77b2c3a..fd08569 100644 --- a/test/transaction-builder.test.js +++ b/test/transaction-builder.test.js @@ -352,9 +352,9 @@ test('buildTransaction with no change (exact amount)', () => { fee }); - // No change when amounts match exactly - assertEqual(tx.prefix.vout.length, 1, 'Should have only 1 output when no change'); - assertEqual(tx._meta.changeIndex, -1, 'Change index should be -1'); + // Change output is always added for privacy (even with 0 amount) + assertEqual(tx.prefix.vout.length, 2, 'Should have 2 outputs (destination + zero-change)'); + assertEqual(tx._meta.changeIndex, -1, 'Change index should be -1 for zero change'); }); test('buildTransaction with multiple inputs', () => { diff --git a/test/wallet-sync.test.js b/test/wallet-sync.test.js index 7932b0a..95ba51a 100644 --- a/test/wallet-sync.test.js +++ b/test/wallet-sync.test.js @@ -158,8 +158,8 @@ test('SYNC_STATUS has correct values', () => { assertEqual(SYNC_STATUS.ERROR, 'error'); }); -test('DEFAULT_BATCH_SIZE is 10', () => { - assertEqual(DEFAULT_BATCH_SIZE, 10); +test('DEFAULT_BATCH_SIZE is 100', () => { + assertEqual(DEFAULT_BATCH_SIZE, 100); }); test('SYNC_UNLOCK_BLOCKS is 10', () => {