@hathor/ct-crypto-mobile
v0.5.0
Published
React Native (iOS/Android) shielded crypto provider for Hathor confidential transactions. Rust core over UniFFI, bridged as a native module; implements the @hathor/ct-crypto-provider interface.
Keywords
Readme
@hathor/ct-crypto-mobile
React Native (iOS + Android) shielded crypto provider for Hathor confidential
transactions. The cryptography is the same Rust core used by
@hathor/ct-crypto-node and @hathor/ct-crypto-wasm, compiled for mobile and
exposed over UniFFI through a React Native native module.
What's in the box
src/ Rust crate (hathor-ct-crypto-mobile): UniFFI surface over crypto-core
ios/ UniFFI-generated Swift API + RN bridge module + XCFramework*
android/ UniFFI-generated Kotlin API + RN bridge module + jniLibs*
js/ MobileShieldedProvider (implements @hathor/ct-crypto-provider)* The compiled Rust binaries (ios/HathorCtCrypto.xcframework,
android/src/main/jniLibs/<abi>/libhathor_ct_crypto_mobile.so) are built by CI
(.github/workflows/build-mobile.yml) and shipped in the npm tarball — they are
not present in a plain git checkout. A release packs the CI artifact of the
release tag, checks it byte-for-byte against the CI builds and the tagged
sources, and publishes that tarball; prepublishOnly refuses any publish from a
directory (see the repo's
RELEASING.md).
Usage (in a React Native app)
npm install @hathor/ct-crypto-mobile
cd ios && pod install # autolinking picks up hathor-ct-crypto-mobile.podspecimport { createMobileShieldedCryptoProvider } from '@hathor/ct-crypto-mobile';
wallet.setShieldedCryptoProvider(createMobileShieldedCryptoProvider());Same IShieldedCryptoProvider contract as the node and wasm providers — see
@hathor/ct-crypto-provider for the interface documentation.
This provider is prover + rewind oriented. It backs output creation,
rewind/decrypt, tag/commitment derivation, surjection proofs and the balancing
factor, but not the OPTIONAL verifier surface (verifyRangeProof,
verifySurjectionProof, verifyBalance, verifyCommitmentsSum,
validateCommitment, validateGenerator) — the UniFFI layer exposes no
verification exports, so those methods are left undefined per the optional
contract. Feature-detect before calling
(if (provider.verifyRangeProof) { ... }), or use @hathor/ct-crypto-node /
@hathor/ct-crypto-wasm for verification.
Bridge marshaling contract
The RN bridge carries neither raw bytes nor BigInt, so between JS and native:
- bytes cross as base64 strings — strict, canonical (RFC 4648, padded)
base64. Both native decoders reject whitespace, non-alphabet characters and
unpadded input, so iOS and Android accept exactly the same inputs
(
Data(base64Encoded:)on Swift; a strict regex beforeBase64.decodeon Android, whoseandroid.util.Base64is otherwise lenient); - u64 values cross as decimal strings (amounts can exceed 2^53, where
JS
Numbersilently loses precision); - records cross as objects with the provider's camelCase keys;
- native errors reject with code
InvalidInput(malformed / bad-length caller input) orCryptoFailed(the operation itself failed).
js/index.js and the two bridge modules (ios/HathorCtCryptoModule.swift,
android/.../HathorCtCryptoModule.kt) are the two ends of this contract —
change them together.
Scan-miss handling on rewind
A rewind* call that rejects with code CryptoFailed — the output is not
addressed to this scan key (the common case while walking the chain) or its
proof/commitment did not open — is translated into the provider's
ScanMissError (the native error is preserved as .cause). Chain scanners
catch it to skip foreign outputs without brittle message-matching:
import { ScanMissError } from '@hathor/ct-crypto-provider';
try {
const rewound = await provider.rewindAmountShieldedOutput(/* … */);
// addressed to us
} catch (err) {
if (err instanceof ScanMissError) continue; // foreign / not ours — skip
throw err; // genuine failure
}An InvalidInput rejection (a programming error) is not a scan-miss and
propagates unchanged.
Regenerating the FFI bindings
The Swift/Kotlin files under ios/ and android/.../uniffi/ are generated
from the Rust crate — never edit them by hand. After changing
src/uniffi_bindings.rs:
cargo build -p hathor-ct-crypto-mobile --release
cargo run -p hathor-ct-crypto-mobile --features uniffi-cli --bin uniffi-bindgen -- \
generate --library target/release/libhathor_ct_crypto_mobile.dylib \
--language swift --no-format --out-dir packages/ct-crypto-mobile/ios
cargo run -p hathor-ct-crypto-mobile --features uniffi-cli --bin uniffi-bindgen -- \
generate --library target/release/libhathor_ct_crypto_mobile.dylib \
--language kotlin --no-format --out-dir packages/ct-crypto-mobile/android/src/main/javaAlways pass --no-format — the committed bindings are unformatted, and the
CI drift check regenerates with --no-format so the comparison does not
depend on whichever swiftformat/ktlint version happens to be installed.
CI fails the build if the committed bindings drift from the Rust surface.
