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', () => {