Skip to content

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

Open
j0ntz wants to merge 8 commits into
masterfrom
agent/1214721783909451
Open

Reimplement Pirate Chain plugin over react-native-pirate-wallet#1055
j0ntz wants to merge 8 commits into
masterfrom
agent/1214721783909451

Conversation

@j0ntz

@j0ntz j0ntz commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

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

The piratechain team released a new SDK family for their orchard upgrade (Pirate-Unified-Light-Wallet), including react-native-pirate-wallet, which replaces the react-native-piratechain wrapper Edge wrote. The new SDK is not a drop-in replacement, so this PR reimplements the piratechain plugin on top of it.

Architecture changes driven by the new SDK:

  • Wallet registry instead of per-seed synchronizers. The native core persists wallets in its own encrypted registry. The io bridge restores each Edge wallet into the registry under the existing walletId alias name (idempotent lookup-by-name, serialized to avoid duplicate restores) and all wallet-scoped calls use the resulting registry wallet id.
  • Polling synchronizer. The SDK's JS synchronizer polls sync_status/get_balance/list_transactions natively, so the io bridge forwards its snapshots as the existing update/statusChanged/error yaob events. Block progress maps from localHeight/targetHeight.
  • Transaction history. list_transactions replaces block-range queries (the blockRange windowing in otherData is gone). Amounts are signed and fee-inclusive for sends, which matches Edge's nativeAmount semantics directly. Recipient addresses are not exposed by the SDK, so sends are detected by sign.
  • Sends. The registry wallet holds the spending keys, so broadcastTx sends outputs (plus the explicit default fee) through the SDK instead of passing the mnemonic per spend.
  • Addresses / keys. Receive addresses come from current_receive_address (still sapling zs1… — orchard is not active on mainnet). derivePublicKey exports the sapling viewing key after registering the wallet. New-wallet birthday heights are probed via a throwaway create_wallet, whose null-birthday path resolves the live chain tip with a checkpoint fallback.
  • Endpoints. The lightwalletd endpoint and checkpoints are baked into the native core, so rpcNode in networkInfo is no longer consumed (kept for info-server payload compatibility).

The plugin compiles against local typings (rnPirateWallet.d.ts) because react-native-pirate-wallet is not published to npm; it only exists as a 473 MB zip asset on the PirateNetwork GitHub release (prebuilt xcframework + jniLibs). GUI integration is blocked on a packaging decision — npm/yarn cannot install zips, so the package needs to be hosted as a tgz (e.g. the zano-utils-js EdgeApp-release pattern) or published before edge-react-gui can swap react-native-piratechain for it. Mnemonic/seed derivation is BIP-39-compatible with the old SDK (same sapling addresses), so existing wallets restore in place; first launch re-restores into the registry from the stored mnemonic + birthday and rescans from the wallet's birthday height.


Note

High Risk
Complete rewrite of ARRR wallet storage, sync, and send paths, including a new device-scoped encrypted registry passphrase and plaintext gRPC lightwalletd traffic.

Overview
Replaces the crashing react-native-piratechain module with the unified react-native-pirate-wallet SDK so ARRR wallets can sync and spend without the old SQLite double-open crash.

Storage and sync move to one device-scoped encrypted registry (edge-pirate-device) keyed by a random per-device passphrase in local storage. Every ARRR wallet lives in that registry and runs its own synchronizer concurrently over a shared block cache. Amounts are string-encoded end to end for full precision, and sends go through the SDK send() API.

Also fixes silent sync stalls by pointing each wallet at the plugin's own lightwalletd node (http://lightd1.pirate.black:9067) via set_lightd_endpoint, since the SDK ignores plugin config and its default node had stopped serving blocks.

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

Upstream SDK fixes

Two defects in the upstream SDK had to be fixed for this plugin to sync and send. They are now filed upstream as code (not just a prose recipe): PirateNetwork/Pirate-Unified-Light-Wallet#19.

  1. Sync-killing per-call runtimeWalletService::execute_blocking dropped its tokio runtime as soon as each call returned, aborting the background sync engine spawned by start_sync. A host that drives the service through execute_blocking (the RN binding) never advanced past the birthday height. PR Properly call onTransactionsChanged on new ETH txs #19 uses a process-wide persistent runtime.
  2. tx-payload camelization — the RN binding camelized the opaque pending payload from build_tx before feeding it back into sign_tx, which rejected it (missing field total_amount). This plugin works around it here via raw build_tx/sign_tx/broadcast_tx invoke; PR Properly call onTransactionsChanged on new ETH txs #19 fixes it at the source so the wrapper's send() works too.

Until those land in an upstream release, the vendored react-native-pirate-wallet used by EdgeApp/edge-react-gui#6021 carries the patched runtime slice.


v1.1.5 reconciliation (2026-07-28)

Reconciled to the officially released Pirate Unified Light Wallet v1.1.5 (RN binding 0.2.0), which merges the upstream fixes (PR #19) and finalizes the wire format:

  • Amounts as decimal strings end to end (AmountString), so balances and sends above 2^53-1 arrrtoshi keep full precision. Dropped safeParseInt on the send path.
  • Sends go through the SDK send() now that it preserves the opaque build/sign/broadcast payloads and normalizes amounts, replacing the manual raw-invoke camelize workaround.
  • Registry storage via the new configureAccountStorage, replacing the removed global app-passphrase flow. This phase used one namespace per wallet; the device-scoped model that superseded it is described below.
  • onError typed unknown, and the orchard to ironwood rename in the SDK type surface.
  • react-native-pirate-wallet peerDependency bumped to ^0.2.0.

Design doc: src/docs/piratechain-sdk-v115-reconcile.md

On-device verification (iOS sim, edge-funds): the app runs with piratechain enabled (no corePlugins disable), ARRR wallets sync the shielded chain with no crash (the old module's SQLite double-open crash is retired), and a real ARRR send broadcast to the transaction-success scene, txid 34ba68b0fee76668790ef7dae32f374c7f378da589022a1034f1112e234e49cd.


Device-scoped registry (2026-08-03)

Rebased onto master (4.82.1 to 4.87.0, including the yarn to npm migration) and restructured storage after the Pirate Chain team confirmed the intended model.

configure_wallet_storage is global: one registry namespace is active at a time, and switching it cancels any running sync and clears the registry and block caches. The per-wallet namespaces above therefore could not support more than one ARRR wallet, which reproduced on device: only the last-selected wallet synced and stayed spendable, while the others' pollers read the wrong namespace. The intended model is one namespace per device holding many wallets, with concurrency coming from wallet-scoped synchronizers.

  • One device registry. configureAccountStorage runs once, at DEVICE_ACCOUNT_ID = 'edge-pirate-device', memoized so concurrent wallet starts share a single setup. Wallets are keyed by alias inside it. The throwaway probe namespace is gone; wallet-free reads use the device registry.
  • Passphrase is a per-device random secret. base16(io.random(32)), minted on first use and persisted to piratechain/devicePassphrase.json on the core disklet (new piratechainDeviceStorage.ts, replacing piratechainCrypto.ts). A seed-derived HMAC cannot key a registry that holds many wallets without arbitrarily privileging one wallet's key material. Existence is checked before reading, so a transient read failure cannot silently mint a new secret and orphan the registry.
  • Handoff is lazy. PiratechainTools.ensureDevicePassphrase() hands the bridge the secret once, awaited by every path that reaches storage. It deliberately does not run in makeCurrencyTools, so constructing tools never depends on the native module being linked.
  • Concurrent sync. No namespace switching remains, so each wallet's synchronizer runs alongside the others. Registry mutations (restore, and the probe wallet's create/delete) still serialize under a lock.
  • Missed-SYNCED backstop (the deferred Bugbot finding): the bridge exposes getStatus(), and the engine reads it once after subscribing, adopting it only when no event has arrived. A statusChanged that fired between start() and subscription can no longer strand the engine at STOPPED.
  • Chain tip without mutating the registry. getLatestNetworkHeight now reads getSyncStatus(walletId).targetHeight from a wallet that is already registered, and keeps the old create-and-delete probe only for an empty registry. Found on device: with the registry shared, the probe's create_wallet mutates it underneath running synchronizers and the native service panics inside pirate_wallet_service_invoke_json, aborting the app (panic_cannot_unwind -> SIGABRT). Creating a new ARRR wallet reproduced it twice and succeeds after the fix.

Old per-wallet registries are abandoned rather than migrated: wallets re-restore from their seeds on first run.

Design doc updated: src/docs/piratechain-sdk-v115-reconcile.md (decision 1 and decision 3 rewritten, phase 4 recorded).

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
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 Outdated
@j0ntz

j0ntz commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence: e2e ARRR send (iOS sim)

arrr my pirate2 synced

arrr my pirate2 synced

arrr send dest zaddr

arrr send dest zaddr

arrr send confirm

arrr send confirm

arrr send success

arrr send success

arrr send txid

arrr send txid

Captured by the agent's in-app test run (build-and-test).

Comment thread src/piratechain/piratechainIo.ts
j0ntz added 4 commits August 3, 2026 17:13
The piratechain team's orchard upgrade replaces the zcash-cloned
react-native-piratechain SDK with a wallet-registry based SDK
(react-native-pirate-wallet) whose lightwalletd endpoint, checkpoints,
and spending keys live inside the native core. Rebuild the engine,
tools, and yaob io bridge on that API: wallets are restored into the
SDK registry under the Edge walletId alias, sync progress comes from
the SDK's polling synchronizer, transactions map from signed
fee-inclusive amounts, and sends go through the registry wallet
instead of passing the mnemonic per spend.
The Pirate Unified Light Wallet v1.1.5 release ships the merged upstream
fixes and finalizes the wire format the plugin targets. Serialize amounts
as decimal strings end to end so balances and sends above 2^53-1 arrrtoshi
keep full precision, and drive sends through the SDK's send() now that it
keeps the opaque build/sign/broadcast payloads verbatim. Replace the
removed global app-passphrase flow with per-wallet configureAccountStorage
so each local wallet lives in its own registry namespace unlocked by a
passphrase derived from that wallet's seed instead of a shared hardcoded
one. Type native bridge errors as unknown to match what the RN bridge
actually delivers.
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 2cbed88 to 6d4e32d Compare August 4, 2026 03:04
Comment thread src/piratechain/piratechainDeviceStorage.ts
j0ntz added 2 commits August 3, 2026 21:31
The SDK's configureAccountStorage selects the wallet registry globally: one
namespace is active at a time, and switching cancels any running sync and
clears the registry and block caches. Per-wallet namespaces therefore left
only the last-selected ARRR wallet syncing, with the others' pollers reading
the wrong namespace, and gave up the shared block cache.

Configure a single device-scoped registry once, key every wallet by alias
inside it, and let each wallet's synchronizer run concurrently. Its passphrase
is a random per-device secret minted from io.random and kept in the plugin's
local storage, replacing the seed-derived HMAC, which cannot key a registry
that holds many wallets. Wallet-free reads use the device registry, so the
throwaway probe namespace is gone.

Also expose the synchronizer status from the bridge and read it once after the
engine subscribes, so a SYNCED that fires before subscription cannot strand
the engine at STOPPED.
Rewrite the storage section and decisions 1 and 3 to the shipped model, and
record the phase-4 divergence from the per-wallet namespaces phase 2 built.
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 6d4e32d to 76bb601 Compare August 4, 2026 04:33
@j0ntz

j0ntz commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence

arrr synced

arrr synced

arrr send ready

arrr send ready

arrr sync stalled

arrr sync stalled

Captured by the agent's in-app test run (build-and-test).

@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

Comment thread src/piratechain/PiratechainEngine.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.

Cursor Bugbot has reviewed your changes using high 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.

Reviewed by Cursor Bugbot for commit 59e95bb. Configure here.

Comment thread src/piratechain/PiratechainEngine.ts Outdated
j0ntz added 2 commits August 11, 2026 20:36
The SDK bakes in a default lightwalletd and never consults the plugin's
networkInfo, so every wallet scanned against that default. When that node
stops serving blocks the failure is silent and total: test_node still
succeeds and the chain tip still resolves, so the app shows 'Sync in
Progress, 0% Complete' forever with no error, while the scan sits in the
Headers stage at zero blocks per second.

Pass the configured node down to makeSynchronizer and apply it with
set_lightd_endpoint before the synchronizer starts. The configured port
moves from 443 to the node's plain gRPC port, which is what the SDK
speaks; test_node fails against https on 443 and succeeds on 9067.
@j0ntz
j0ntz force-pushed the agent/1214721783909451 branch from 59e95bb to a58a7ed Compare August 12, 2026 03:40
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.

2 participants