Skip to content

Reimplement Pirate Chain plugin over react-native-pirate-wallet - #1055

Merged
j0ntz merged 4 commits into
masterfrom
agent/1214721783909451
Sep 19, 2026
Merged

j0ntz merged 4 commits into
masterfrom
agent/1214721783909451

Conversation

@j0ntz

@j0ntz j0ntz commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

Technical Design Document

piratechain-unified-wallet-sdk.md

, committed on this branch. It holds the design, every decision with its rejected alternatives, the phase history, and the test evidence. Read it instead of a long description here.

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Asana: https://app.asana.com/0/1215088146871429/1214721783909451

Reimplements the Pirate Chain plugin over react-native-pirate-wallet (npm 0.3.4), the Pirate team's replacement for the react-native-piratechain module Edge wrote. In the old module the Swift and Rust layers each opened the wallet's SQLite database and the app crashed when they overlapped, which is why unrelated work has to set piratechain: false in the GUI's corePlugins.ts. The new SDK keeps all database access in Rust, and this branch retires that workaround.

The plugin's public shape and the engine's transaction mapping are unchanged. What is new:

  • One device-scoped SDK wallet registry holds every ARRR wallet, keyed by wallet-id alias. The native module mints the registry credential and keeps it in the iOS Keychain or Android Keystore; it never reaches JavaScript. Each wallet's signing keys are wrapped under a second credential derived from the mnemonic; protection is enabled the first time a wallet is seen and the key is unlocked only for the span of a send, so a registry at rest can sync but cannot sign.
  • A send is refused, with the reason, until the SDK reports the wallet spendable; the two retryable broadcast errors map to the same wait-and-retry message.
  • Sync comes from the SDK's polling synchronizer and history from list_transactions. Amounts cross the wire as decimal strings, and sends go through the SDK's send().
  • Each wallet is pointed at Edge's own lightwalletd node with set_lightd_endpoint. Without it a wallet scans against the SDK's baked-in default, which no longer answers at all, and the app sits at 0% sync with no error anywhere. That node is now https://lightwalletd2.cryptoforge.cc:443: TLS instead of plain gRPC, and measurably faster under 0.3.2 (below).
  • getLatestNetworkHeight takes the chain tip from a registered wallet or from test_node, falling back to the create-and-delete probe only when the registry is empty. Running that probe while a synchronizer is live panics the native service and aborts the app.
  • A wallet takes a pool of lightwalletd endpoints rather than one, through set_lightd_endpoint_pool, and it now ships with an alternate (lightwalletd1.cryptoforge.cc) rather than empty. The SDK validates a pool and rejects one that names an unrecognized host or mixes transports; a rejected pool falls back to the single endpoint rather than failing the wallet, which both rejections were observed doing on device. The info server can still replace the list without an app release.

The plugin compiles against the package's own index.d.ts: react-native-pirate-wallet is a devDependency like the other native SDKs, the hand-written ambient declaration is gone, create-hmac is a declared dependency with @types/create-hmac, and the endpoint calls go through the typed testLightdEndpoint, setLightdEndpoint and setLightdEndpointPool; raw invoke remains for set_tunnel only. The bridge exposes start and the engine calls it after subscribing, so the first status change is never missed. getMaxSpendable refuses for the same reason makeSpend does, and a transaction whose processing throws is retried on the next poll.

Paired with EdgeApp/edge-react-gui#6021, which swaps the GUI dependency and has to land after this one publishes.

Verified on the iOS simulator, with the mocha suite at 490 passing.

A controlled cold-sync A/B settled whether to ship the 0.3.2 bump. One wallet 1,056,918 blocks behind, every other ARRR wallet archived, the same simulator and session, each side forced cold and confirmed cold by watching the block cache grow from zero, and the SDK swapped by replacing libpirate_ffi_native.a and relinking so the static archive was the only variable:

SDK node cold time to 100%
0.3.0 http://lightd1.pirate.black:9067 72 s
0.3.2 http://lightd1.pirate.black:9067 ~30 min projected (15.86% in 5.6 min)
0.3.2 https://lightwalletd2.cryptoforge.cc:443 95 s

The first two rows read as a 20-40x regression in 0.3.2, which was the working verdict until the third row was measured. 0.3.2 is not slow; the pair of 0.3.2 and lightd1.pirate.black is. So this branch ships the bump and moves the endpoint, which also retires the plaintext transport the plugin's own comment had flagged as an exposure.

The plugin also gates spending on the SDK's get_spendability_status. A wallet reports SYNCED before its spend anchor is usable, and a send in that window used to build, get confirmed by the user, and only then fail inside the SDK, which the app rendered as "check your network connection". makeSpend now reads the status and throws PendingFundsError while the wallet cannot spend, so the send scene holds no transaction and its confirm slider stays disabled. A status the plugin cannot read falls through to the old SYNCED-only gate rather than blocking the send. A wallet that has not reached SYNCED is refused with the same PendingFundsError, so the amount modal and the send scene's error card show one message for every wait.

Testing showed the status is necessary and not sufficient, so broadcastTx restates ERR_SYNC_FINALIZING and ERR_WITNESS_REPAIR_QUEUED the same way: on 2026-08-31 a wallet reading spendable: true, repairQueued: false, reasonCode: OK with a validated anchor still had four broadcasts rejected by the node on anchor grounds, and the status did not move afterwards. No principal or fee was lost. Section 10 of the doc carries that back to the Pirate Chain team as the open question.

Both configurations then broadcast a funded ARRR send to the transaction-success scene: 0.3.0 at txid d4bc2294a427e9c0f197c5ccc21d4ac2687d28fc5f4f3a718f77c757bc130585, and the shipping configuration at txid 239ab8743afbed4f8047ed4e0b05d2aa3e39ebfa7ab9824ddfa52c72c47c4205. Section 7 of the doc has the full matrix, the node survey, the SDK's pool-validation rules, and what stayed uncovered (automatic failover under a stalling primary, which the SDK's recognized-host rule makes untestable with a local proxy). Section 10 is a report for the Pirate Chain team.


Note

High Risk
This replaces the entire ARRR native stack and changes registry credentials, signing-key lifecycle, sync endpoints, and send/broadcast paths—core wallet and funds-handling behavior.

Overview
Pirate Chain (ARRR) moves off the crash-prone react-native-piratechain module onto the Pirate team's unified react-native-pirate-wallet (^0.3.4). The engine and IO bridge are rebuilt around that SDK while keeping the plugin's outward API the same.

Sync and network behavior now force Edge's TLS lightwalletd (lightwalletd2.cryptoforge.cc) with an optional failover pool (lightwalletdFailoverUrls, overridable from the info server). Wallets use a single device registry (configureSecureAccountStorage / Keychain or Keystore) plus signing protection unlocked only during send via an HMAC-derived credential from the mnemonic.

Spending is gated on SDK spendability (get_spendability_status) and pre-sync checks throw PendingFundsError with wait-and-retry copy; broadcastTx maps ERR_SYNC_FINALIZING / ERR_WITNESS_REPAIR_QUEUED the same way. Sends use the SDK send() path with string amounts; history comes from the poller's full list with per-tx cleaning. start() runs only after subscriptions attach, and chain-tip lookup avoids mutating a live registry (empty-registry probe only).

Dependencies add create-hmac; peer/dev dependency swaps react-native-pirate-wallet. A large piratechain-unified-wallet-sdk.md design doc and CHANGELOG entries document the migration.

Reviewed by Cursor Bugbot for commit 057ad63. Bugbot is set up for automated code reviews on this repo. Configure here.


Test evidence

390ef73
Rebuild Pirate Chain on react-native-pirate-wallet

arrr my pirate2 synced

arrr send dest zaddr

arrr send confirm

arrr send success

arrr send txid

arrr synced

arrr send ready

arrr sync stalled

arrr synced 0.3.0

arrr send ready

arrr send success

arrr balance after send

arrr synced 0.3.2 tls pool

arrr synced 0.3.0

arrr send ready

arrr send success

arrr txid

arrr send success 0.3.2 tls

arrr txid 0.3.2 tls

synced max fee slider armed

transaction history
8118f87
Gate ARRR sends on SDK spendability

before generic error

gate passed slider armed

not spendable error

arrr sync in progress

presync refusal error card
428d199
Log the ARRR endpoint pool diagnostics
🪓 forced the bridge's getSpendability in the updot-baked plugin copy to return spendable:false with reasonCode ERR_WITNESS_REPAIR_QUEUED (frames 04, 05) then ERR_SYNC_FINALIZING (frame 06); reverted, git status clean.

keychain registry syncing

gate passed slider armed

transaction success

txid

max fills spendable balance

send armed

transaction success

🪓 max refused witness repair

🪓 not spendable witness repair

🪓 not spendable sync finalizing

send armed fresh registry

transaction success fresh registry

Comment thread src/piratechain/PiratechainEngine.ts Outdated
Comment thread src/piratechain/piratechainIo.ts Outdated
Comment thread src/piratechain/piratechainIo.ts
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 9df27fb to c26cdcb Compare June 5, 2026 20:16
Comment thread src/piratechain/PiratechainEngine.ts
Comment thread src/piratechain/piratechainIo.ts
@j0ntz

j0ntz commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Simulator-tested update (force-pushed as part of the single commit):

  • App passphrase unlock: the SDK's SQLCipher store rejects every wallet call with Security error: App is locked until set_app_passphrase/unlock_app runs. The io bridge now unlocks (or provisions) the passphrase store before any wallet operation. Verified on the iOS sim: registry restore, viewing-key derivation, and zs1… receive address all work.
  • Direct tunnel: the SDK transports through Tor by default, which never bootstraps inside Edge — sync sat at stage: Headers, target_height: 0 forever. The bridge now issues set_tunnel {mode: Direct} after unlock, matching every other Edge plugin's connectivity. Verified the sync config flips to transport: Direct against http://64.23.167.130:9067 (server healthy; tip 3,990,043 via grpcurl).

Upstream blocker found while testing: block sync still cannot progress through this binding. pirate-ffi-native builds a throwaway current-thread tokio runtime per invoke, and sync_control::start_sync spawns the sync engine onto it — the task is destroyed when the call returns. The Flutter app works only because flutter_rust_bridge keeps a persistent runtime; the RN/iOS/Android SDK bindings all share this defect, and the bounded start_background_sync API is not exposed through the JSON dispatcher. Needs a Pirate-team fix (persistent global runtime in pirate-ffi-native, or expose start_background_sync via invoke). Until then balances/sends can't be exercised end-to-end.

@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 7049511 to 0481ded Compare June 5, 2026 23:23
@j0ntz

j0ntz commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

End-to-end sync + send verified on the iOS simulator (with a locally rebuilt pirate core carrying the one-line persistent-runtime fix):

  • Funded wallet (254.547 ARRR) restored from its Edge mnemonic + birthday and fully scanned ~940k blocks to chain tip; SDK balance matched Edge's cached pre-upgrade balance to the arrrtoshi.
  • Sent 3.261 ARRR wallet-to-wallet in the same account: txid b44367658e7c92cd916eccbdee3dc8f6861a48985683037040a8ead2035c9881, broadcast accepted by http://64.23.167.130:9067.

Two more findings, both addressed in the latest force-push:

  1. Wrapper send bug (upstream): sdk.send()/signTransaction() camelize the build_tx result and feed it back into sign_tx, which rejects it (Invalid request JSON: missing field 'total_amount'). The bridge now drives build_tx → sign_tx → broadcast_tx over the raw invoke channel so the pending/signed payloads round-trip untouched.
  2. Runtime defect confirmed + fix validated: rebuilding libpirate_ffi_native with a persistent global tokio runtime in WalletService::execute_blocking (instead of a per-call current-thread runtime) makes foreground sync work through the json-invoke bindings. That's the patch to upstream to the Pirate team — without it the stock release binaries cannot sync outside their Flutter app.

Comment thread src/piratechain/PiratechainEngine.ts

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two non-blocking suggestions on the new react-native-pirate-wallet integration, both at the native-bridge boundary. The plugin reimplementation reads cleanly otherwise.

Comment thread src/piratechain/rnPirateWallet.d.ts Outdated
Comment thread src/piratechain/PiratechainEngine.ts
Comment thread src/piratechain/piratechainIo.ts Outdated
Comment thread src/piratechain/piratechainIo.ts Outdated
Comment thread src/piratechain/PiratechainEngine.ts Outdated
Comment thread src/piratechain/PiratechainEngine.ts
Comment thread src/piratechain/PiratechainEngine.ts
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch 2 times, most recently from a0927f1 to beaae5d Compare July 29, 2026 01:06
Comment thread src/piratechain/piratechainIo.ts
Comment thread src/piratechain/piratechainIo.ts Outdated
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 2cbed88 to 6d4e32d Compare August 4, 2026 03:04
Comment thread src/piratechain/piratechainDeviceStorage.ts Outdated
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 6d4e32d to 76bb601 Compare August 4, 2026 04:33
Comment thread src/piratechain/PiratechainEngine.ts Outdated
Comment thread src/piratechain/PiratechainEngine.ts Outdated
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 59e95bb to a58a7ed Compare August 12, 2026 03:40
Comment thread package.json Outdated
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 25992e1 to 983e6b0 Compare August 17, 2026 22:13
Comment thread src/piratechain/piratechainDeviceStorage.ts Outdated
Comment thread src/piratechain/piratechainIo.ts
Comment thread src/piratechain/piratechainIo.ts
Comment thread src/piratechain/piratechainIo.ts Outdated
Comment thread src/piratechain/piratechainIo.ts
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 983e6b0 to 70ea0d9 Compare August 17, 2026 22:43

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/piratechain/PiratechainEngine.ts

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/piratechain/piratechainIo.ts
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 5128245 to e71029a Compare September 7, 2026 20:51

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/piratechain/piratechainIo.ts Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Agentic security review for this head found one medium issue: signing protection is not applied at restore and fail-opens if the signing-session RPCs throw, which can leave ARRR spending keys wrapped only by the device registry credential.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread src/piratechain/piratechainIo.ts
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch 2 times, most recently from 6ba9f1b to 04fdfd6 Compare September 8, 2026 20:07

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/piratechain/piratechainIo.ts Outdated

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local review of the head commit. The first two comments (rnPirateWallet.d.ts and package.json) are one coupled change — the hand-written ambient declaration wins over node_modules, so the devDependency only helps once the file is gone. Everything else stands alone. I skipped the queryMutex boolean, which cursor already raised at line 123.

Comment thread src/piratechain/rnPirateWallet.d.ts Outdated
Comment thread package.json
Comment thread src/declare-modules.d.ts Outdated
Comment thread src/piratechain/PiratechainEngine.ts
Comment thread src/piratechain/PiratechainEngine.ts Outdated
Comment thread src/piratechain/piratechainIo.ts Outdated
Comment thread src/piratechain/PiratechainEngine.ts Outdated
Comment thread src/piratechain/piratechainInfo.ts Outdated
Comment thread src/piratechain/piratechainIo.ts Outdated
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 4ba24aa to 866a558 Compare September 15, 2026 00:55

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 866a558. Configure here.

Comment thread src/piratechain/piratechainIo.ts
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch 2 times, most recently from 855af01 to 359221e Compare September 15, 2026 02:33

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 359221e5. All nine earlier threads are fixed as described; tsc and eslint pass with types now resolved from node_modules/react-native-pirate-wallet/src/index.d.ts and @types/create-hmac. Two new comments on code the fixes introduced — the first also corrects a premise in my earlier getMaxSpendable comment.

Comment thread src/piratechain/PiratechainEngine.ts Outdated
Comment thread src/piratechain/piratechainIo.ts
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch 2 times, most recently from 428d199 to fda1f6c Compare September 16, 2026 22:30
@j0ntz

j0ntz commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author
  • fa8ea18: pre-sync refusal throws PendingFundsError
  • 74f202a: per-entry transaction cleaning, plus the design doc update

The Pirate team's unified wallet replaces the zcash-cloned
react-native-piratechain module, in which the Swift and Rust layers
each opened the wallet's SQLite database and the app crashed when they
overlapped. The new SDK keeps every database access in Rust and holds
the lightwalletd endpoint, checkpoints and spending keys inside its
native core. Rebuild the engine, tools and yaob io bridge on that API,
against react-native-pirate-wallet 0.3.4 from npm as a peer dependency.
The typings in rnPirateWallet.d.ts declare the 0.3.4 surface the
plugin consumes.

Registry: the SDK's namespace is selected globally, and switching
cancels any running sync and clears the registry and block caches, so
the bridge configures one device-scoped registry once at init and
holds every ARRR wallet inside it under its Edge walletId alias, each
wallet's synchronizer running concurrently over the shared block cache.
The native module mints the registry credential and holds it in the
iOS Keychain or the Android Keystore through configureSecureAccountStorage,
so it never crosses the bridge. Wallet-free reads use the same registry,
and the chain tip comes from a registered wallet's sync status, then
from test_node against the configured node, before the create-and-delete
probe that the native service cannot survive while a synchronizer runs.

Nodes: the SDK bakes in a default lightwalletd and never consults the
plugin's networkInfo, so the bridge points each wallet at the plugin's
own nodes before its synchronizer starts: lightwalletd2.cryptoforge.cc
over TLS as the primary, with lightwalletd1 as the alternate in an
automatic-failover pool through set_lightd_endpoint_pool. The SDK
refuses a pool that mixes transports or names a host it does not
recognize, and a rejected pool falls back to the single endpoint. The
info server can replace the list without an app release.

Sync progress comes from the SDK's polling synchronizer, with the
status read once after the engine subscribes so an early SYNCED cannot
strand the engine at STOPPED. Amounts cross the wire as decimal strings
so balances above 2^53-1 arrrtoshi keep full precision, and sends go
through the SDK's send(), which builds, signs and broadcasts through
the registry wallet instead of passing the mnemonic per spend.

The design, its decisions with their rejected alternatives, the test
evidence and the phase history are in
src/docs/piratechain-unified-wallet-sdk.md.
A Pirate Chain wallet reports SYNCED before its spend anchor is
usable, so a send in that window built, was confirmed by the user,
and then failed inside the SDK with ERR_SYNC_FINALIZING, which the
app rendered as a network error.

makeSpend now reads get_spendability_status and throws
PendingFundsError while the wallet cannot spend, so the send scene
holds no transaction and keeps its confirm slider disabled. The
status's reasonCode is a closed set (OK, ERR_SYNC_FINALIZING,
ERR_WITNESS_REPAIR_QUEUED, ERR_RESCAN_REQUIRED) and the message
branches on it, with the two booleans as the fallback for a status
that arrives without one. A broadcast that still fails with one of the
two retryable codes is restated the same way, so the advice is to wait
and retry rather than to check the network. A status the plugin cannot
read falls through to the SYNCED-only gate.
SDK 0.3.4 can wrap a wallet's seed and spending keys under a second key
derived from a credential the host supplies, holding that key in memory
only, so a locked registry still syncs but cannot sign. Edge has no
account-session secret to hand a currency plugin, so the credential is
an HMAC of the wallet mnemonic under a fixed domain: the engine holds
the mnemonic exactly while the account is unlocked, which puts the
SDK's lock on Edge's schedule. Protection is enabled once per wallet,
unlocked on every synchronizer start and again before each send, and
locked when the synchronizer stops. Sync never depends on it, so a
failed signing call logs and a later send reports the SDK's own reason.
SDK 0.3.4 adds a live probe of the configured lightwalletd pool that
reports which node it would select and why it rejected the rest. A
wallet that never syncs otherwise looks the same whether every node is
down or the pool was refused, so the engine logs the probe once per
start, off the sync path, at warn level so it reaches the app's
persisted logs and ships with a user's log export.
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from fda1f6c to 057ad63 Compare September 19, 2026 01:01
@j0ntz
j0ntz enabled auto-merge September 19, 2026 01:01
@j0ntz
j0ntz merged commit 373bd8b into master Sep 19, 2026
4 checks passed
@j0ntz
j0ntz deleted the agent/1214721783909451 branch September 19, 2026 01:10
j0ntz added a commit that referenced this pull request Sep 19, 2026
@j0ntz
j0ntz restored the agent/1214721783909451 branch September 19, 2026 01:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants