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.
- package/CHANGELOG.md +90 -0
- package/LICENSE +27 -0
- package/README.md +24 -0
- package/android/build.gradle +40 -0
- package/android/src/main/java/app/edge/rnzano/RnZanoModule.java +73 -0
- package/android/src/main/java/app/edge/rnzano/RnZanoPackage.java +21 -0
- package/android/src/main/jniLibs/arm64-v8a/librnzano.so +0 -0
- package/ios/ZanoModule.h +4 -0
- package/ios/ZanoModule.mm +138 -0
- package/ios/ZanoModule.xcframework/Info.plist +43 -0
- package/ios/ZanoModule.xcframework/ios-arm64/libzano-module.a +0 -0
- package/ios/ZanoModule.xcframework/ios-arm64-simulator/libzano-module.a +0 -0
- package/ios/react-native-zano.xcodeproj/project.pbxproj +1 -0
- package/lib/scripts/build-native-host.d.ts +1 -0
- package/lib/scripts/build-native-host.js +175 -0
- package/lib/scripts/smoke-node.d.ts +1 -0
- package/lib/scripts/smoke-node.js +33 -0
- package/lib/scripts/update-sources.d.ts +1 -0
- package/lib/scripts/update-sources.js +412 -0
- package/lib/scripts/utils/android-tools.d.ts +1 -0
- package/lib/scripts/utils/android-tools.js +25 -0
- package/lib/scripts/utils/closeWalletPatch.d.ts +23 -0
- package/lib/scripts/utils/closeWalletPatch.js +215 -0
- package/lib/scripts/utils/common.d.ts +37 -0
- package/lib/scripts/utils/common.js +186 -0
- package/lib/scripts/utils/ios-tools.d.ts +8 -0
- package/lib/scripts/utils/ios-tools.js +26 -0
- package/lib/scripts/utils/sdkFolders.d.ts +27 -0
- package/lib/scripts/utils/sdkFolders.js +43 -0
- package/lib/src/CppBridge.d.ts +142 -0
- package/lib/src/CppBridge.js +668 -0
- package/lib/src/index.d.ts +4 -0
- package/lib/src/index.js +28 -0
- package/lib/src/load-addon.d.ts +5 -0
- package/lib/src/load-addon.js +50 -0
- package/lib/src/node.d.ts +11 -0
- package/lib/src/node.js +25 -0
- package/lib/src/types.d.ts +292 -0
- package/lib/src/types.js +40 -0
- package/lib/src/walletFilePassword.d.ts +14 -0
- package/lib/src/walletFilePassword.js +74 -0
- package/node.d.ts +5 -0
- package/node.js +2 -0
- package/package.json +105 -0
- package/prebuilds/darwin-arm64/zano.node +0 -0
- package/src/node/zano-napi.cpp +176 -0
- package/src/zano-wrapper/zano-methods.hpp +15 -0
- 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;
|