Skip to content

Commit c26cdcb

Browse files
committed
Reimplement Pirate Chain plugin over react-native-pirate-wallet
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.
1 parent 834932b commit c26cdcb

9 files changed

Lines changed: 424 additions & 203 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- changed: (ARRR) Reimplement the Pirate Chain plugin over the unified `react-native-pirate-wallet` SDK, replacing `react-native-piratechain`.
6+
57
## 4.82.1 (2026-05-27)
68

79
- changed: (FIO) Replace forked `@fioprotocol/fiosdk` with official npm 1.10.3.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@
183183
"prettier": "^2.2.0",
184184
"process": "^0.11.10",
185185
"querystring": "^0.2.1",
186-
"react-native-piratechain": "0.5.0",
187186
"react-native-zano": "^0.2.7",
188187
"react-native-zcash": "0.10.1",
189188
"rimraf": "^3.0.2",
@@ -200,7 +199,7 @@
200199
"webpack-dev-server": "^4.11.1"
201200
},
202201
"peerDependencies": {
203-
"react-native-piratechain": "v0.5.0",
202+
"react-native-pirate-wallet": "^0.1.1",
204203
"react-native-zano": "^0.2.7",
205204
"react-native-zcash": "^0.10.1"
206205
}

src/piratechain/PiratechainEngine.ts

Lines changed: 68 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,12 @@ import {
1111
InsufficientFundsError,
1212
NoAmountSpecifiedError
1313
} from 'edge-core-js/types'
14-
import type {
15-
ConfirmedTransaction,
16-
SpendInfo,
17-
StatusEvent
18-
} from 'react-native-piratechain'
14+
import type { PirateTransaction } from 'react-native-pirate-wallet'
1915
import { base16, base64 } from 'rfc4648'
2016

2117
import { CurrencyEngine } from '../common/CurrencyEngine'
2218
import { PluginEnvironment } from '../common/innerPlugin'
23-
import { cleanTxLogs } from '../common/utils'
19+
import { cleanTxLogs, safeParseInt } from '../common/utils'
2420
import type { PiratechainIo, PiratechainSynchronizer } from './piratechainIo'
2521
import {
2622
makePiratechainSyncTracker,
@@ -44,9 +40,8 @@ export class PiratechainEngine extends CurrencyEngine<
4440
pluginId: string
4541
networkInfo: PiratechainNetworkInfo
4642
otherData!: PiratechainWalletOtherData
47-
synchronizerStatus!: StatusEvent['name']
43+
synchronizerStatus!: 'STOPPED' | 'SYNCING' | 'SYNCED'
4844
availableZatoshi!: string
49-
initialNumBlocksToDownload!: number
5045
birthdayHeight: number
5146
queryMutex: boolean
5247
makeSynchronizer: PiratechainIo['makeSynchronizer']
@@ -57,7 +52,6 @@ export class PiratechainEngine extends CurrencyEngine<
5752
synchronizer?: PiratechainSynchronizer
5853
synchronizerPromise: Promise<PiratechainSynchronizer>
5954
synchronizerResolver!: (synchronizer: PiratechainSynchronizer) => void
60-
lastUpdateFromSynchronizer?: number
6155

6256
constructor(
6357
env: PluginEnvironment<PiratechainNetworkInfo>,
@@ -85,17 +79,8 @@ export class PiratechainEngine extends CurrencyEngine<
8579
}
8680

8781
initData(): void {
88-
// walletLocalData
89-
if (this.otherData.blockRange.first === 0) {
90-
this.otherData.blockRange = {
91-
first: this.birthdayHeight,
92-
last: this.birthdayHeight
93-
}
94-
}
95-
9682
// Engine variables
97-
this.initialNumBlocksToDownload = -1
98-
this.synchronizerStatus = 'DISCONNECTED'
83+
this.synchronizerStatus = 'STOPPED'
9984
this.availableZatoshi = '0'
10085
}
10186

@@ -115,18 +100,13 @@ export class PiratechainEngine extends CurrencyEngine<
115100
this.synchronizerStatus = payload.name
116101
await this.queryAll()
117102
})
118-
this.synchronizer.on('error', async payload => {
103+
this.synchronizer.on('error', payload => {
104+
// The polling synchronizer retries transient errors on its own:
119105
this.log.warn(`Synchronizer error: ${payload.message}`)
120-
if (payload.level === 'critical') {
121-
await this.killEngine()
122-
this.lastUpdateFromSynchronizer = undefined
123-
await this.startEngine()
124-
}
125106
})
126107
}
127108

128109
async queryAll(): Promise<void> {
129-
this.lastUpdateFromSynchronizer = Date.now()
130110
if (this.queryMutex) return
131111
this.queryMutex = true
132112
try {
@@ -150,10 +130,10 @@ export class PiratechainEngine extends CurrencyEngine<
150130
async queryBalance(): Promise<void> {
151131
if (!this.isSynced() || this.synchronizer == null) return
152132
try {
153-
const balances = await this.synchronizer.getBalance()
154-
if (balances.totalZatoshi === '-1') return
155-
this.availableZatoshi = balances.availableZatoshi
156-
this.updateBalance(null, balances.totalZatoshi)
133+
const balance = await this.synchronizer.getBalance()
134+
// `total` includes pending; `spendable` is the confirmed balance:
135+
this.availableZatoshi = String(balance.spendable)
136+
this.updateBalance(null, String(balance.total))
157137
this.syncTracker.updateBalanceRatio(1)
158138
} catch (e: any) {
159139
this.warn('Failed to update balances', e)
@@ -164,42 +144,10 @@ export class PiratechainEngine extends CurrencyEngine<
164144
async queryTransactions(): Promise<void> {
165145
if (this.synchronizer == null) return
166146
try {
167-
let first = this.otherData.blockRange.first
168-
let last = this.otherData.blockRange.last
169-
const blocksToHeight =
170-
this.walletLocalData.blockHeight - this.birthdayHeight
171-
while (this.isSynced() && last <= this.walletLocalData.blockHeight) {
172-
const transactions = await this.synchronizer.getTransactions({
173-
first,
174-
last
175-
})
176-
177-
for (const tx of transactions) this.processTransaction(tx)
178-
179-
if (last === this.walletLocalData.blockHeight) {
180-
first = this.walletLocalData.blockHeight
181-
this.walletLocalDataDirty = true
182-
this.syncTracker.updateTransactionRatio(1)
183-
break
184-
}
185-
186-
first = last + 1
187-
last =
188-
last + this.networkInfo.transactionQueryLimit <
189-
this.walletLocalData.blockHeight
190-
? last + this.networkInfo.transactionQueryLimit
191-
: this.walletLocalData.blockHeight
192-
193-
this.otherData.blockRange = {
194-
first,
195-
last
196-
}
197-
this.walletLocalDataDirty = true
198-
199-
if (blocksToHeight > 0) {
200-
const historyRatio = (last - this.birthdayHeight) / blocksToHeight
201-
this.syncTracker.updateTransactionRatio(historyRatio)
202-
}
147+
const transactions = await this.synchronizer.getTransactions()
148+
for (const tx of transactions) this.processTransaction(tx)
149+
if (this.isSynced()) {
150+
this.syncTracker.updateTransactionRatio(1)
203151
}
204152
} catch (e: any) {
205153
this.error(
@@ -209,41 +157,39 @@ export class PiratechainEngine extends CurrencyEngine<
209157
}
210158
}
211159

212-
processTransaction(tx: ConfirmedTransaction): void {
213-
let netNativeAmount = tx.value
160+
processTransaction(tx: PirateTransaction): void {
161+
// A negative amount is a send and already includes the network fee:
162+
const netNativeAmount = String(tx.amount)
214163
const ourReceiveAddresses = []
215-
if (tx.toAddress != null) {
216-
// check if tx is a spend
217-
netNativeAmount = `-${add(
218-
netNativeAmount,
219-
this.networkInfo.defaultNetworkFee
220-
)}`
221-
} else {
164+
if (tx.amount >= 0) {
222165
ourReceiveAddresses.push(this.walletInfo.keys.publicKey)
223166
}
224167

225-
const edgeMemos: EdgeMemo[] = tx.memos
226-
.filter(text => text !== '')
227-
.map(text => ({
228-
memoName: 'memo',
229-
type: 'text',
230-
value: text
231-
}))
168+
const edgeMemos: EdgeMemo[] =
169+
tx.memo != null && tx.memo !== ''
170+
? [
171+
{
172+
memoName: 'memo',
173+
type: 'text',
174+
value: tx.memo
175+
}
176+
]
177+
: []
232178

233179
const edgeTransaction: EdgeTransaction = {
234-
blockHeight: tx.minedHeight,
180+
blockHeight: tx.height ?? 0,
235181
currencyCode: this.currencyInfo.currencyCode,
236-
date: tx.blockTimeInSeconds,
182+
date: tx.timestamp,
237183
isSend: netNativeAmount.startsWith('-'),
238184
memos: edgeMemos,
239185
nativeAmount: netNativeAmount,
240-
networkFee: this.networkInfo.defaultNetworkFee,
186+
networkFee: String(tx.fee),
241187
networkFees: [],
242188
otherParams: {},
243189
ourReceiveAddresses, // blank if you sent money otherwise array of addresses that are yours in this transaction
244190
signedTx: '',
245191
tokenId: null,
246-
txid: tx.rawTransactionId,
192+
txid: tx.txId,
247193
walletId: this.walletId
248194
}
249195
this.addTransaction(null, edgeTransaction)
@@ -256,26 +202,21 @@ export class PiratechainEngine extends CurrencyEngine<
256202
this.currencyInfo.pluginId
257203
)(opts?.privateKeys)
258204

259-
const { rpcNode } = this.networkInfo
260205
this.birthdayHeight = piratechainPrivateKeys.birthdayHeight
261206

262207
try {
263208
// Replace this.synchronizerPromise with a fresh promise. The old promise might have already been resolved
264209
this.synchronizerPromise = this.makeSynchronizer({
265-
mnemonicSeed: piratechainPrivateKeys.mnemonic,
266-
birthdayHeight: piratechainPrivateKeys.birthdayHeight,
267-
alias: base16.stringify(base64.parse(this.walletId)),
268-
...rpcNode
210+
name: base16.stringify(base64.parse(this.walletId)),
211+
mnemonic: piratechainPrivateKeys.mnemonic,
212+
birthdayHeight: piratechainPrivateKeys.birthdayHeight
269213
})
270214
this.synchronizer = await this.synchronizerPromise
271215
// People might be waiting on the old promise, so resolve that
272216
this.synchronizerResolver(this.synchronizer)
273217
} catch (e) {
274-
// The synchronizer cannot start if it isn't present.
275-
if (
276-
String(e) ===
277-
'Invariant Violation: `new NativeEventEmitter()` requires a non-null argument.'
278-
) {
218+
// The synchronizer cannot start if the native module isn't present:
219+
if (String(e).includes('native module is not linked')) {
279220
this.log.warn('SDK not present')
280221
} else throw e
281222
}
@@ -309,8 +250,10 @@ export class PiratechainEngine extends CurrencyEngine<
309250
await super.killEngine()
310251
await this.clearBlockchainCache()
311252
await this.startEngine()
312-
this.synchronizer
313-
?.rescan()
253+
this.synchronizerPromise
254+
.then(async synchronizer => {
255+
await synchronizer.rescan(this.birthdayHeight)
256+
})
314257
.catch((e: any) => this.warn('resyncBlockchain failed: ', e))
315258
this.initData()
316259
this.syncTracker.resetSync()
@@ -381,42 +324,42 @@ export class PiratechainEngine extends CurrencyEngine<
381324
}
382325

383326
async broadcastTx(
384-
edgeTransaction: EdgeTransaction,
385-
opts?: EdgeEnginePrivateKeyOptions
327+
edgeTransaction: EdgeTransaction
386328
): Promise<EdgeTransaction> {
387329
const { memos } = edgeTransaction
388-
const piratechainPrivateKeys = asPiratechainPrivateKeys(this.pluginId)(
389-
opts?.privateKeys
390-
)
391330
if (
392331
edgeTransaction.spendTargets == null ||
393332
edgeTransaction.spendTargets.length !== 1
394333
)
395334
throw new Error('Invalid spend targets')
396335

397-
const memo = memos[0]?.type === 'text' ? memos[0].value : ''
398336
const spendTarget = edgeTransaction.spendTargets[0]
399-
const txParams: SpendInfo = {
400-
zatoshi: sub(
401-
abs(edgeTransaction.nativeAmount),
402-
edgeTransaction.networkFee
403-
),
404-
toAddress: spendTarget.publicAddress,
405-
memo,
406-
mnemonicSeed: piratechainPrivateKeys.mnemonic
407-
}
337+
if (spendTarget.publicAddress == null)
338+
throw new Error('Missing publicAddress')
339+
340+
// The registry wallet holds the spending keys, so the send call
341+
// only needs the outputs. Edge's nativeAmount includes the fee:
342+
const memo = memos[0]?.type === 'text' ? memos[0].value : undefined
343+
const spendAmount = sub(
344+
abs(edgeTransaction.nativeAmount),
345+
edgeTransaction.networkFee
346+
)
408347

409348
try {
410349
const synchronizer = await this.synchronizerPromise
411-
const signedTx = await synchronizer.sendToAddress(txParams)
412-
if ('txId' in signedTx) {
413-
edgeTransaction.txid = signedTx.txId
414-
edgeTransaction.signedTx = signedTx.raw
415-
edgeTransaction.date = Date.now() / 1000
416-
this.warn(`SUCCESS broadcastTx\n${cleanTxLogs(edgeTransaction)}`)
417-
} else {
418-
throw new Error(signedTx.errorMessage)
419-
}
350+
const txid = await synchronizer.send(
351+
[
352+
{
353+
addr: spendTarget.publicAddress,
354+
amount: safeParseInt(spendAmount),
355+
memo
356+
}
357+
],
358+
safeParseInt(edgeTransaction.networkFee)
359+
)
360+
edgeTransaction.txid = txid
361+
edgeTransaction.date = Date.now() / 1000
362+
this.warn(`SUCCESS broadcastTx\n${cleanTxLogs(edgeTransaction)}`)
420363
} catch (e: any) {
421364
this.warn('FAILURE broadcastTx failed: ', e)
422365
throw e
@@ -427,11 +370,11 @@ export class PiratechainEngine extends CurrencyEngine<
427370
async getFreshAddress(): Promise<EdgeFreshAddress> {
428371
const getSynchronizerAddresses = async (): Promise<EdgeFreshAddress> => {
429372
const synchronizer = await this.synchronizerPromise
430-
const { saplingAddress } = await synchronizer.deriveUnifiedAddress()
431-
this.otherData.cachedAddress = saplingAddress
373+
const publicAddress = await synchronizer.getCurrentAddress()
374+
this.otherData.cachedAddress = publicAddress
432375
this.walletLocalDataDirty = true
433376
return {
434-
publicAddress: saplingAddress
377+
publicAddress
435378
}
436379
}
437380

0 commit comments

Comments
 (0)