react-native-pirate-wallet is the React Native wrapper for the unified Pirate wallet backend.
It exposes one JS API over the same native service layer used by the Android SDK and iOS SDK.
The package is meant for React Native wallets such as Edge Wallet.
Repo-level build and integration notes:
docs/react-native-plugin.md
- Android: JNI bridge over
libpirate_ffi_native.so - iOS: Objective-C bridge over
PirateWalletNative.xcframework - JS: typed wallet wrapper plus a polling synchronizer
The JS surface mirrors the SDK boundary used by the native Android and iOS SDKs.
Configure wallet storage before any wallet operation. The recommended mobile path creates a random registry credential inside iOS Keychain or Android Keystore and never returns it to JavaScript:
const sdk = createPirateWalletSdk()
await sdk.configureSecureAccountStorage({
accountId: edgeAccountIdHash
})The native module creates an app-private directory for that account and asks the Rust service to open or create that account's wallet namespace. The selected directory contains:
wallet_registry.db- per-wallet database files
- database salts
- sealed database key files
The protected device credential unlocks the registry and per-wallet databases in that namespace so viewing data and the compact-block cache can support concurrent synchronization before an Edge account is unlocked.
By default, Android derives the directory under:
Context.filesDir/pirate_wallet/accounts/<sanitized-account-id>
iOS derives the directory under:
Application Support/PirateWallet/accounts/<sanitized-account-id>
Integrations that need to provide their own app-private path may pass
storagePath:
await sdk.configureSecureAccountStorage({
accountId: edgeAccountIdHash,
storagePath: accountPrivatePath
})android/example/ios/scripts/src/
Native libraries are distributed in exact-version Android ARM, Android x86_64, iOS device, iOS simulator arm64, and iOS simulator x86_64 companion packages. On macOS, CocoaPods combines the two thin simulator archives into the universal XCFramework slice expected by Xcode.
Before testing or packaging this plugin from the monorepo, stage the native artifacts:
bash scripts/prepare-react-native-plugin.shThat copies:
- Android JNI libraries into the two Android companion packages
- the iOS device XCFramework slice and two thin simulator archives into the three iOS companion packages
There is also a minimal consumer app in:
bindings/react-native-pirate-wallet/example/
That app is used to verify install, autolinking, and a couple of real native calls.
Main exports:
PirateWalletSdkPirateWalletSynchronizercreatePirateWalletSdk()
The synchronizer is implemented in JS and polls the native service through the bridge. It does not depend on native event emitters.
The JS wrapper is a typed layer over the native invoke(requestJson, pretty) bridge.
Low-level entry points:
sdk.invoke(requestJson, pretty = false)- sends a raw JSON request to the native bridge
- returns a JSON envelope string
sdk.configureSecureAccountStorage({ accountId, storagePath? })- recommended mobile entry point
- stores a random registry credential in iOS Keychain or Android Keystore
- the credential never crosses the React Native bridge
- selects an account-specific registry/database directory
sdk.configureAccountStorage({ accountId, passphrase, storagePath? })- advanced compatibility entry point for hosts that already protect the credential
- RPC:
configure_wallet_storage - selects an account-specific registry/database directory
- creates the registry with
passphraseif it does not exist - unlocks the existing registry with
passphraseif it already exists - clears loaded registry state, active wallet state, DB caches, and sync caches before switching namespaces
sdk.buildInfoJson(pretty = false)- raw JSON envelope for
get_build_info
- raw JSON envelope for
sdk.buildInfo()- RPC:
get_build_info - returns:
versiongitCommitbuildDaterustVersiontargetTriple
- RPC:
createPirateWalletSdk()- returns a new
PirateWalletSdkinstance backed by the linked native module
- returns a new
The typed JS methods below unwrap the native JSON envelope and return the result value directly.
All arrrtoshi amount values on the JSON wire are decimal strings, not JSON
numbers. This includes balances, transaction amounts, fees, pending
transaction totals, payment disclosure amounts, and parseAmount() results.
Amount request fields accept decimal strings, safe integer numbers, or
bigint; the JS wrapper serializes them as strings before calling native code.
walletRegistryExists()- RPC:
wallet_registry_exists - returns
boolean
- RPC:
listWallets()- RPC:
list_wallets - returns
WalletMeta[]
- RPC:
getActiveWalletId()- RPC:
get_active_wallet - returns
string | null
- RPC:
getActiveWallet()- helper over
getActiveWalletId()andlistWallets() - returns
WalletMeta | null
- helper over
getWallet(walletId)- helper over
listWallets() - returns
WalletMeta | null
- helper over
createWallet(requestOrName, birthdayHeight?)- RPC:
create_wallet - request fields:
name- optional
birthdayHeight - optional
mnemonicLanguage
- returns wallet id string
- RPC:
restoreWallet(requestOrName, mnemonic?, birthdayHeight?, mnemonicLanguage?)- RPC:
restore_wallet - request fields:
namemnemonic- optional
birthdayHeight - optional
mnemonicLanguage
- returns wallet id string
- RPC:
importViewingWallet(requestOrName, saplingViewingKey?, ironwoodViewingKey?, birthdayHeight)- RPC:
import_viewing_wallet - request fields:
name- optional
saplingViewingKey - optional
ironwoodViewingKey birthdayHeight
- returns wallet id string
- RPC:
switchWallet(walletId)- RPC:
switch_wallet - returns acknowledgement object
- RPC:
renameWallet(walletId, newName)- RPC:
rename_wallet - returns acknowledgement object
- RPC:
deleteWallet(walletId)- RPC:
delete_wallet - returns acknowledgement object
- RPC:
setWalletBirthdayHeight(walletId, birthdayHeight)- RPC:
set_wallet_birthday_height - returns acknowledgement object
- RPC:
getLatestBirthdayHeight(walletId)- helper over
getWallet(walletId) - returns
number | null
- helper over
Wallet metadata is stored in the backend registry. The registry also persists
an active wallet ID, which acts as the SDK's current-wallet pointer for flows
that need one. Integrations that already keep their own wallet selection can
call wallet-scoped methods directly with walletId.
switchWallet(walletId) updates the active-wallet pointer, records last-used
metadata, and stops sync for the previously active wallet. Multi-wallet sync
should be driven by wallet-scoped synchronizers rather than by switching the
active wallet between running wallets.
generateMnemonic(wordCount?, mnemonicLanguage?)- RPC:
generate_mnemonic - returns mnemonic string
- RPC:
validateMnemonic(mnemonic, mnemonicLanguage?)- RPC:
validate_mnemonic - returns
boolean
- RPC:
inspectMnemonic(mnemonic)- RPC:
inspect_mnemonic - returns:
isValiddetectedLanguageambiguousLanguageswordCount
- RPC:
getNetworkInfo()- RPC:
get_network_info - returns:
namecoinTyperpcPortdefaultBirthday
- RPC:
Endpoint configuration is wallet-scoped. The recommended integration flow is to test candidate servers, save one primary plus its alternates, and then start or restart that wallet's synchronizer:
const primary = 'https://lightd1.pirate.black:443'
const alternates = [
'https://lightwalletd1.cryptoforge.cc:443',
'https://pirate.mathnodes.com:443'
]
const tests = await Promise.all(
[primary, ...alternates].map(url => sdk.testLightdEndpoint({ url }))
)
if (tests.every(result => result.success)) {
await sdk.setLightdEndpointPool({
walletId,
url: primary,
failoverEndpoints: alternates
})
}
const saved = await sdk.getLightdEndpointConfig(walletId)getLightdEndpoint(walletId)- RPC:
get_lightd_endpoint - returns the effective primary endpoint URL
- RPC:
getLightdEndpointConfig(walletId)- RPC:
get_lightd_endpoint_config - returns
LightdEndpointConfig:hostportuseTlstlsPinlabelautomaticFailoverfailoverEndpointsisConfigured
- RPC:
testLightdEndpoint({ url, tlsPin? })- RPC:
test_node - also accepts
testLightdEndpoint(url, tlsPin?) - tests through the currently selected Direct, Tor, SOCKS5, or I2P transport
- reports success, height, latency, transport, TLS/pin information, server version, chain name, and any connection error
- RPC:
setLightdEndpoint({ walletId, url, tlsPin? })- RPC:
set_lightd_endpoint - also accepts
setLightdEndpoint(walletId, url, tlsPin?) - saves one primary and clears any previously configured failover pool
- RPC:
setLightdEndpointPool({ walletId, url, failoverEndpoints, tlsPin? })- RPC:
set_lightd_endpoint_pool - also accepts
setLightdEndpointPool(walletId, url, failoverEndpoints, tlsPin?) - saves the primary and up to 16 explicit alternates
- an empty
failoverEndpointsarray disables automatic failover
- RPC:
Pool membership is validated by the backend before anything is persisted.
Every member must resolve to the same recognized Pirate network, use the same
clearnet, onion, or I2P route, and use the same HTTP/TLS security mode as the
primary. The primary is removed from the alternate list and duplicate
alternates are collapsed. A pinned primary cannot use automatic failover,
because one server's SPKI pin cannot authenticate unrelated servers; use
setLightdEndpoint() when pinning a single server.
Saving either endpoint form cancels an existing sync session for that wallet so it cannot continue against stale connection state. Restart the synchronizer after the setter resolves. Pool candidates are still checked for compatible chain identity and history before failover or historical work is assigned; the array order is not a request to trust a candidate blindly.
testLightdEndpoint() returns a structured failure result for connection-level
failures. Invalid setter input or a rejected pool throws through the normal SDK
promise, so applications should show the error and retain the previous saved
configuration.
formatAmount(arrrtoshis)- RPC:
format_amount - returns formatted string
- RPC:
parseAmount(arrr)- RPC:
parse_amount - returns integer arrrtoshis as a decimal string
- RPC:
-
isValidShieldedAddr(address)- RPC:
is_valid_shielded_address - returns
boolean
- RPC:
-
validateAddress(address)- RPC:
validate_address - returns:
isValidaddressTypereason
- RPC:
-
validateConsensusBranch(walletId)- RPC:
validate_consensus_branch - returns:
sdkBranchIdserverBranchIdisValidhasServerBranchhasSdkBranchisServerNewerisSdkNewererrorMessage
Consensus branch IDs are opaque. Use
isValidfor compatibility; the two*Newerfields remain for wire compatibility and are alwaysfalse. - RPC:
Receive-address APIs are shielded and wallet-scoped:
getCurrentReceiveAddress(walletId)- helper over
getCurrentAddress(walletId)
- helper over
getCurrentAddress(walletId)- RPC:
current_receive_address - returns the current external receive address without rotating it
- RPC:
getNextReceiveAddress(walletId)- helper over
getNextAddress(walletId)
- helper over
getNextAddress(walletId)- RPC:
next_receive_address - rotates to and returns the next external receive address
- RPC:
listAddresses(walletId)- RPC:
list_addresses - returns generated external receive addresses
- RPC:
listAddressBalances(walletId, keyId?)- RPC:
list_address_balances - without
keyId, returns external receive-address balance entries only - with
keyId, also returns internal change-address entries for that key group
- RPC:
These APIs return shielded receive addresses. Newly generated addresses use
Sapling before Ironwood activation and Ironwood after activation. At activation,
both current- and next-address calls select Ironwood; existing Sapling addresses
remain valid, so listAddresses(walletId) can contain both pools over time.
Internal change is always included in getBalance(walletId). Do not sum an
unfiltered listAddressBalances(walletId) response to calculate the wallet
total, because its default external-only view intentionally omits internal
address rows.
getBalance(walletId)- RPC:
get_balance - returns decimal-string amount fields:
totalspendablepending
- RPC:
getShieldedPoolBalances(walletId)- RPC:
get_shielded_pool_balances - returns:
saplingironwood
- RPC:
getSpendabilityStatus(walletId)- RPC:
get_spendability_status - returns:
spendablerescanRequiredtargetHeightanchorHeightvalidatedAnchorHeightrepairQueuedreasonCode
- RPC:
reasonCode is a closed set:
OK: signing may proceedERR_SYNC_FINALIZING: scanning reached the tip but anchor validation is still finishingERR_WITNESS_REPAIR_QUEUED: witness repair is queued or actively processingERR_RESCAN_REQUIRED: imported key material or local state requires a historical replay
Keep the send action disabled unless spendable is true. A queued repair
remains visible until witness reconstruction and anchor validation both finish.
getLightdEndpointPoolDiagnostics(walletId)- RPC:
get_lightd_endpoint_pool_diagnostics - performs a live readiness and same-chain probe using the wallet's current transport
- returns the configured primary, selected active endpoint, failover mode, and per-endpoint health, tip, latency, and rejection reason
activeEndpointisnullwhen no configured candidate passes the complete probe
- RPC:
listTransactions(walletId, limit?)- RPC:
list_transactions - returns transaction array
- RPC:
fetchTransactionMemo(walletId, txId, outputIndex?)- RPC:
fetch_transaction_memo - returns
string | null
- RPC:
getTransactionDetails(walletId, txId)- RPC:
get_transaction_details - returns transaction detail object or
null
- RPC:
exportPaymentDisclosures(walletId, txId)- RPC:
export_payment_disclosures - returns all recoverable payment disclosures for a sent transaction
- RPC:
exportSaplingPaymentDisclosure(walletId, txId, outputIndex)- RPC:
export_sapling_payment_disclosure - returns one Sapling output disclosure string
- RPC:
exportIronwoodPaymentDisclosure(walletId, txId, actionIndex)- RPC:
export_ironwood_payment_disclosure - returns one Ironwood action disclosure string
- RPC:
verifyPaymentDisclosure(walletId, disclosure)- RPC:
verify_payment_disclosure - decrypts one Sapling or Ironwood disclosure using the wallet's configured lightwalletd endpoint
- RPC:
PaymentDisclosure includes disclosureType, txid, outputIndex, address,
amount, optional memo, and the shareable disclosure string.
verifyPaymentDisclosure returns the same decrypted payment fields plus
memoHex.
getFeeInfo()- RPC:
get_fee_info - returns decimal-string fee fields plus
memoFeeMultiplier:defaultFeeminFeemaxFeefeePerOutputmemoFeeMultiplier
- RPC:
startSync(walletIdOrRequest, mode = 'Compact')- RPC:
start_sync - request fields:
walletIdmode
- returns acknowledgement object
- RPC:
getSyncStatus(walletId)- RPC:
sync_status - returns:
localHeighttargetHeightpercentetastagelastCheckpointblocksPerSecondnotesDecryptedlastBatchMs
- RPC:
cancelSync(walletId)- RPC:
cancel_sync - returns acknowledgement object
- RPC:
rescan(walletIdOrRequest, fromHeight?)- RPC:
rescan - request fields:
walletIdfromHeight
- returns acknowledgement object
- RPC:
Each wallet has its own sync state. Apps can run more than one wallet sync by creating synchronizers for different wallet IDs, subject to normal device, network, and lightwalletd resource limits. Compact block ranges are cached per endpoint, so later scans for another wallet on the same endpoint can reuse previously fetched ranges.
buildTransaction(walletIdOrRequest, outputs?, fee?)- RPC:
build_tx - request fields:
walletIdoutputs- optional
fee
- each output contains:
addramount- optional
memo
- returns pending transaction object
- RPC:
signTransaction(walletId, pending)- RPC:
sign_tx - returns signed transaction object
- RPC:
broadcastTransaction(walletId, signed)- RPC:
broadcast_tx - uses the specified wallet's endpoint pool and repair state
- returns transaction id string
- RPC:
send(walletId, outputsOrOutput, fee?)- helper over
buildTransaction(),signTransaction(), andbroadcastTransaction() - returns transaction id string
- helper over
buildTransaction(), signTransaction(), broadcastTransaction(), and
send() are wallet-scoped. broadcastTransaction() requires the wallet ID so
endpoint selection and repair state always belong to the wallet that created
the transaction.
Wallet-scoped signing protection is opt-in and additive. Enabling it wraps the seed and spending keys with a second key derived from the Edge account session credential. Viewing keys and cached compact blocks remain available while the wallet is locked.
// Run once after creating or restoring this wallet.
await sdk.enableWalletSigningProtection(walletId, edgeAccountSessionCredential)
// Run after the Edge account is unlocked in a later app session.
await sdk.unlockWalletSigning(walletId, edgeAccountSessionCredential)
// Gate the send action on both status calls.
const signing = await sdk.getWalletSigningStatus(walletId)
const spendability = await sdk.getSpendabilityStatus(walletId)
// Run when the account locks, the app signs out, or protected state is cleared.
await sdk.lockWalletSigning(walletId)enableWalletSigningProtection(walletId, sessionCredential)performs the one-time atomic key rewrap and leaves that wallet unlocked for the sessionunlockWalletSigning(walletId, sessionCredential)installs only that wallet's signing key in memorygetWalletSigningStatus(walletId)returnsprotectionEnabledandunlockedlockWalletSigning(walletId)clears the credential and cached wallet database handleslockAllWalletSigning()clears all signing sessions and wallet database handles
Once protection is enabled, signTransaction() fails with
ERR_SIGNING_SESSION_LOCKED until the wallet is unlocked. Do not persist the
session credential in AsyncStorage, Redux persistence, logs, or crash reports.
Change-address selection is automatic. Sapling-only change uses legacy same-address change before Ironwood activation and Sapling internal change after activation; Ironwood spends or outputs use Ironwood internal change.
exportSaplingViewingKey(walletId)- RPC:
export_sapling_viewing_key - returns Sapling viewing key string
- RPC:
exportIronwoodViewingKey(walletId)- RPC:
export_ironwood_viewing_key - returns Ironwood viewing key string
- RPC:
importSaplingViewingKeyAsWatchOnly(requestOrName, saplingViewingKey?, birthdayHeight?)- RPC:
import_sapling_viewing_key_as_watch_only - returns wallet id string
- RPC:
getWatchOnlyCapabilities(walletId)- RPC:
get_watch_only_capabilities - returns capability object
- RPC:
These methods live under sdk.advancedKeyManagement.
listKeyGroups(walletId)- RPC:
list_key_groups - returns key group array
- RPC:
exportKeyGroupKeys(walletId, keyId)- RPC:
export_key_group_keys - returns:
keyIdsaplingViewingKeyironwoodViewingKeysaplingSpendingKeyironwoodSpendingKey
- RPC:
importSpendingKey(requestOrWalletId, birthdayHeight?, saplingSpendingKey?, ironwoodSpendingKey?)- RPC:
import_spending_key - returns key id number
- RPC:
exportSeed(walletId, mnemonicLanguage?)- RPC:
export_seed_raw - returns mnemonic string
- RPC:
Where mnemonicLanguage is supported, the accepted values are:
englishchinese_simplifiedchinese_traditionalfrenchitalianjapanesekoreanspanish
Behavior:
- if omitted during
restoreWallet()orvalidateMnemonic(), the backend attempts autodetection - if omitted during
exportSeed(), the wallet's original stored mnemonic language is used - if provided during export, the same seed entropy is re-rendered in the requested language
Create a synchronizer with:
createSynchronizer(walletId, config?)
Public state:
statusprogresssyncStatuslatestBirthdayHeightbalancetransactionslastError
Methods:
currentSnapshot()isRunning()isSyncing()isComplete()start()stop()refresh()close()subscribe(callbacks?)
stop() and close() both cancel backend sync for the wallet. In React Native code,
await synchronizer.close() instead of treating close() as a local timer-only cleanup step.
A synchronizer is scoped to one wallet ID. Create one synchronizer per wallet when running multi-wallet sync.
Callback hooks:
onStatusChangedonUpdateonError
Install the package in the app and run CocoaPods as usual:
npm install react-native-pirate-wallet
cd ios && pod installOn Android, npm installs the exact-version ARM and x86_64 companions automatically. The wrapper autolinks as a standard React Native native module and adds their JNI libraries to the build.
On macOS, npm installs the exact-version device and simulator companions.
During pod install, the podspec assembles and links
PirateWalletNative.xcframework from those packages.