● Restructure native module for Expo auto-discovery, fix 3 test failures
Move native code from native/ to Expo-compatible layout with android/
and ios/ at package root. Add Android JNI bridge (OnLoad.cpp,
ExpoSalviumCryptoModule.java, ExpoSalviumCryptoPackage.java).
Prebuilt binaries now go in prebuilt/ instead of native/lib/.
Test fixes:
- wallet-sync: update stale DEFAULT_BATCH_SIZE assertion (10 -> 100)
- bulletproofs+: fix hashToPoint -> hashToPointMonero rename in test,
fix serializeProof to include V array matching parseProof format
- transaction-builder: expect 2 outputs for exact-amount tx (zero-change
output is always added for privacy)
This commit is contained in:
@@ -10,17 +10,18 @@ find_package(ReactAndroid REQUIRED CONFIG)
|
|||||||
# Pre-built Rust .so
|
# Pre-built Rust .so
|
||||||
add_library(salvium_crypto SHARED IMPORTED)
|
add_library(salvium_crypto SHARED IMPORTED)
|
||||||
set_target_properties(salvium_crypto PROPERTIES
|
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
|
# JSI bridge module
|
||||||
add_library(${PROJECT_NAME} SHARED
|
add_library(${PROJECT_NAME} SHARED
|
||||||
../../cpp/SalviumCryptoModule.cpp
|
../cpp/SalviumCryptoModule.cpp
|
||||||
|
../cpp/OnLoad.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(${PROJECT_NAME} PRIVATE
|
target_include_directories(${PROJECT_NAME} PRIVATE
|
||||||
../../cpp
|
../cpp
|
||||||
../../../../crates/salvium-crypto/include
|
../crates/salvium-crypto/include
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(${PROJECT_NAME}
|
target_link_libraries(${PROJECT_NAME}
|
||||||
@@ -39,10 +39,9 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-built Rust .so files
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
jniLibs.srcDirs = ["../../lib/android"]
|
jniLibs.srcDirs = ["../prebuilt/android"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.salvium.crypto">
|
||||||
|
</manifest>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||||
|
List<NativeModule> modules = new ArrayList<>();
|
||||||
|
modules.add(new ExpoSalviumCryptoModule(reactContext));
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <fbjni/fbjni.h>
|
||||||
|
#include <jsi/jsi.h>
|
||||||
|
#include <ReactCommon/CallInvokerHolder.h>
|
||||||
|
|
||||||
|
#include "SalviumCryptoModule.h"
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_com_salvium_crypto_ExpoSalviumCryptoModule_nativeInstall(
|
||||||
|
JNIEnv *env, jobject thiz, jlong jsiRuntimePtr) {
|
||||||
|
auto *rt = reinterpret_cast<facebook::jsi::Runtime *>(jsiRuntimePtr);
|
||||||
|
if (rt) {
|
||||||
|
salvium::install(*rt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "SalviumCryptoModule.h"
|
#include "SalviumCryptoModule.h"
|
||||||
#include "../../crates/salvium-crypto/include/salvium_crypto.h"
|
#include "salvium_crypto.h"
|
||||||
|
|
||||||
#include <jsi/jsi.h>
|
#include <jsi/jsi.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
require 'json'
|
|
||||||
|
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = "ExpoSalviumCrypto"
|
s.name = "ExpoSalviumCrypto"
|
||||||
s.version = "0.1.0"
|
s.version = "0.1.0"
|
||||||
@@ -11,17 +9,17 @@ Pod::Spec.new do |s|
|
|||||||
s.platforms = { :ios => "13.0" }
|
s.platforms = { :ios => "13.0" }
|
||||||
s.source = { :git => "https://github.com/salvium/salvium-js.git", :tag => s.version.to_s }
|
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.source_files = "../cpp/SalviumCryptoModule.{h,cpp}"
|
||||||
s.header_dir = "SalviumCrypto"
|
s.header_dir = "SalviumCrypto"
|
||||||
|
|
||||||
# Rust static library
|
# Rust static library (prebuilt universal binary)
|
||||||
s.vendored_libraries = "../lib/ios/libsalvium_crypto.a"
|
s.vendored_libraries = "../prebuilt/ios/libsalvium_crypto.a"
|
||||||
|
|
||||||
# C header search path for salvium_crypto.h
|
# 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 = {
|
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",
|
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17",
|
||||||
"OTHER_LDFLAGS" => "-lsalvium_crypto"
|
"OTHER_LDFLAGS" => "-lsalvium_crypto"
|
||||||
}
|
}
|
||||||
@@ -6,15 +6,16 @@
|
|||||||
# Set ANDROID_NDK_HOME to your NDK path (e.g. ~/Android/Sdk/ndk/26.1.10909125)
|
# Set ANDROID_NDK_HOME to your NDK path (e.g. ~/Android/Sdk/ndk/26.1.10909125)
|
||||||
#
|
#
|
||||||
# Produces:
|
# Produces:
|
||||||
# native/lib/android/arm64-v8a/libsalvium_crypto.so
|
# prebuilt/android/arm64-v8a/libsalvium_crypto.so
|
||||||
# native/lib/android/armeabi-v7a/libsalvium_crypto.so
|
# prebuilt/android/armeabi-v7a/libsalvium_crypto.so
|
||||||
# native/lib/android/x86_64/libsalvium_crypto.so
|
# prebuilt/android/x86_64/libsalvium_crypto.so
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
CRATE_DIR="$SCRIPT_DIR/../crates/salvium-crypto"
|
ROOT_DIR="$SCRIPT_DIR/.."
|
||||||
OUT_DIR="$SCRIPT_DIR/lib/android"
|
CRATE_DIR="$ROOT_DIR/crates/salvium-crypto"
|
||||||
|
OUT_DIR="$ROOT_DIR/prebuilt/android"
|
||||||
|
|
||||||
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
|
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
|
||||||
echo "Error: ANDROID_NDK_HOME is not set."
|
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"
|
cargo build --release --target "$target" --manifest-path "$CRATE_DIR/Cargo.toml"
|
||||||
|
|
||||||
mkdir -p "$OUT_DIR/$abi"
|
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"
|
"$OUT_DIR/$abi/libsalvium_crypto.so"
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -4,13 +4,14 @@
|
|||||||
# Prerequisites:
|
# Prerequisites:
|
||||||
# rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
CRATE_DIR="$SCRIPT_DIR/../crates/salvium-crypto"
|
ROOT_DIR="$SCRIPT_DIR/.."
|
||||||
OUT_DIR="$SCRIPT_DIR/lib/ios"
|
CRATE_DIR="$ROOT_DIR/crates/salvium-crypto"
|
||||||
|
OUT_DIR="$ROOT_DIR/prebuilt/ios"
|
||||||
|
|
||||||
TARGETS=(
|
TARGETS=(
|
||||||
aarch64-apple-ios # Device (arm64)
|
aarch64-apple-ios # Device (arm64)
|
||||||
@@ -31,7 +32,7 @@ mkdir -p "$OUT_DIR"
|
|||||||
# Collect all .a paths
|
# Collect all .a paths
|
||||||
LIBS=()
|
LIBS=()
|
||||||
for target in "${TARGETS[@]}"; do
|
for target in "${TARGETS[@]}"; do
|
||||||
LIBS+=("$CRATE_DIR/../../target/$target/release/libsalvium_crypto.a")
|
LIBS+=("$CRATE_DIR/target/$target/release/libsalvium_crypto.a")
|
||||||
done
|
done
|
||||||
|
|
||||||
lipo -create "${LIBS[@]}" -output "$OUT_DIR/libsalvium_crypto.a"
|
lipo -create "${LIBS[@]}" -output "$OUT_DIR/libsalvium_crypto.a"
|
||||||
@@ -1082,14 +1082,19 @@ export function serializeProof(proof) {
|
|||||||
const { V, A, A1, B, r1, s1, d1, L, R } = proof;
|
const { V, A, A1, B, r1, s1, d1, L, R } = proof;
|
||||||
|
|
||||||
// Monero/Salvium binary format for BulletproofPlus:
|
// Monero/Salvium binary format for BulletproofPlus:
|
||||||
|
// varint(V.length), V[0..n] (32 bytes each)
|
||||||
// A (32), A1 (32), B (32)
|
// A (32), A1 (32), B (32)
|
||||||
// r1 (32), s1 (32), d1 (32)
|
// r1 (32), s1 (32), d1 (32)
|
||||||
// varint(L.length), L[0..n] (32 bytes each)
|
// varint(L.length), L[0..n] (32 bytes each)
|
||||||
// varint(R.length), R[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 = [];
|
const chunks = [];
|
||||||
|
|
||||||
|
// V (commitments)
|
||||||
|
chunks.push(_encodeVarint(V.length));
|
||||||
|
for (const v of V) chunks.push(v.toBytes());
|
||||||
|
|
||||||
// A, A1, B (points)
|
// A, A1, B (points)
|
||||||
chunks.push(A.toBytes());
|
chunks.push(A.toBytes());
|
||||||
chunks.push(A1.toBytes());
|
chunks.push(A1.toBytes());
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
scalarToBytes,
|
scalarToBytes,
|
||||||
bytesToPoint,
|
bytesToPoint,
|
||||||
hashToScalar,
|
hashToScalar,
|
||||||
hashToPoint,
|
hashToPointMonero,
|
||||||
initGenerators,
|
initGenerators,
|
||||||
initTranscript,
|
initTranscript,
|
||||||
parseProof,
|
parseProof,
|
||||||
@@ -152,9 +152,9 @@ test('hashToScalar produces different output for different input', () => {
|
|||||||
assertTrue(scalar1 !== scalar2, 'Different inputs should produce different outputs');
|
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 data = new TextEncoder().encode('test data');
|
||||||
const point = hashToPoint(data);
|
const point = hashToPointMonero(data);
|
||||||
assertExists(point);
|
assertExists(point);
|
||||||
// Point should not be identity
|
// Point should not be identity
|
||||||
assertTrue(!point.equals(Point.ZERO), 'Should not be identity point');
|
assertTrue(!point.equals(Point.ZERO), 'Should not be identity point');
|
||||||
|
|||||||
@@ -352,9 +352,9 @@ test('buildTransaction with no change (exact amount)', () => {
|
|||||||
fee
|
fee
|
||||||
});
|
});
|
||||||
|
|
||||||
// No change when amounts match exactly
|
// Change output is always added for privacy (even with 0 amount)
|
||||||
assertEqual(tx.prefix.vout.length, 1, 'Should have only 1 output when no change');
|
assertEqual(tx.prefix.vout.length, 2, 'Should have 2 outputs (destination + zero-change)');
|
||||||
assertEqual(tx._meta.changeIndex, -1, 'Change index should be -1');
|
assertEqual(tx._meta.changeIndex, -1, 'Change index should be -1 for zero change');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTransaction with multiple inputs', () => {
|
test('buildTransaction with multiple inputs', () => {
|
||||||
|
|||||||
@@ -158,8 +158,8 @@ test('SYNC_STATUS has correct values', () => {
|
|||||||
assertEqual(SYNC_STATUS.ERROR, 'error');
|
assertEqual(SYNC_STATUS.ERROR, 'error');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('DEFAULT_BATCH_SIZE is 10', () => {
|
test('DEFAULT_BATCH_SIZE is 100', () => {
|
||||||
assertEqual(DEFAULT_BATCH_SIZE, 10);
|
assertEqual(DEFAULT_BATCH_SIZE, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SYNC_UNLOCK_BLOCKS is 10', () => {
|
test('SYNC_UNLOCK_BLOCKS is 10', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user