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 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 slices into the two iOS companion packages
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.0",
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.0",
56
- "react-native-pirate-wallet-android-x86_64": "0.3.0",
57
- "react-native-pirate-wallet-ios-device": "0.3.0",
58
- "react-native-pirate-wallet-ios-simulator": "0.3.0"
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
- 'use strict';
1
+ "use strict";
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
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 = 'PirateWalletNative.xcframework';
8
- const iosPackages = [
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: 'react-native-pirate-wallet-ios-device',
11
- slice: 'ios-arm64',
15
+ name: "react-native-pirate-wallet-ios-simulator-arm64",
16
+ architecture: "arm64",
12
17
  },
13
18
  {
14
- name: 'react-native-pirate-wallet-ios-simulator',
15
- slice: 'ios-arm64_x86_64-simulator',
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, 'utf8'));
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
- 'react-native-pirate-wallet for iOS',
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 (!['EXDEV', 'EPERM', 'EACCES', 'EMLINK'].includes(error.code)) {
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 !== 'darwin' && !process.argv.includes('--force')) {
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, 'package.json'), 'utf8'),
127
+ fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")
89
128
  );
90
- const resolved = iosPackages.map(({name, slice}) => {
91
- const expectedVersion = wrapperPackage.optionalDependencies?.[name];
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
- name,
98
- slice,
99
- framework: path.join(root, 'ios', 'Frameworks', frameworkName),
136
+ ...descriptor,
137
+ ...resolvePackage(descriptor.name, expectedVersion),
100
138
  };
101
- });
139
+ }
102
140
 
103
- const infoPlists = resolved.map(({framework}) =>
104
- fs.readFileSync(path.join(framework, 'Info.plist')),
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
- if (!infoPlists.slice(1).every(data => data.equals(infoPlists[0]))) {
107
- throw new Error('iOS binary packages contain different XCFramework metadata');
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 output = path.join(packageRoot, 'ios', 'Frameworks', frameworkName);
111
- fs.rmSync(output, {recursive: true, force: true});
112
- fs.mkdirSync(output, {recursive: true});
113
- fs.writeFileSync(path.join(output, 'Info.plist'), infoPlists[0]);
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
- for (const {name, slice, framework} of resolved) {
116
- const source = path.join(framework, slice);
117
- if (!fs.statSync(source, {throwIfNoEntry: false})?.isDirectory()) {
118
- throw new Error(`${name} is missing XCFramework slice ${slice}`);
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
- linkTree(source, path.join(output, slice));
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
- 'use strict';
1
+ "use strict";
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
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, 'package.json'), 'utf8'),
42
+ fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")
43
43
  );
44
44
 
45
- if (packageJson.name !== 'react-native-pirate-wallet') {
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(`Package version is not valid semantic versioning: ${packageJson.version}`);
49
+ fail(
50
+ `Package version is not valid semantic versioning: ${packageJson.version}`
51
+ );
50
52
  }
51
53
  if (packageJson.private === true) {
52
- fail('The publishable package must not be marked private');
54
+ fail("The publishable package must not be marked private");
53
55
  }
54
- if (packageJson.repository?.url !== 'https://github.com/PirateNetwork/Pirate-Unified-Light-Wallet.git') {
55
- fail('The repository URL must match the GitHub repository used for npm provenance');
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 !== 'public') {
58
- fail('publishConfig.access must remain public');
64
+ if (packageJson.publishConfig?.access !== "public") {
65
+ fail("publishConfig.access must remain public");
59
66
  }
60
67
 
61
68
  [
62
- 'LICENSE-MIT',
63
- 'README.md',
64
- 'react-native.config.js',
65
- 'react-native-pirate-wallet.podspec',
66
- 'scripts/assemble-ios-framework.js',
67
- 'scripts/resolve-android-packages.js',
68
- 'test/smoke.js',
69
- 'src/index.js',
70
- 'src/index.d.ts',
71
- 'android/src/main/AndroidManifest.xml',
72
- 'android/src/main/java/com/pirate/wallet/reactnative/PirateWalletReactNativeModule.kt',
73
- 'ios/PirateWalletReactNative.m',
74
- 'ios/PirateWalletReactNative.swift',
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('--publish-layout')) {
84
+ if (process.argv.includes("--publish-layout")) {
78
85
  [
79
- 'android/.gradle',
80
- 'android/build',
81
- 'android/src/main/jniLibs',
82
- 'ios/Frameworks/PirateWalletNative.xcframework',
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
- 'react-native-pirate-wallet-android',
88
- 'react-native-pirate-wallet-android-x86_64',
89
- 'react-native-pirate-wallet-ios-device',
90
- 'react-native-pirate-wallet-ios-simulator',
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] !== packageJson.version
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('--publish-layout') &&
102
- (process.platform === 'darwin' || process.argv.includes('--all-platforms'))
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(packageRoot, 'ios', 'Frameworks', 'PirateWalletNative.xcframework'),
106
- ).filter(file => file.endsWith('.a'));
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('The iOS XCFramework must contain device and simulator static libraries');
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(`The iOS static library is empty: ${path.relative(packageRoot, library)}`);
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('./resolve-android-packages');
139
+ const { resolveAndroidJniLibsPaths } = require("./resolve-android-packages");
119
140
  if (resolveAndroidJniLibsPaths().length !== 2) {
120
- fail('Both Android binary packages must resolve');
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