zano-native 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/LICENSE +27 -0
  3. package/README.md +24 -0
  4. package/android/build.gradle +40 -0
  5. package/android/src/main/java/app/edge/rnzano/RnZanoModule.java +73 -0
  6. package/android/src/main/java/app/edge/rnzano/RnZanoPackage.java +21 -0
  7. package/android/src/main/jniLibs/arm64-v8a/librnzano.so +0 -0
  8. package/ios/ZanoModule.h +4 -0
  9. package/ios/ZanoModule.mm +138 -0
  10. package/ios/ZanoModule.xcframework/Info.plist +43 -0
  11. package/ios/ZanoModule.xcframework/ios-arm64/libzano-module.a +0 -0
  12. package/ios/ZanoModule.xcframework/ios-arm64-simulator/libzano-module.a +0 -0
  13. package/ios/react-native-zano.xcodeproj/project.pbxproj +1 -0
  14. package/lib/scripts/build-native-host.d.ts +1 -0
  15. package/lib/scripts/build-native-host.js +175 -0
  16. package/lib/scripts/smoke-node.d.ts +1 -0
  17. package/lib/scripts/smoke-node.js +33 -0
  18. package/lib/scripts/update-sources.d.ts +1 -0
  19. package/lib/scripts/update-sources.js +412 -0
  20. package/lib/scripts/utils/android-tools.d.ts +1 -0
  21. package/lib/scripts/utils/android-tools.js +25 -0
  22. package/lib/scripts/utils/closeWalletPatch.d.ts +23 -0
  23. package/lib/scripts/utils/closeWalletPatch.js +215 -0
  24. package/lib/scripts/utils/common.d.ts +37 -0
  25. package/lib/scripts/utils/common.js +186 -0
  26. package/lib/scripts/utils/ios-tools.d.ts +8 -0
  27. package/lib/scripts/utils/ios-tools.js +26 -0
  28. package/lib/scripts/utils/sdkFolders.d.ts +27 -0
  29. package/lib/scripts/utils/sdkFolders.js +43 -0
  30. package/lib/src/CppBridge.d.ts +142 -0
  31. package/lib/src/CppBridge.js +668 -0
  32. package/lib/src/index.d.ts +4 -0
  33. package/lib/src/index.js +28 -0
  34. package/lib/src/load-addon.d.ts +5 -0
  35. package/lib/src/load-addon.js +50 -0
  36. package/lib/src/node.d.ts +11 -0
  37. package/lib/src/node.js +25 -0
  38. package/lib/src/types.d.ts +292 -0
  39. package/lib/src/types.js +40 -0
  40. package/lib/src/walletFilePassword.d.ts +14 -0
  41. package/lib/src/walletFilePassword.js +74 -0
  42. package/node.d.ts +5 -0
  43. package/node.js +2 -0
  44. package/package.json +105 -0
  45. package/prebuilds/darwin-arm64/zano.node +0 -0
  46. package/src/node/zano-napi.cpp +176 -0
  47. package/src/zano-wrapper/zano-methods.hpp +15 -0
  48. package/zano-native.podspec +27 -0
@@ -0,0 +1,668 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CppBridge = void 0;
4
+ const types_1 = require("./types");
5
+ const walletFilePassword_1 = require("./walletFilePassword");
6
+ function isWrongPassword(error) {
7
+ return error instanceof types_1.ZanoError && error.code === 'WRONG_PASSWORD';
8
+ }
9
+ function isInvalidFile(error) {
10
+ return error instanceof types_1.ZanoError && error.code === 'INVALID_FILE';
11
+ }
12
+ function isAlreadyExists(error) {
13
+ return error instanceof types_1.ZanoError && error.code === 'ALREADY_EXISTS';
14
+ }
15
+ class CppBridge {
16
+ constructor(zanoModule) {
17
+ // Whether `configurePostponedRun` has succeeded. The native flag it sets
18
+ // is process-wide and sticky, so one success covers every later call:
19
+ this.postponedRunConfigured = false;
20
+ // The native side omits `documentDirectory` when it could not create the
21
+ // wallet directory or exclude it from device backups. That directory
22
+ // holds the seed and spend keys, so a missing value must stop the bridge
23
+ // here rather than let every path below concatenate `undefined` into a
24
+ // storage path the SDK would happily create somewhere unprotected.
25
+ if (zanoModule.documentDirectory == null ||
26
+ zanoModule.documentDirectory === '') {
27
+ throw new types_1.ZanoError('INTERNAL_ERROR', 'Zano native module reported no document directory');
28
+ }
29
+ this.documentDirectory = zanoModule.documentDirectory;
30
+ this.module = zanoModule;
31
+ }
32
+ // -----------------------------------------------------------------------------
33
+ // Raw API
34
+ // -----------------------------------------------------------------------------
35
+ async init(rpcAddress, logLevel) {
36
+ const response = await this.module.callZano('init', [
37
+ rpcAddress,
38
+ this.documentDirectory,
39
+ logLevel.toFixed()
40
+ ]);
41
+ return JSON.parse(response);
42
+ }
43
+ async initWithIpPort(ip, port, logLevel) {
44
+ const response = await this.module.callZano('initWithIpPort', [
45
+ ip,
46
+ port,
47
+ this.documentDirectory,
48
+ logLevel.toFixed()
49
+ ]);
50
+ return JSON.parse(response);
51
+ }
52
+ async reset() {
53
+ const response = await this.module.callZano('reset', []);
54
+ return JSON.parse(response);
55
+ }
56
+ async setLogLevel(logLevel) {
57
+ return await this.module.callZano('setLogLevel', [logLevel.toFixed()]);
58
+ }
59
+ async getVersion() {
60
+ return await this.module.callZano('getVersion', []);
61
+ }
62
+ async getWalletFiles() {
63
+ const files = await this.module.callZano('getWalletFiles', []);
64
+ return JSON.parse(files);
65
+ }
66
+ async getExportPrivateInfo(targetDir) {
67
+ const response = await this.module.callZano('getExportPrivateInfo', [
68
+ targetDir
69
+ ]);
70
+ return JSON.parse(response);
71
+ }
72
+ async deleteWallet(fileName) {
73
+ const response = await this.module.callZano('deleteWallet', [fileName]);
74
+ return JSON.parse(response);
75
+ }
76
+ async getAddressInfo(addr) {
77
+ const response = await this.module.callZano('getAddressInfo', [addr]);
78
+ return JSON.parse(response);
79
+ }
80
+ async getAppconfig(encryptionKey) {
81
+ const response = await this.module.callZano('getAppconfig', [encryptionKey]);
82
+ return JSON.parse(response);
83
+ }
84
+ async setAppconfig(confStr, encryptionKey) {
85
+ const response = await this.module.callZano('setAppconfig', [
86
+ confStr,
87
+ encryptionKey
88
+ ]);
89
+ return JSON.parse(response);
90
+ }
91
+ async generateRandomKey(length) {
92
+ return await this.module.callZano('generateRandomKey', [length.toFixed()]);
93
+ }
94
+ async getLogsBuffer() {
95
+ return await this.module.callZano('getLogsBuffer', []);
96
+ }
97
+ async truncateLog() {
98
+ const response = await this.module.callZano('truncateLog', []);
99
+ return JSON.parse(response);
100
+ }
101
+ async getConnectivityStatus() {
102
+ const response = await this.module.callZano('getConnectivityStatus', []);
103
+ return JSON.parse(response);
104
+ }
105
+ /**
106
+ * Raw native open. Note that once `startWallet` or `generateSeedPhrase`
107
+ * has run, the process-wide postponed-run mode is configured and stays on:
108
+ * a wallet opened here will not sync until `run_wallet` is issued for it
109
+ * (via `syncCall`). Prefer `startWallet`.
110
+ */
111
+ async open(path, password) {
112
+ const response = await this.module.callZano('open', [path, password]);
113
+ return JSON.parse(response);
114
+ }
115
+ /**
116
+ * Raw native restore. Subject to the same postponed-run caveat as `open`:
117
+ * under postponed mode the restored wallet will not sync until
118
+ * `run_wallet` is issued for it.
119
+ */
120
+ async restore(seed, path, password, seedPassword) {
121
+ const response = await this.module.callZano('restore', [
122
+ seed,
123
+ path,
124
+ password,
125
+ seedPassword
126
+ ]);
127
+ return JSON.parse(response);
128
+ }
129
+ /**
130
+ * Raw native generate. Subject to the same postponed-run caveat as `open`:
131
+ * under postponed mode the generated wallet will not sync until
132
+ * `run_wallet` is issued for it.
133
+ */
134
+ async generate(path, password) {
135
+ const response = await this.module.callZano('generate', [path, password]);
136
+ return JSON.parse(response);
137
+ }
138
+ async getOpenedWallets() {
139
+ const response = await this.module.callZano('getOpenedWallets', []);
140
+ return JSON.parse(response);
141
+ }
142
+ async getWalletStatus(walletId) {
143
+ const response = await this.module.callZano('getWalletStatus', [
144
+ walletId.toFixed()
145
+ ]);
146
+ return JSON.parse(response);
147
+ }
148
+ async closeWallet(walletId) {
149
+ const response = await this.module.callZano('closeWallet', [
150
+ walletId.toFixed()
151
+ ]);
152
+ return JSON.parse(response);
153
+ }
154
+ async invoke(walletId, params) {
155
+ return await this.module.callZano('invoke', [walletId.toFixed(), params]);
156
+ }
157
+ async asyncCall(methodName, instanceId, params) {
158
+ const response = await this.module.callZano('asyncCall', [
159
+ methodName,
160
+ instanceId.toFixed(),
161
+ params
162
+ ]);
163
+ return JSON.parse(response);
164
+ }
165
+ async tryPullResult(arg) {
166
+ const response = await this.module.callZano('tryPullResult', [
167
+ arg.toFixed()
168
+ ]);
169
+ return JSON.parse(response);
170
+ }
171
+ async syncCall(methodName, instanceId, params) {
172
+ return await this.module.callZano('syncCall', [
173
+ methodName,
174
+ instanceId.toFixed(),
175
+ params
176
+ ]);
177
+ }
178
+ /**
179
+ * Tells the native library not to start a wallet's refresh worker as part
180
+ * of `open`/`restore`/`generate`; `runWallet` starts it explicitly. The
181
+ * flag is process-wide and sticky, so every open made after this call must
182
+ * be followed by `runWallet` once the wallet should sync -- and one
183
+ * success is enough, so this short-circuits instead of paying a native
184
+ * round trip per wallet start. Requires `init` to have run.
185
+ */
186
+ async configurePostponedRun() {
187
+ if (this.postponedRunConfigured)
188
+ return;
189
+ const response = await this.syncCall('configure', 0, JSON.stringify({ postponed_run_wallet: true }));
190
+ // Same `syncCall` primitive as `runWallet`, same quirk: one native
191
+ // failure path answers with a bare return-code string rather than JSON,
192
+ // so a parse failure is a failure report, not a protocol surprise:
193
+ let parsed;
194
+ try {
195
+ parsed = JSON.parse(response);
196
+ }
197
+ catch (error) {
198
+ throw new Error(`Zano configure returned ${response}`);
199
+ }
200
+ if (parsed.status !== 'OK') {
201
+ throw new Error(`Zano configure returned ${response}`);
202
+ }
203
+ this.postponedRunConfigured = true;
204
+ }
205
+ /**
206
+ * Starts the refresh worker for an open wallet. Idempotent: the native
207
+ * side skips the spawn when the worker is already running.
208
+ *
209
+ * Public because adopting a wallet is public behavior: `startWallet`
210
+ * rethrows ALREADY_EXISTS for its caller to recover from, and the wallet
211
+ * the caller then adopts was opened with the refresh worker postponed, so
212
+ * it does not sync until this runs.
213
+ */
214
+ async runWallet(walletId) {
215
+ const response = await this.syncCall('run_wallet', walletId, '');
216
+ // One native failure path answers with a bare return-code string rather
217
+ // than JSON (the postponed main worker failing to start), so a parse
218
+ // failure is a failure report, not a protocol surprise:
219
+ let parsed;
220
+ try {
221
+ parsed = JSON.parse(response);
222
+ }
223
+ catch (error) {
224
+ throw new Error(`Zano run_wallet returned ${response}`);
225
+ }
226
+ if (parsed.error_code !== 'OK') {
227
+ throw new Error(`Zano run_wallet returned ${response}`);
228
+ }
229
+ }
230
+ async isWalletExist(path) {
231
+ const response = await this.module.callZano('isWalletExist', [
232
+ this.documentDirectory + '/wallets/' + path
233
+ ]);
234
+ return response === '1';
235
+ }
236
+ async getWalletInfo(walletId) {
237
+ const response = await this.module.callZano('getWalletInfo', [
238
+ walletId.toFixed()
239
+ ]);
240
+ return JSON.parse(response);
241
+ }
242
+ async resetWalletPassword(walletId, password) {
243
+ return await this.module.callZano('resetWalletPassword', [
244
+ walletId.toFixed(),
245
+ password
246
+ ]);
247
+ }
248
+ // 0 (default), 1 (unimportant), 2 (normal), 3 (elevated), 4 (priority)
249
+ async getCurrentTxFee(priority) {
250
+ const fee = await this.module.callZano('getCurrentTxFee', [
251
+ priority.toFixed()
252
+ ]);
253
+ return parseInt(fee);
254
+ }
255
+ // -----------------------------------------------------------------------------
256
+ // Convenience API
257
+ // -----------------------------------------------------------------------------
258
+ async getSeedPhraseInfo(seed, seedPassword) {
259
+ const params = {
260
+ seed_phrase: seed,
261
+ seed_password: seedPassword
262
+ };
263
+ const seedInfo = await this._asyncCallWithRetry('get_seed_phrase_info', 0, JSON.stringify(params));
264
+ return seedInfo;
265
+ }
266
+ async generateSeedPhrase(rpcAddress, storagePath, seedPassword, logLevel = -1) {
267
+ await this.init(rpcAddress, logLevel);
268
+ // Native `generate` auto-starts the wallet's refresh worker unless
269
+ // postponed-run is configured first, and the worker holds the per-wallet
270
+ // mutex for the whole of each refresh. The `closeWallet` below takes that
271
+ // same mutex on React Native's shared native-module queue, so without
272
+ // this the create-wallet path stalls every native call in the app for as
273
+ // long as the refresh runs. The flag is process-wide, so this matters
274
+ // whenever `generateSeedPhrase` is the first call to configure it.
275
+ await this.configurePostponedRun();
276
+ const response = await this.generate(storagePath, seedPassword);
277
+ const result = this.expectWallet(this.handleRpcResponse(response));
278
+ const { response: closeResponse } = await this.closeWallet(result.wallet_id);
279
+ if (closeResponse !== 'OK') {
280
+ // The file is still open in this process, so deleting it would leave a
281
+ // dangling handle. Leaving it is safe: `startWallet` re-keys or
282
+ // rebuilds whatever it finds.
283
+ throw new Error(`closeWallet returned ${closeResponse}`);
284
+ }
285
+ // `generate` writes a wallet file as a side effect, encrypted with
286
+ // `seedPassword` -- typically the empty string. The caller only wants
287
+ // the seed, so remove the file; the first `startWallet` recreates it
288
+ // via `restore`, encrypted with the derived password. If this delete
289
+ // silently fails (the native layer reports OK regardless), the leftover
290
+ // file is re-keyed by `startWallet`'s migration instead.
291
+ await this.deleteWallet(storagePath);
292
+ return result;
293
+ }
294
+ /**
295
+ * Opens the wallet file at `storagePath`, creating it from the mnemonic if
296
+ * it does not exist.
297
+ *
298
+ * The file on disk is encrypted with a password derived from the mnemonic,
299
+ * never with `seedPassword`. Versions 0.3.0 and earlier used `seedPassword`
300
+ * for both roles, so files written by them were keyed with the seed
301
+ * passphrase -- the empty string for most wallets. A file still encrypted
302
+ * that way is re-keyed in place the first time it opens. A file that no
303
+ * known password opens, or that the SDK cannot parse a wallet out of at
304
+ * all - the leavings of a crash during the file's first write - is
305
+ * deleted and rebuilt from the mnemonic, costing one re-scan - but only
306
+ * for a wallet with no seed passphrase. With one set, the passphrase is
307
+ * far and away the likeliest thing to be wrong, and rebuilding would
308
+ * restore a different wallet over a file that was intact, so that case
309
+ * throws instead.
310
+ *
311
+ * The migration is decided entirely by what the file does, so it is
312
+ * idempotent and self-healing: an interrupted re-key leaves the file on
313
+ * its old password for the next attempt.
314
+ */
315
+ async startWallet(mnemonicSeed, seedPassword, storagePath, opts = {}) {
316
+ const log = opts.log ?? (() => { });
317
+ const filePassword = (0, walletFilePassword_1.deriveWalletFilePassword)(mnemonicSeed);
318
+ // An auto-run open starts the refresh worker, which takes the per-wallet
319
+ // lock for the entire first catch-up scan -- minutes for a wallet that is
320
+ // weeks behind. The migration's `resetWalletPassword` then blocks on that
321
+ // lock, and since it runs on React Native's shared native-module queue,
322
+ // every native call in the app queues behind it for the whole scan.
323
+ // Open without running instead, and start the worker explicitly once the
324
+ // wallet this method returns is the one that should sync.
325
+ await this.configurePostponedRun();
326
+ const started = async (wallet) => {
327
+ await this.runWallet(wallet.wallet_id);
328
+ return wallet;
329
+ };
330
+ const openWith = async (password) => {
331
+ const wallet = this.expectWallet(this.handleRpcResponse(await this.open(storagePath, password)));
332
+ if (wallet.recovered) {
333
+ // The keys decrypted but the body did not, so the native layer wiped
334
+ // the history and will re-scan. It is also the signature of a file
335
+ // left half-encrypted by a botched re-key.
336
+ log('Zano wallet file was recovered; its history will re-sync');
337
+ }
338
+ return wallet;
339
+ };
340
+ const restoreFresh = async () => this.expectWallet(this.handleRpcResponse(await this.restore(mnemonicSeed, storagePath, filePassword, seedPassword)));
341
+ const rebuild = async () => {
342
+ await this.deleteWallet(storagePath);
343
+ // The native delete reports OK whether or not it removed anything, so
344
+ // confirm before restoring. `restore` onto a file that survived answers
345
+ // ALREADY_EXISTS, which callers recover from by adopting the open
346
+ // wallet -- and nothing is open here, so they would find none and
347
+ // rebuild again on every start. Failing here says what went wrong once.
348
+ // Ask the filesystem rather than the listing: `getWalletFiles` can
349
+ // answer without an `items` field, and treating that as proof the file
350
+ // is gone would restore onto a survivor anyway.
351
+ if (await this.isWalletExist(storagePath)) {
352
+ throw new Error('Could not delete the Zano wallet file');
353
+ }
354
+ return await restoreFresh();
355
+ };
356
+ const files = await this.getWalletFiles();
357
+ const exists = 'items' in files && files.items.includes(storagePath);
358
+ if (!exists) {
359
+ return await started(await restoreFresh());
360
+ }
361
+ try {
362
+ return await started(await openWith(filePassword));
363
+ }
364
+ catch (error) {
365
+ // A file the SDK cannot parse a wallet header out of - zero bytes
366
+ // after a crash during its first write, or other corruption - fails
367
+ // with INVALID_FILE before any password is consulted, so the password
368
+ // ladder below has nothing to probe. Without this branch the error
369
+ // propagated as-is and the engine retried the same doomed open
370
+ // forever. Route it to the same policy as a file no password opens:
371
+ // without a passphrase, the rebuild recreates the identical wallet at
372
+ // the cost of a re-scan; with one, an unreadable file cannot
373
+ // corroborate the passphrase, and a wrong one would rebuild a
374
+ // different wallet, so refuse.
375
+ if (isInvalidFile(error)) {
376
+ if (seedPassword !== '') {
377
+ throw new Error('The Zano wallet file is unreadable, and cannot be rebuilt ' +
378
+ 'because a seed passphrase is set');
379
+ }
380
+ log('Zano wallet file is unreadable, rebuilding it');
381
+ return await started(await rebuild());
382
+ }
383
+ // Anything other than a bad password -- including ALREADY_EXISTS,
384
+ // which callers recover from by adopting the open wallet -- is not
385
+ // ours to handle.
386
+ if (!isWrongPassword(error))
387
+ throw error;
388
+ }
389
+ // Files written by 0.3.0 and earlier are keyed with the seed passphrase,
390
+ // which was '' unless the user set one:
391
+ const legacyPasswords = seedPassword === '' ? [''] : [seedPassword, ''];
392
+ for (const legacy of legacyPasswords) {
393
+ if (legacy === filePassword)
394
+ continue;
395
+ let wallet;
396
+ try {
397
+ wallet = await openWith(legacy);
398
+ }
399
+ catch (error) {
400
+ if (!isWrongPassword(error))
401
+ throw error;
402
+ continue;
403
+ }
404
+ // Whether we still hold this wallet open. Rebuilding deletes the file,
405
+ // so it is only safe once nothing has it open.
406
+ let held = true;
407
+ try {
408
+ // `reset_wallet_password` only assigns the in-memory password. The
409
+ // file is re-encrypted when the wallet next stores, which closing it
410
+ // does.
411
+ //
412
+ // Do not replace this with a `store(path, password)` call: the SDK
413
+ // encrypts the keys blob with the argument but the body with the
414
+ // in-memory password, producing a file that opens, fails to
415
+ // deserialize its body, wipes the history and silently re-scans.
416
+ const resetCode = await this.resetWalletPassword(wallet.wallet_id, filePassword);
417
+ if (resetCode !== 'OK') {
418
+ throw new Error(`resetWalletPassword returned ${resetCode}`);
419
+ }
420
+ // `closeWallet`, not `stopWallet`: the async 'close' path discards
421
+ // the native return code and always reports OK, so it cannot tell
422
+ // us whether the file was actually written.
423
+ const { response } = await this.closeWallet(wallet.wallet_id);
424
+ if (response !== 'OK') {
425
+ throw new Error(`closeWallet returned ${response}`);
426
+ }
427
+ held = false;
428
+ // Only believe the migration once the file really opens with the
429
+ // new password:
430
+ const migrated = await started(await openWith(filePassword));
431
+ log('Zano wallet file re-keyed with a derived password');
432
+ return migrated;
433
+ }
434
+ catch (error) {
435
+ // Someone else has this wallet open, and callers recover from that by
436
+ // adopting it, exactly as the first probe allows. Deleting the file
437
+ // would pull it out from under that handle.
438
+ if (isAlreadyExists(error))
439
+ throw error;
440
+ // Past the close, the file on disk really is re-keyed -- both
441
+ // `resetWalletPassword` and `closeWallet` reported OK. Only a wrong
442
+ // password on the confirming open says otherwise. Anything else is a
443
+ // failure to confirm a file that is almost certainly correct, and
444
+ // rebuilding would spend a full rescan replacing it with its twin.
445
+ if (!held && !isWrongPassword(error))
446
+ throw error;
447
+ log(`Zano wallet file re-key failed: ${String(error)}`);
448
+ if (held) {
449
+ try {
450
+ const { response } = await this.closeWallet(wallet.wallet_id);
451
+ if (response !== 'OK') {
452
+ throw new Error(`closeWallet returned ${response}`);
453
+ }
454
+ }
455
+ catch (closeError) {
456
+ // We cannot delete a file this process still has open. Leaving it
457
+ // alone is safe: it is still keyed with the legacy password, so
458
+ // the next start simply tries the migration again.
459
+ log(`Zano wallet file re-key could not release the wallet, leaving the file alone: ${String(closeError)}`);
460
+ throw error;
461
+ }
462
+ }
463
+ // Rebuilding restores from the mnemonic with `seedPassword`, which
464
+ // reproduces this wallet only if that is the passphrase the file was
465
+ // written with. Opening it with `seedPassword` proves exactly that;
466
+ // opening it with '' does not, and 0.3.0 wrote '' for wallets it
467
+ // believed had no passphrase. When the two disagree, the file we just
468
+ // opened is the user's wallet and the rebuild would not be, so leave
469
+ // it alone -- the same rule the post-loop path applies.
470
+ if (legacy !== seedPassword) {
471
+ throw new Error('The Zano wallet file was written without this seed passphrase, so it cannot be rebuilt with one');
472
+ }
473
+ log('Rebuilding the Zano wallet file');
474
+ return await started(await rebuild());
475
+ }
476
+ }
477
+ // Every password this package has ever written has now been tried: the
478
+ // derived one, the seed passphrase, and the empty string 0.3.0 used for
479
+ // wallets without one. Reaching here with a passphrase set means it does
480
+ // not belong to this file, and rebuilding would restore from the mnemonic
481
+ // with that same passphrase -- a different wallet, written over a file
482
+ // that was fine. Zano's checksum rejects most wrong passphrases outright,
483
+ // so the usual outcome would be a deleted file and a failed restore; the
484
+ // rest of the time it is a wallet whose keys are not the user's, opening
485
+ // cleanly ever after because the file password comes from the mnemonic
486
+ // alone and so cannot tell the two apart.
487
+ if (seedPassword !== '') {
488
+ throw new Error('The Zano wallet file does not open with this seed passphrase');
489
+ }
490
+ log('Zano wallet file opens with no known password, rebuilding it');
491
+ return await started(await rebuild());
492
+ }
493
+ async stopWallet(walletId) {
494
+ const closeResponse = await this._asyncCallWithRetry('close', walletId, '');
495
+ if (closeResponse.return_code !== 'OK') {
496
+ throw new Error(`${closeResponse.return_code}`);
497
+ }
498
+ return closeResponse.return_code;
499
+ }
500
+ async removeWallet(walletId) {
501
+ const response = await this.getOpenedWallets();
502
+ const result = this.handleRpcResponse(response);
503
+ const wallet = result.find(w => w.wallet_id === walletId);
504
+ if (wallet == null)
505
+ return;
506
+ await this.stopWallet(walletId);
507
+ await this.deleteWallet(wallet.wi.path);
508
+ }
509
+ async walletStatus(walletId) {
510
+ const walletStatus = await this._asyncCallWithRetry('get_wallet_status', walletId, '');
511
+ return walletStatus;
512
+ }
513
+ async getBalances(walletId) {
514
+ const params = {
515
+ method: 'getbalance'
516
+ };
517
+ const response = await this._asyncCallWithRetry('invoke', walletId, JSON.stringify(params));
518
+ const result = this.handleRpcResponse(response);
519
+ return result;
520
+ }
521
+ /**
522
+ * Fetches a page of wallet history. Uses `get_recent_txs_and_info3`, the
523
+ * HF6-ready endpoint: the v2 variant reports only the deprecated
524
+ * transaction-wide payment id - an empty string for every id created
525
+ * since HF6, when ids moved into individual outputs - and its legacy
526
+ * serializer refuses entries carrying more than one distinct per-output
527
+ * id, which the HF6 migration guide says to expect.
528
+ */
529
+ async getTransactions(walletId, offset = 0) {
530
+ const params = {
531
+ method: 'get_recent_txs_and_info3',
532
+ params: {
533
+ count: 100,
534
+ exclude_mining_txs: true,
535
+ exclude_unconfirmed: false,
536
+ offset,
537
+ order: 'FROM_BEGIN_TO_END',
538
+ update_provision_info: true
539
+ }
540
+ };
541
+ const response = await this._asyncCallWithRetry('invoke', walletId, JSON.stringify(params));
542
+ const result = this.handleRpcResponse(response);
543
+ return result;
544
+ }
545
+ async whitelistAssets(walletId, assetIds) {
546
+ const currentWhitelistParams = {
547
+ method: 'assets_whitelist_get',
548
+ params: {}
549
+ };
550
+ const whitelistAssetsResponse = await this._asyncCallWithRetry('invoke', walletId, JSON.stringify(currentWhitelistParams));
551
+ let whitelistSet = new Set();
552
+ if ('local_whitelist' in whitelistAssetsResponse) {
553
+ whitelistSet = new Set(whitelistAssetsResponse.local_whitelist.map(asset => asset.asset_id));
554
+ }
555
+ for (const assetId of assetIds) {
556
+ if (!whitelistSet.has(assetId)) {
557
+ const addAssetParams = {
558
+ method: 'assets_whitelist_add',
559
+ params: {
560
+ asset_id: assetId
561
+ }
562
+ };
563
+ await this._asyncCallWithRetry('invoke', walletId, JSON.stringify(addAssetParams));
564
+ }
565
+ }
566
+ }
567
+ async transfer(walletId, opts) {
568
+ const params = {
569
+ method: 'transfer',
570
+ params: {
571
+ destinations: opts.transfers.map(t => ({
572
+ address: t.recipient,
573
+ amount: t.nativeAmount,
574
+ asset_id: t.assetId
575
+ })),
576
+ comment: opts.comment,
577
+ fee: opts.fee,
578
+ // Since HF6, payment ids travel inside integrated addresses, one
579
+ // per destination, and the wallet attaches each embedded id
580
+ // natively -- a single transaction may pay several integrated
581
+ // addresses carrying different ids. This request-level field is the
582
+ // old transaction-wide mechanism, and the node rejects any
583
+ // non-empty value outright. A caller with a separate payment id
584
+ // must fold it into an integrated destination address first.
585
+ payment_id: '',
586
+ hide_receiver: true,
587
+ mixin: 15,
588
+ push_payer: false,
589
+ service_entries_permanent: true
590
+ }
591
+ };
592
+ const response = await this._asyncCallWithRetry('invoke', walletId, JSON.stringify(params));
593
+ const result = this.handleRpcResponse(response);
594
+ return result.tx_hash;
595
+ }
596
+ async burnAsset(walletId, opts) {
597
+ const params = {
598
+ method: 'burn_asset',
599
+ params: {
600
+ asset_id: opts.assetId,
601
+ burn_amount: opts.burnAmount,
602
+ native_amount: opts.nativeAmount ?? 0,
603
+ point_tx_to_address: opts.pointTxToAddress ?? '',
604
+ service_entries: opts.serviceEntries ?? []
605
+ }
606
+ };
607
+ const response = await this._asyncCallWithRetry('invoke', walletId, JSON.stringify(params));
608
+ const result = this.handleRpcResponse(response);
609
+ return result.tx_id;
610
+ }
611
+ // -----------------------------------------------------------------------------
612
+ // Utils
613
+ // -----------------------------------------------------------------------------
614
+ /**
615
+ * Validates that a wallet payload really carries a wallet handle.
616
+ * Guards the paths that must not mistake a degenerate payload for an
617
+ * open wallet.
618
+ */
619
+ expectWallet(wallet) {
620
+ if (typeof wallet.wallet_id !== 'number') {
621
+ throw new types_1.ZanoError('INTERNAL_ERROR', 'Response carried no wallet_id');
622
+ }
623
+ return wallet;
624
+ }
625
+ handleRpcResponse(json) {
626
+ if ('error' in json) {
627
+ throw new types_1.ZanoError(String(json.error.code), json.error.message);
628
+ }
629
+ if (!('result' in json) || json.result == null) {
630
+ throw new types_1.ZanoError('INTERNAL_ERROR', 'Unknown error');
631
+ }
632
+ // The native layer's catch-all macros report failure as a
633
+ // success-shaped payload: `{result: {return_code: "INTERNAL_ERROR ..."}}`
634
+ // from PLAIN_WALLET_CATCH, or `"UNINITIALIZED"` when `init` has not run.
635
+ // Passing those through would hand callers a result whose real fields
636
+ // are all undefined.
637
+ const returnCode = json.result.return_code;
638
+ if (typeof returnCode === 'string' && returnCode !== 'OK') {
639
+ throw new types_1.ZanoError(returnCode);
640
+ }
641
+ return json.result;
642
+ }
643
+ async _asyncCallWithRetry(methodName, instanceId, params) {
644
+ while (true) {
645
+ const jobIdResponse = await this.asyncCall(methodName, instanceId, params);
646
+ while (true) {
647
+ const tryPullResponse = await this.tryPullResult(jobIdResponse.job_id);
648
+ await new Promise(resolve => setTimeout(resolve, 100)); // 100 ms recommended by documentation
649
+ if (tryPullResponse.status === 'idle') {
650
+ // try this again. job ID is still valid
651
+ continue;
652
+ }
653
+ else if (tryPullResponse.status === 'delivered') {
654
+ const error = (0, types_1.asMaybeBusy)(tryPullResponse.result);
655
+ if (error != null) {
656
+ // try this again. job ID is no longer valid
657
+ break;
658
+ }
659
+ return tryPullResponse.result;
660
+ }
661
+ else if (tryPullResponse.status === 'canceled') {
662
+ throw new Error(`${methodName} job canceled`);
663
+ }
664
+ }
665
+ }
666
+ }
667
+ }
668
+ exports.CppBridge = CppBridge;
@@ -0,0 +1,4 @@
1
+ import { CppBridge, NativeZanoModule } from './CppBridge';
2
+ export declare function makeZano(): CppBridge;
3
+ export type { CppBridge, NativeZanoModule };
4
+ export * from './types';