react-native-pirate-wallet 0.3.0 → 0.3.3
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/README.md +84 -2
- package/package.json +6 -5
- package/scripts/assemble-ios-framework.js +143 -45
- package/scripts/verify-package.js +67 -46
- package/src/index.d.ts +57 -0
- package/src/index.js +83 -0
- package/test/smoke.js +91 -0
package/README.md
CHANGED
|
@@ -77,7 +77,9 @@ await sdk.configureAccountStorage({
|
|
|
77
77
|
- `src/`
|
|
78
78
|
|
|
79
79
|
Native libraries are distributed in exact-version Android ARM, Android x86_64,
|
|
80
|
-
iOS device, and iOS simulator companion packages.
|
|
80
|
+
iOS device, iOS simulator arm64, and iOS simulator x86_64 companion packages.
|
|
81
|
+
On macOS, CocoaPods combines the two thin simulator archives into the universal
|
|
82
|
+
XCFramework slice expected by Xcode.
|
|
81
83
|
|
|
82
84
|
## Preparing native artifacts in this repo
|
|
83
85
|
|
|
@@ -90,7 +92,8 @@ bash scripts/prepare-react-native-plugin.sh
|
|
|
90
92
|
That copies:
|
|
91
93
|
|
|
92
94
|
- Android JNI libraries into the two Android companion packages
|
|
93
|
-
- iOS XCFramework
|
|
95
|
+
- the iOS device XCFramework slice and two thin simulator archives into the
|
|
96
|
+
three iOS companion packages
|
|
94
97
|
|
|
95
98
|
There is also a minimal consumer app in:
|
|
96
99
|
|
|
@@ -237,6 +240,85 @@ active wallet between running wallets.
|
|
|
237
240
|
- `coinType`
|
|
238
241
|
- `rpcPort`
|
|
239
242
|
- `defaultBirthday`
|
|
243
|
+
|
|
244
|
+
### Lightwalletd endpoints and failover pools
|
|
245
|
+
|
|
246
|
+
Endpoint configuration is wallet-scoped. The recommended integration flow is
|
|
247
|
+
to test candidate servers, save one primary plus its alternates, and then start
|
|
248
|
+
or restart that wallet's synchronizer:
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
const primary = 'https://lightd1.pirate.black:443'
|
|
252
|
+
const alternates = [
|
|
253
|
+
'https://lightwalletd1.cryptoforge.cc:443',
|
|
254
|
+
'https://pirate.mathnodes.com:443'
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
const tests = await Promise.all(
|
|
258
|
+
[primary, ...alternates].map(url => sdk.testLightdEndpoint({ url }))
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if (tests.every(result => result.success)) {
|
|
262
|
+
await sdk.setLightdEndpointPool({
|
|
263
|
+
walletId,
|
|
264
|
+
url: primary,
|
|
265
|
+
failoverEndpoints: alternates
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const saved = await sdk.getLightdEndpointConfig(walletId)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- `getLightdEndpoint(walletId)`
|
|
273
|
+
- RPC: `get_lightd_endpoint`
|
|
274
|
+
- returns the effective primary endpoint URL
|
|
275
|
+
- `getLightdEndpointConfig(walletId)`
|
|
276
|
+
- RPC: `get_lightd_endpoint_config`
|
|
277
|
+
- returns `LightdEndpointConfig`:
|
|
278
|
+
- `host`
|
|
279
|
+
- `port`
|
|
280
|
+
- `useTls`
|
|
281
|
+
- `tlsPin`
|
|
282
|
+
- `label`
|
|
283
|
+
- `automaticFailover`
|
|
284
|
+
- `failoverEndpoints`
|
|
285
|
+
- `isConfigured`
|
|
286
|
+
- `testLightdEndpoint({ url, tlsPin? })`
|
|
287
|
+
- RPC: `test_node`
|
|
288
|
+
- also accepts `testLightdEndpoint(url, tlsPin?)`
|
|
289
|
+
- tests through the currently selected Direct, Tor, SOCKS5, or I2P transport
|
|
290
|
+
- reports success, height, latency, transport, TLS/pin information, server
|
|
291
|
+
version, chain name, and any connection error
|
|
292
|
+
- `setLightdEndpoint({ walletId, url, tlsPin? })`
|
|
293
|
+
- RPC: `set_lightd_endpoint`
|
|
294
|
+
- also accepts `setLightdEndpoint(walletId, url, tlsPin?)`
|
|
295
|
+
- saves one primary and clears any previously configured failover pool
|
|
296
|
+
- `setLightdEndpointPool({ walletId, url, failoverEndpoints, tlsPin? })`
|
|
297
|
+
- RPC: `set_lightd_endpoint_pool`
|
|
298
|
+
- also accepts
|
|
299
|
+
`setLightdEndpointPool(walletId, url, failoverEndpoints, tlsPin?)`
|
|
300
|
+
- saves the primary and up to 16 explicit alternates
|
|
301
|
+
- an empty `failoverEndpoints` array disables automatic failover
|
|
302
|
+
|
|
303
|
+
Pool membership is validated by the backend before anything is persisted.
|
|
304
|
+
Every member must resolve to the same recognized Pirate network, use the same
|
|
305
|
+
clearnet, onion, or I2P route, and use the same HTTP/TLS security mode as the
|
|
306
|
+
primary. The primary is removed from the alternate list and duplicate
|
|
307
|
+
alternates are collapsed. A pinned primary cannot use automatic failover,
|
|
308
|
+
because one server's SPKI pin cannot authenticate unrelated servers; use
|
|
309
|
+
`setLightdEndpoint()` when pinning a single server.
|
|
310
|
+
|
|
311
|
+
Saving either endpoint form cancels an existing sync session for that wallet so
|
|
312
|
+
it cannot continue against stale connection state. Restart the synchronizer
|
|
313
|
+
after the setter resolves. Pool candidates are still checked for compatible
|
|
314
|
+
chain identity and history before failover or historical work is assigned; the
|
|
315
|
+
array order is not a request to trust a candidate blindly.
|
|
316
|
+
|
|
317
|
+
`testLightdEndpoint()` returns a structured failure result for connection-level
|
|
318
|
+
failures. Invalid setter input or a rejected pool throws through the normal SDK
|
|
319
|
+
promise, so applications should show the error and retain the previous saved
|
|
320
|
+
configuration.
|
|
321
|
+
|
|
240
322
|
- `formatAmount(arrrtoshis)`
|
|
241
323
|
- RPC: `format_amount`
|
|
242
324
|
- returns formatted string
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-pirate-wallet",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "React Native wrapper for the Pirate Unified Wallet native SDK surfaces",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -52,10 +52,11 @@
|
|
|
52
52
|
"react-native": ">=0.71.0 <1.0.0"
|
|
53
53
|
},
|
|
54
54
|
"optionalDependencies": {
|
|
55
|
-
"react-native-pirate-wallet-android": "0.3.
|
|
56
|
-
"react-native-pirate-wallet-android-x86_64": "0.3.
|
|
57
|
-
"react-native-pirate-wallet-ios-device": "0.3.
|
|
58
|
-
"react-native-pirate-wallet-ios-simulator": "0.3.
|
|
55
|
+
"react-native-pirate-wallet-android": "0.3.3",
|
|
56
|
+
"react-native-pirate-wallet-android-x86_64": "0.3.3",
|
|
57
|
+
"react-native-pirate-wallet-ios-device": "0.3.3",
|
|
58
|
+
"react-native-pirate-wallet-ios-simulator-arm64": "0.3.3",
|
|
59
|
+
"react-native-pirate-wallet-ios-simulator-x86_64": "0.3.3"
|
|
59
60
|
},
|
|
60
61
|
"publishConfig": {
|
|
61
62
|
"access": "public"
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const childProcess = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
5
6
|
|
|
6
|
-
const packageRoot = path.resolve(__dirname,
|
|
7
|
-
const frameworkName =
|
|
8
|
-
const
|
|
7
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
8
|
+
const frameworkName = "PirateWalletNative.xcframework";
|
|
9
|
+
const devicePackage = {
|
|
10
|
+
name: "react-native-pirate-wallet-ios-device",
|
|
11
|
+
slice: "ios-arm64",
|
|
12
|
+
};
|
|
13
|
+
const simulatorPackages = [
|
|
9
14
|
{
|
|
10
|
-
name:
|
|
11
|
-
|
|
15
|
+
name: "react-native-pirate-wallet-ios-simulator-arm64",
|
|
16
|
+
architecture: "arm64",
|
|
12
17
|
},
|
|
13
18
|
{
|
|
14
|
-
name:
|
|
15
|
-
|
|
19
|
+
name: "react-native-pirate-wallet-ios-simulator-x86_64",
|
|
20
|
+
architecture: "x86_64",
|
|
16
21
|
},
|
|
17
22
|
];
|
|
18
23
|
|
|
@@ -22,43 +27,41 @@ function candidatePackageJsonPaths(packageName) {
|
|
|
22
27
|
candidates.push(
|
|
23
28
|
require.resolve(`${packageName}/package.json`, {
|
|
24
29
|
paths: [process.cwd(), packageRoot],
|
|
25
|
-
})
|
|
30
|
+
})
|
|
26
31
|
);
|
|
27
32
|
} catch (_) {
|
|
28
33
|
// The monorepo package is resolved below before it has been published.
|
|
29
34
|
}
|
|
30
|
-
candidates.push(
|
|
31
|
-
path.resolve(packageRoot, '..', packageName, 'package.json'),
|
|
32
|
-
);
|
|
35
|
+
candidates.push(path.resolve(packageRoot, "..", packageName, "package.json"));
|
|
33
36
|
return [...new Set(candidates)];
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
function resolvePackage(packageName, expectedVersion) {
|
|
37
40
|
for (const packageJsonPath of candidatePackageJsonPaths(packageName)) {
|
|
38
|
-
if (!fs.statSync(packageJsonPath, {throwIfNoEntry: false})?.isFile()) {
|
|
41
|
+
if (!fs.statSync(packageJsonPath, { throwIfNoEntry: false })?.isFile()) {
|
|
39
42
|
continue;
|
|
40
43
|
}
|
|
41
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath,
|
|
44
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
42
45
|
if (packageJson.name !== packageName) {
|
|
43
46
|
continue;
|
|
44
47
|
}
|
|
45
48
|
if (packageJson.version !== expectedVersion) {
|
|
46
49
|
throw new Error(
|
|
47
50
|
`${packageName}@${packageJson.version} does not match ` +
|
|
48
|
-
`react-native-pirate-wallet@${expectedVersion}
|
|
51
|
+
`react-native-pirate-wallet@${expectedVersion}`
|
|
49
52
|
);
|
|
50
53
|
}
|
|
51
|
-
return path.dirname(packageJsonPath);
|
|
54
|
+
return { root: path.dirname(packageJsonPath), packageJson };
|
|
52
55
|
}
|
|
53
56
|
throw new Error(
|
|
54
57
|
`${packageName}@${expectedVersion} is required to build ` +
|
|
55
|
-
|
|
58
|
+
"react-native-pirate-wallet for iOS"
|
|
56
59
|
);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function linkTree(source, destination) {
|
|
60
|
-
fs.mkdirSync(destination, {recursive: true});
|
|
61
|
-
for (const entry of fs.readdirSync(source, {withFileTypes: true})) {
|
|
63
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
64
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
62
65
|
const sourcePath = path.join(source, entry.name);
|
|
63
66
|
const destinationPath = path.join(destination, entry.name);
|
|
64
67
|
if (entry.isDirectory()) {
|
|
@@ -71,7 +74,7 @@ function linkTree(source, destination) {
|
|
|
71
74
|
try {
|
|
72
75
|
fs.linkSync(sourcePath, destinationPath);
|
|
73
76
|
} catch (error) {
|
|
74
|
-
if (![
|
|
77
|
+
if (!["EXDEV", "EPERM", "EACCES", "EMLINK"].includes(error.code)) {
|
|
75
78
|
throw error;
|
|
76
79
|
}
|
|
77
80
|
fs.copyFileSync(sourcePath, destinationPath);
|
|
@@ -79,45 +82,140 @@ function linkTree(source, destination) {
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
function runXcrun(args) {
|
|
86
|
+
const result = childProcess.spawnSync("xcrun", args, { encoding: "utf8" });
|
|
87
|
+
if (result.error) {
|
|
88
|
+
throw result.error;
|
|
89
|
+
}
|
|
90
|
+
if (result.status !== 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`xcrun ${args.join(" ")} failed:\n${result.stderr || result.stdout}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return result.stdout.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function verifyArchitectures(archive, expected) {
|
|
99
|
+
const actual = runXcrun(["lipo", "-archs", archive]).split(/\s+/).sort();
|
|
100
|
+
const wanted = [...expected].sort();
|
|
101
|
+
if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`${archive} has architectures ${actual.join(
|
|
104
|
+
", "
|
|
105
|
+
)}; expected ${wanted.join(", ")}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertMatchingFile(left, right, label) {
|
|
111
|
+
const leftData = fs.readFileSync(left);
|
|
112
|
+
const rightData = fs.readFileSync(right);
|
|
113
|
+
if (!leftData.equals(rightData)) {
|
|
114
|
+
throw new Error(`iOS binary packages contain different ${label}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
82
118
|
function assemble() {
|
|
83
|
-
if (process.platform !==
|
|
119
|
+
if (process.platform !== "darwin") {
|
|
120
|
+
if (process.argv.includes("--force")) {
|
|
121
|
+
throw new Error("iOS XCFramework assembly requires macOS and xcrun lipo");
|
|
122
|
+
}
|
|
84
123
|
return;
|
|
85
124
|
}
|
|
86
125
|
|
|
87
126
|
const wrapperPackage = JSON.parse(
|
|
88
|
-
fs.readFileSync(path.join(packageRoot,
|
|
127
|
+
fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")
|
|
89
128
|
);
|
|
90
|
-
|
|
91
|
-
const expectedVersion =
|
|
129
|
+
function resolveExact(descriptor) {
|
|
130
|
+
const expectedVersion =
|
|
131
|
+
wrapperPackage.optionalDependencies?.[descriptor.name];
|
|
92
132
|
if (expectedVersion !== wrapperPackage.version) {
|
|
93
|
-
throw new Error(`${name} must use the exact wrapper version`);
|
|
133
|
+
throw new Error(`${descriptor.name} must use the exact wrapper version`);
|
|
94
134
|
}
|
|
95
|
-
const root = resolvePackage(name, expectedVersion);
|
|
96
135
|
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
framework: path.join(root, 'ios', 'Frameworks', frameworkName),
|
|
136
|
+
...descriptor,
|
|
137
|
+
...resolvePackage(descriptor.name, expectedVersion),
|
|
100
138
|
};
|
|
101
|
-
}
|
|
139
|
+
}
|
|
102
140
|
|
|
103
|
-
const
|
|
104
|
-
|
|
141
|
+
const device = resolveExact(devicePackage);
|
|
142
|
+
const simulators = simulatorPackages.map(resolveExact);
|
|
143
|
+
const deviceFramework = path.join(
|
|
144
|
+
device.root,
|
|
145
|
+
"ios",
|
|
146
|
+
"Frameworks",
|
|
147
|
+
frameworkName
|
|
105
148
|
);
|
|
106
|
-
|
|
107
|
-
|
|
149
|
+
const deviceSlice = path.join(deviceFramework, device.slice);
|
|
150
|
+
if (!fs.statSync(deviceSlice, { throwIfNoEntry: false })?.isDirectory()) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`${device.name} is missing XCFramework slice ${device.slice}`
|
|
153
|
+
);
|
|
108
154
|
}
|
|
109
155
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
156
|
+
const simulatorInputs = simulators.map((simulator) => {
|
|
157
|
+
const metadata = simulator.packageJson.pirateWalletNative;
|
|
158
|
+
if (
|
|
159
|
+
metadata?.platform !== "ios-simulator" ||
|
|
160
|
+
JSON.stringify(metadata.architectures) !==
|
|
161
|
+
JSON.stringify([simulator.architecture])
|
|
162
|
+
) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`${simulator.name} does not identify its expected simulator architecture`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const archive = path.join(simulator.root, "ios", "libpirate_ffi_native.a");
|
|
168
|
+
const headers = path.join(simulator.root, "ios", "Headers");
|
|
169
|
+
verifyArchitectures(archive, [simulator.architecture]);
|
|
170
|
+
return { ...simulator, archive, headers };
|
|
171
|
+
});
|
|
114
172
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
173
|
+
const canonicalHeaders = path.join(deviceSlice, "Headers");
|
|
174
|
+
for (const simulator of simulatorInputs) {
|
|
175
|
+
for (const header of ["module.modulemap", "pirate_wallet_service.h"]) {
|
|
176
|
+
assertMatchingFile(
|
|
177
|
+
path.join(canonicalHeaders, header),
|
|
178
|
+
path.join(simulator.headers, header),
|
|
179
|
+
header
|
|
180
|
+
);
|
|
119
181
|
}
|
|
120
|
-
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const frameworksRoot = path.join(packageRoot, "ios", "Frameworks");
|
|
185
|
+
fs.mkdirSync(frameworksRoot, { recursive: true });
|
|
186
|
+
const temporaryRoot = fs.mkdtempSync(
|
|
187
|
+
path.join(frameworksRoot, `${frameworkName}.tmp-`)
|
|
188
|
+
);
|
|
189
|
+
const output = path.join(frameworksRoot, frameworkName);
|
|
190
|
+
try {
|
|
191
|
+
fs.copyFileSync(
|
|
192
|
+
path.join(deviceFramework, "Info.plist"),
|
|
193
|
+
path.join(temporaryRoot, "Info.plist")
|
|
194
|
+
);
|
|
195
|
+
linkTree(deviceSlice, path.join(temporaryRoot, device.slice));
|
|
196
|
+
|
|
197
|
+
const simulatorSlice = path.join(
|
|
198
|
+
temporaryRoot,
|
|
199
|
+
"ios-arm64_x86_64-simulator"
|
|
200
|
+
);
|
|
201
|
+
linkTree(canonicalHeaders, path.join(simulatorSlice, "Headers"));
|
|
202
|
+
const universalArchive = path.join(
|
|
203
|
+
simulatorSlice,
|
|
204
|
+
"libpirate_ffi_native.a"
|
|
205
|
+
);
|
|
206
|
+
runXcrun([
|
|
207
|
+
"lipo",
|
|
208
|
+
"-create",
|
|
209
|
+
...simulatorInputs.map((simulator) => simulator.archive),
|
|
210
|
+
"-output",
|
|
211
|
+
universalArchive,
|
|
212
|
+
]);
|
|
213
|
+
verifyArchitectures(universalArchive, ["arm64", "x86_64"]);
|
|
214
|
+
|
|
215
|
+
fs.rmSync(output, { recursive: true, force: true });
|
|
216
|
+
fs.renameSync(temporaryRoot, output);
|
|
217
|
+
} finally {
|
|
218
|
+
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
|
121
219
|
}
|
|
122
220
|
}
|
|
123
221
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
5
|
|
|
6
|
-
const packageRoot = path.resolve(__dirname,
|
|
6
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
7
7
|
|
|
8
8
|
function fail(message) {
|
|
9
9
|
console.error(`[react-native-pirate-wallet] ${message}`);
|
|
@@ -12,7 +12,7 @@ function fail(message) {
|
|
|
12
12
|
|
|
13
13
|
function requireFile(relativePath) {
|
|
14
14
|
const absolutePath = path.join(packageRoot, relativePath);
|
|
15
|
-
if (!fs.statSync(absolutePath, {throwIfNoEntry: false})?.isFile()) {
|
|
15
|
+
if (!fs.statSync(absolutePath, { throwIfNoEntry: false })?.isFile()) {
|
|
16
16
|
fail(`Required package file is missing: ${relativePath}`);
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
@@ -28,96 +28,117 @@ function rejectPath(relativePath) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function collectFiles(directory) {
|
|
31
|
-
if (!fs.statSync(directory, {throwIfNoEntry: false})?.isDirectory()) {
|
|
31
|
+
if (!fs.statSync(directory, { throwIfNoEntry: false })?.isDirectory()) {
|
|
32
32
|
return [];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
return fs.readdirSync(directory, {withFileTypes: true}).flatMap(entry => {
|
|
35
|
+
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
36
36
|
const entryPath = path.join(directory, entry.name);
|
|
37
37
|
return entry.isDirectory() ? collectFiles(entryPath) : [entryPath];
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const packageJson = JSON.parse(
|
|
42
|
-
fs.readFileSync(path.join(packageRoot,
|
|
42
|
+
fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")
|
|
43
43
|
);
|
|
44
44
|
|
|
45
|
-
if (packageJson.name !==
|
|
45
|
+
if (packageJson.name !== "react-native-pirate-wallet") {
|
|
46
46
|
fail(`Unexpected package name: ${packageJson.name}`);
|
|
47
47
|
}
|
|
48
48
|
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(packageJson.version)) {
|
|
49
|
-
fail(
|
|
49
|
+
fail(
|
|
50
|
+
`Package version is not valid semantic versioning: ${packageJson.version}`
|
|
51
|
+
);
|
|
50
52
|
}
|
|
51
53
|
if (packageJson.private === true) {
|
|
52
|
-
fail(
|
|
54
|
+
fail("The publishable package must not be marked private");
|
|
53
55
|
}
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
+
if (
|
|
57
|
+
packageJson.repository?.url !==
|
|
58
|
+
"https://github.com/PirateNetwork/Pirate-Unified-Light-Wallet.git"
|
|
59
|
+
) {
|
|
60
|
+
fail(
|
|
61
|
+
"The repository URL must match the GitHub repository used for npm provenance"
|
|
62
|
+
);
|
|
56
63
|
}
|
|
57
|
-
if (packageJson.publishConfig?.access !==
|
|
58
|
-
fail(
|
|
64
|
+
if (packageJson.publishConfig?.access !== "public") {
|
|
65
|
+
fail("publishConfig.access must remain public");
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
[
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
"LICENSE-MIT",
|
|
70
|
+
"README.md",
|
|
71
|
+
"react-native.config.js",
|
|
72
|
+
"react-native-pirate-wallet.podspec",
|
|
73
|
+
"scripts/assemble-ios-framework.js",
|
|
74
|
+
"scripts/resolve-android-packages.js",
|
|
75
|
+
"test/smoke.js",
|
|
76
|
+
"src/index.js",
|
|
77
|
+
"src/index.d.ts",
|
|
78
|
+
"android/src/main/AndroidManifest.xml",
|
|
79
|
+
"android/src/main/java/com/pirate/wallet/reactnative/PirateWalletReactNativeModule.kt",
|
|
80
|
+
"ios/PirateWalletReactNative.m",
|
|
81
|
+
"ios/PirateWalletReactNative.swift",
|
|
75
82
|
].forEach(requireFile);
|
|
76
83
|
|
|
77
|
-
if (process.argv.includes(
|
|
84
|
+
if (process.argv.includes("--publish-layout")) {
|
|
78
85
|
[
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
"android/.gradle",
|
|
87
|
+
"android/build",
|
|
88
|
+
"android/src/main/jniLibs",
|
|
89
|
+
"ios/Frameworks/PirateWalletNative.xcframework",
|
|
83
90
|
].forEach(rejectPath);
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
const binaryPackageNames = [
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
"react-native-pirate-wallet-android",
|
|
95
|
+
"react-native-pirate-wallet-android-x86_64",
|
|
96
|
+
"react-native-pirate-wallet-ios-device",
|
|
97
|
+
"react-native-pirate-wallet-ios-simulator-arm64",
|
|
98
|
+
"react-native-pirate-wallet-ios-simulator-x86_64",
|
|
91
99
|
];
|
|
92
100
|
for (const binaryPackageName of binaryPackageNames) {
|
|
93
101
|
if (
|
|
94
|
-
packageJson.optionalDependencies?.[binaryPackageName] !==
|
|
102
|
+
packageJson.optionalDependencies?.[binaryPackageName] !==
|
|
103
|
+
packageJson.version
|
|
95
104
|
) {
|
|
96
105
|
fail(`${binaryPackageName} must use the same exact version as the wrapper`);
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
if (
|
|
101
|
-
!process.argv.includes(
|
|
102
|
-
(process.platform ===
|
|
110
|
+
!process.argv.includes("--publish-layout") &&
|
|
111
|
+
(process.platform === "darwin" || process.argv.includes("--all-platforms"))
|
|
103
112
|
) {
|
|
104
113
|
const staticLibraries = collectFiles(
|
|
105
|
-
path.join(
|
|
106
|
-
|
|
114
|
+
path.join(
|
|
115
|
+
packageRoot,
|
|
116
|
+
"ios",
|
|
117
|
+
"Frameworks",
|
|
118
|
+
"PirateWalletNative.xcframework"
|
|
119
|
+
)
|
|
120
|
+
).filter((file) => file.endsWith(".a"));
|
|
107
121
|
if (staticLibraries.length !== 2) {
|
|
108
|
-
fail(
|
|
122
|
+
fail(
|
|
123
|
+
"The iOS XCFramework must contain device and simulator static libraries"
|
|
124
|
+
);
|
|
109
125
|
}
|
|
110
126
|
for (const library of staticLibraries) {
|
|
111
127
|
if (fs.statSync(library).size === 0) {
|
|
112
|
-
fail(
|
|
128
|
+
fail(
|
|
129
|
+
`The iOS static library is empty: ${path.relative(
|
|
130
|
+
packageRoot,
|
|
131
|
+
library
|
|
132
|
+
)}`
|
|
133
|
+
);
|
|
113
134
|
}
|
|
114
135
|
}
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
try {
|
|
118
|
-
const {resolveAndroidJniLibsPaths} = require(
|
|
139
|
+
const { resolveAndroidJniLibsPaths } = require("./resolve-android-packages");
|
|
119
140
|
if (resolveAndroidJniLibsPaths().length !== 2) {
|
|
120
|
-
fail(
|
|
141
|
+
fail("Both Android binary packages must resolve");
|
|
121
142
|
}
|
|
122
143
|
} catch (error) {
|
|
123
144
|
fail(error.message);
|
package/src/index.d.ts
CHANGED
|
@@ -42,6 +42,50 @@ export interface PirateWalletAccountStorageConfig {
|
|
|
42
42
|
storagePath?: string | null
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export interface LightdEndpointConfig {
|
|
46
|
+
host: string
|
|
47
|
+
port: number
|
|
48
|
+
useTls: boolean
|
|
49
|
+
tlsPin: string | null
|
|
50
|
+
label: string | null
|
|
51
|
+
automaticFailover: boolean
|
|
52
|
+
failoverEndpoints: string[]
|
|
53
|
+
isConfigured: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SetLightdEndpointRequest {
|
|
57
|
+
walletId: string
|
|
58
|
+
url: string
|
|
59
|
+
tlsPin?: string | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SetLightdEndpointPoolRequest extends SetLightdEndpointRequest {
|
|
63
|
+
failoverEndpoints: string[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TestLightdEndpointRequest {
|
|
67
|
+
url: string
|
|
68
|
+
tlsPin?: string | null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface NodeTestResult {
|
|
72
|
+
success: boolean
|
|
73
|
+
latestBlockHeight: number | null
|
|
74
|
+
transportMode: string
|
|
75
|
+
tlsEnabled: boolean
|
|
76
|
+
tlsPinMatched: boolean | null
|
|
77
|
+
expectedPin: string | null
|
|
78
|
+
actualPin: string | null
|
|
79
|
+
errorMessage: string | null
|
|
80
|
+
responseTimeMs: number
|
|
81
|
+
serverVersion: string | null
|
|
82
|
+
chainName: string | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface Acknowledgement {
|
|
86
|
+
acknowledged: true
|
|
87
|
+
}
|
|
88
|
+
|
|
45
89
|
export interface SynchronizerSnapshot {
|
|
46
90
|
walletId: string
|
|
47
91
|
alias: string
|
|
@@ -209,6 +253,19 @@ export class PirateWalletSdk {
|
|
|
209
253
|
isValidShieldedAddr(address: string): Promise<boolean>
|
|
210
254
|
validateAddress(address: string): Promise<any>
|
|
211
255
|
validateConsensusBranch(walletId: string): Promise<any>
|
|
256
|
+
getLightdEndpoint(walletId: string): Promise<string>
|
|
257
|
+
getLightdEndpointConfig(walletId: string): Promise<LightdEndpointConfig>
|
|
258
|
+
setLightdEndpoint(request: SetLightdEndpointRequest): Promise<Acknowledgement>
|
|
259
|
+
setLightdEndpoint(walletId: string, url: string, tlsPin?: string | null): Promise<Acknowledgement>
|
|
260
|
+
setLightdEndpointPool(request: SetLightdEndpointPoolRequest): Promise<Acknowledgement>
|
|
261
|
+
setLightdEndpointPool(
|
|
262
|
+
walletId: string,
|
|
263
|
+
url: string,
|
|
264
|
+
failoverEndpoints: string[],
|
|
265
|
+
tlsPin?: string | null
|
|
266
|
+
): Promise<Acknowledgement>
|
|
267
|
+
testLightdEndpoint(request: TestLightdEndpointRequest): Promise<NodeTestResult>
|
|
268
|
+
testLightdEndpoint(url: string, tlsPin?: string | null): Promise<NodeTestResult>
|
|
212
269
|
formatAmount(arrrtoshis: AmountInput): Promise<string>
|
|
213
270
|
parseAmount(arrr: string): Promise<AmountString>
|
|
214
271
|
getCurrentReceiveAddress(walletId: string): Promise<string>
|
package/src/index.js
CHANGED
|
@@ -83,6 +83,33 @@ function buildRequest(method, params = {}) {
|
|
|
83
83
|
return JSON.stringify(request)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function requireNonEmptyString(value, name) {
|
|
87
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
88
|
+
throw new Error(`${name} must be a non-empty string.`)
|
|
89
|
+
}
|
|
90
|
+
return value
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function optionalNonEmptyString(value, name) {
|
|
94
|
+
if (value === undefined || value === null) {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
return requireNonEmptyString(value, name)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateFailoverEndpoints(value) {
|
|
101
|
+
if (!Array.isArray(value)) {
|
|
102
|
+
throw new Error('failoverEndpoints must be an array of endpoint URL strings.')
|
|
103
|
+
}
|
|
104
|
+
if (value.length > 16) {
|
|
105
|
+
throw new Error('failoverEndpoints may contain at most 16 endpoint URLs.')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return value.map((endpoint, index) =>
|
|
109
|
+
requireNonEmptyString(endpoint, `failoverEndpoints[${index}]`)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
function unwrapEnvelope(responseJson, method, options = {}) {
|
|
87
114
|
const { camelizeResult = true } = options
|
|
88
115
|
let envelope
|
|
@@ -667,6 +694,62 @@ class PirateWalletSdk {
|
|
|
667
694
|
return this._call('validate_consensus_branch', { wallet_id: walletId })
|
|
668
695
|
}
|
|
669
696
|
|
|
697
|
+
getLightdEndpoint(walletId) {
|
|
698
|
+
return this._call('get_lightd_endpoint', {
|
|
699
|
+
wallet_id: requireNonEmptyString(walletId, 'walletId')
|
|
700
|
+
})
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
getLightdEndpointConfig(walletId) {
|
|
704
|
+
return this._call('get_lightd_endpoint_config', {
|
|
705
|
+
wallet_id: requireNonEmptyString(walletId, 'walletId')
|
|
706
|
+
})
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
setLightdEndpoint(requestOrWalletId, url = null, tlsPin = null) {
|
|
710
|
+
const request =
|
|
711
|
+
typeof requestOrWalletId === 'object' && requestOrWalletId !== null
|
|
712
|
+
? requestOrWalletId
|
|
713
|
+
: { walletId: requestOrWalletId, url, tlsPin }
|
|
714
|
+
|
|
715
|
+
return this._call('set_lightd_endpoint', {
|
|
716
|
+
wallet_id: requireNonEmptyString(request.walletId, 'walletId'),
|
|
717
|
+
url: requireNonEmptyString(request.url, 'url'),
|
|
718
|
+
tls_pin_opt: optionalNonEmptyString(request.tlsPin, 'tlsPin')
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
setLightdEndpointPool(
|
|
723
|
+
requestOrWalletId,
|
|
724
|
+
url = null,
|
|
725
|
+
failoverEndpoints = [],
|
|
726
|
+
tlsPin = null
|
|
727
|
+
) {
|
|
728
|
+
const request =
|
|
729
|
+
typeof requestOrWalletId === 'object' && requestOrWalletId !== null
|
|
730
|
+
? requestOrWalletId
|
|
731
|
+
: { walletId: requestOrWalletId, url, failoverEndpoints, tlsPin }
|
|
732
|
+
|
|
733
|
+
return this._call('set_lightd_endpoint_pool', {
|
|
734
|
+
wallet_id: requireNonEmptyString(request.walletId, 'walletId'),
|
|
735
|
+
url: requireNonEmptyString(request.url, 'url'),
|
|
736
|
+
tls_pin_opt: optionalNonEmptyString(request.tlsPin, 'tlsPin'),
|
|
737
|
+
failover_endpoints: validateFailoverEndpoints(request.failoverEndpoints)
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
testLightdEndpoint(requestOrUrl, tlsPin = null) {
|
|
742
|
+
const request =
|
|
743
|
+
typeof requestOrUrl === 'object' && requestOrUrl !== null
|
|
744
|
+
? requestOrUrl
|
|
745
|
+
: { url: requestOrUrl, tlsPin }
|
|
746
|
+
|
|
747
|
+
return this._call('test_node', {
|
|
748
|
+
url: requireNonEmptyString(request.url, 'url'),
|
|
749
|
+
tls_pin: optionalNonEmptyString(request.tlsPin, 'tlsPin')
|
|
750
|
+
})
|
|
751
|
+
}
|
|
752
|
+
|
|
670
753
|
formatAmount(arrrtoshis) {
|
|
671
754
|
return this._call('format_amount', { arrrtoshis })
|
|
672
755
|
}
|
package/test/smoke.js
CHANGED
|
@@ -48,6 +48,51 @@ function createMockNativeModule() {
|
|
|
48
48
|
])
|
|
49
49
|
case 'get_active_wallet':
|
|
50
50
|
return ok('wallet-1')
|
|
51
|
+
case 'get_lightd_endpoint':
|
|
52
|
+
assert.strictEqual(request.wallet_id, 'wallet-1')
|
|
53
|
+
return ok('https://lightd1.pirate.black:443')
|
|
54
|
+
case 'get_lightd_endpoint_config':
|
|
55
|
+
assert.strictEqual(request.wallet_id, 'wallet-1')
|
|
56
|
+
return ok({
|
|
57
|
+
host: 'lightd1.pirate.black',
|
|
58
|
+
port: 443,
|
|
59
|
+
use_tls: true,
|
|
60
|
+
tls_pin: null,
|
|
61
|
+
label: 'Primary',
|
|
62
|
+
automatic_failover: true,
|
|
63
|
+
failover_endpoints: ['https://lightwalletd1.cryptoforge.cc:443'],
|
|
64
|
+
is_configured: true
|
|
65
|
+
})
|
|
66
|
+
case 'set_lightd_endpoint':
|
|
67
|
+
assert.strictEqual(request.wallet_id, 'wallet-1')
|
|
68
|
+
assert.strictEqual(request.url, 'https://lightd1.pirate.black:443')
|
|
69
|
+
assert.strictEqual(request.tls_pin_opt, 'base64-spki-pin')
|
|
70
|
+
return ok({ acknowledged: true })
|
|
71
|
+
case 'set_lightd_endpoint_pool':
|
|
72
|
+
assert.strictEqual(request.wallet_id, 'wallet-1')
|
|
73
|
+
assert.strictEqual(request.url, 'https://lightd1.pirate.black:443')
|
|
74
|
+
assert.strictEqual(request.tls_pin_opt, undefined)
|
|
75
|
+
assert.deepStrictEqual(request.failover_endpoints, [
|
|
76
|
+
'https://lightwalletd1.cryptoforge.cc:443',
|
|
77
|
+
'https://pirate.mathnodes.com:443'
|
|
78
|
+
])
|
|
79
|
+
return ok({ acknowledged: true })
|
|
80
|
+
case 'test_node':
|
|
81
|
+
assert.strictEqual(request.url, 'https://lightwalletd1.cryptoforge.cc:443')
|
|
82
|
+
assert.strictEqual(request.tls_pin, undefined)
|
|
83
|
+
return ok({
|
|
84
|
+
success: true,
|
|
85
|
+
latest_block_height: 4200000,
|
|
86
|
+
transport_mode: 'Direct',
|
|
87
|
+
tls_enabled: true,
|
|
88
|
+
tls_pin_matched: null,
|
|
89
|
+
expected_pin: null,
|
|
90
|
+
actual_pin: 'observed-pin',
|
|
91
|
+
error_message: null,
|
|
92
|
+
response_time_ms: 95,
|
|
93
|
+
server_version: 'lightwalletd',
|
|
94
|
+
chain_name: 'main'
|
|
95
|
+
})
|
|
51
96
|
case 'current_receive_address':
|
|
52
97
|
assert.strictEqual(request.wallet_id, 'wallet-1')
|
|
53
98
|
return ok('pirate1current')
|
|
@@ -164,6 +209,52 @@ async function main() {
|
|
|
164
209
|
const latestBirthdayHeight = await sdk.getLatestBirthdayHeight('wallet-1')
|
|
165
210
|
assert.strictEqual(latestBirthdayHeight, 345678)
|
|
166
211
|
|
|
212
|
+
const endpoint = await sdk.getLightdEndpoint('wallet-1')
|
|
213
|
+
assert.strictEqual(endpoint, 'https://lightd1.pirate.black:443')
|
|
214
|
+
|
|
215
|
+
const endpointConfig = await sdk.getLightdEndpointConfig('wallet-1')
|
|
216
|
+
assert.strictEqual(endpointConfig.automaticFailover, true)
|
|
217
|
+
assert.deepStrictEqual(endpointConfig.failoverEndpoints, [
|
|
218
|
+
'https://lightwalletd1.cryptoforge.cc:443'
|
|
219
|
+
])
|
|
220
|
+
|
|
221
|
+
const endpointTest = await sdk.testLightdEndpoint(
|
|
222
|
+
'https://lightwalletd1.cryptoforge.cc:443'
|
|
223
|
+
)
|
|
224
|
+
assert.strictEqual(endpointTest.latestBlockHeight, 4200000)
|
|
225
|
+
assert.strictEqual(endpointTest.responseTimeMs, 95)
|
|
226
|
+
|
|
227
|
+
const endpointAck = await sdk.setLightdEndpoint({
|
|
228
|
+
walletId: 'wallet-1',
|
|
229
|
+
url: 'https://lightd1.pirate.black:443',
|
|
230
|
+
tlsPin: 'base64-spki-pin'
|
|
231
|
+
})
|
|
232
|
+
assert.strictEqual(endpointAck.acknowledged, true)
|
|
233
|
+
|
|
234
|
+
const poolAck = await sdk.setLightdEndpointPool({
|
|
235
|
+
walletId: 'wallet-1',
|
|
236
|
+
url: 'https://lightd1.pirate.black:443',
|
|
237
|
+
failoverEndpoints: [
|
|
238
|
+
'https://lightwalletd1.cryptoforge.cc:443',
|
|
239
|
+
'https://pirate.mathnodes.com:443'
|
|
240
|
+
]
|
|
241
|
+
})
|
|
242
|
+
assert.strictEqual(poolAck.acknowledged, true)
|
|
243
|
+
|
|
244
|
+
assert.throws(
|
|
245
|
+
() =>
|
|
246
|
+
sdk.setLightdEndpointPool({
|
|
247
|
+
walletId: 'wallet-1',
|
|
248
|
+
url: 'https://lightd1.pirate.black:443',
|
|
249
|
+
failoverEndpoints: 'not-an-array'
|
|
250
|
+
}),
|
|
251
|
+
/failoverEndpoints must be an array/
|
|
252
|
+
)
|
|
253
|
+
assert.throws(
|
|
254
|
+
() => sdk.testLightdEndpoint(' '),
|
|
255
|
+
/url must be a non-empty string/
|
|
256
|
+
)
|
|
257
|
+
|
|
167
258
|
assert.strictEqual(await sdk.getCurrentAddress('wallet-1'), 'pirate1current')
|
|
168
259
|
assert.strictEqual(await sdk.getNextAddress('wallet-1'), 'pirate1next')
|
|
169
260
|
|