@stellar/typescript-wallet-sdk 5.0.0 → 5.1.0-beta.1792196327472
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 +10 -0
- package/lib/bundle.js +160 -7
- package/lib/bundle.js.map +1 -1
- package/lib/bundle_browser.js +9188 -2996
- package/lib/bundle_browser.js.map +1 -1
- package/lib/walletSdk/Exceptions/index.d.ts +12 -0
- package/lib/walletSdk/Recovery/index.d.ts +35 -1
- package/lib/walletSdk/Types/recovery.d.ts +51 -0
- package/package.json +2 -2
- package/src/walletSdk/Exceptions/index.ts +51 -0
- package/src/walletSdk/Recovery/index.ts +120 -4
- package/src/walletSdk/Types/recovery.ts +51 -0
- package/test/recovery.test.ts +258 -22
package/test/recovery.test.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import { xdr } from "@stellar/stellar-sdk";
|
|
3
3
|
|
|
4
|
-
import { SigningKeypair, Wallet } from "../src";
|
|
4
|
+
import { AccountService, Recovery, SigningKeypair, Wallet } from "../src";
|
|
5
|
+
import {
|
|
6
|
+
DuplicateAccountSignerError,
|
|
7
|
+
DuplicateRecoverySignerError,
|
|
8
|
+
RecoverySignerEqualsDeviceKeyError,
|
|
9
|
+
SignerKeyEqualsMasterKeyError,
|
|
10
|
+
} from "../src/walletSdk/Exceptions";
|
|
5
11
|
|
|
6
12
|
const testingAccountKp = SigningKeypair.fromSecret(
|
|
7
13
|
"SDZZHNNOHOLFCAQ7XZZREXTXFTEPPT3L527WB2LVYCXODEDGTT6KBUSL",
|
|
@@ -17,29 +23,32 @@ const sponsorAccountKp = SigningKeypair.fromSecret(
|
|
|
17
23
|
|
|
18
24
|
let wallet: Wallet;
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
await stellar.server.loadAccount(testingAccountKp.publicKey);
|
|
28
|
-
} catch (e) {
|
|
29
|
-
await axios.get(
|
|
30
|
-
"https://friendbot.stellar.org/?addr=" + testingAccountKp.publicKey,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
26
|
+
// Shared by every suite in this file. Kept at file scope rather than inside one
|
|
27
|
+
// describe so that running either suite on its own — e.g. with a -t filter —
|
|
28
|
+
// still gets a wallet and funded accounts.
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
wallet = Wallet.TestNet();
|
|
31
|
+
const stellar = wallet.stellar();
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
33
|
+
// make sure testing accounts exist
|
|
34
|
+
try {
|
|
35
|
+
await stellar.server.loadAccount(testingAccountKp.publicKey);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
await axios.get(
|
|
38
|
+
"https://friendbot.stellar.org/?addr=" + testingAccountKp.publicKey,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
42
41
|
|
|
42
|
+
try {
|
|
43
|
+
await stellar.server.loadAccount(sponsorAccountKp.publicKey);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
await axios.get(
|
|
46
|
+
"https://friendbot.stellar.org/?addr=" + sponsorAccountKp.publicKey,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}, 60000);
|
|
50
|
+
|
|
51
|
+
describe("Recovery / Register Signers", () => {
|
|
43
52
|
it("defaults work", async () => {
|
|
44
53
|
const transaction = await wallet
|
|
45
54
|
.recovery({ servers: {} })
|
|
@@ -105,3 +114,230 @@ describe("Recovery / Register Signers", () => {
|
|
|
105
114
|
expect(transaction.operations.length).toBe(5);
|
|
106
115
|
});
|
|
107
116
|
});
|
|
117
|
+
|
|
118
|
+
describe("Recovery / Signer set validation", () => {
|
|
119
|
+
const deviceKp = SigningKeypair.fromSecret(
|
|
120
|
+
"SC2VAILFM6DTF7MLDKFMK2RWQ4YTOYSKLMMXVNSRPYZ3VIA6L5WNCK4C",
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const recoverySigner1Kp = SigningKeypair.fromSecret(
|
|
124
|
+
"SB42ZOPNSEGUGXBX4VE7ZQ6C5RRVX3JD3NU6V37QTWNPC4VF2LA2EXUF",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const recoverySigner2Kp = SigningKeypair.fromSecret(
|
|
128
|
+
"SANCUS65KI66LSQI2REKJODIWISJVJKYONMCFP4DF76IUFDCNHKVFHFD",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const threshold = { low: 10, medium: 10, high: 10 };
|
|
132
|
+
|
|
133
|
+
// Stand in for the SEP-30 enrolment round trip so these tests exercise the
|
|
134
|
+
// validation alone, with no recovery server or SEP-10 auth involved.
|
|
135
|
+
const mockEnrollment = (
|
|
136
|
+
signers: { serverKey: string; signerKey: string }[],
|
|
137
|
+
) =>
|
|
138
|
+
jest
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
.spyOn(Recovery.prototype as any, "enrollWithRecoveryServer")
|
|
141
|
+
.mockResolvedValue(signers);
|
|
142
|
+
|
|
143
|
+
const recoverableWalletConfig = () => ({
|
|
144
|
+
accountAddress: testingAccountKp,
|
|
145
|
+
deviceAddress: deviceKp,
|
|
146
|
+
accountThreshold: threshold,
|
|
147
|
+
accountIdentity: {},
|
|
148
|
+
signerWeight: { device: 10, recoveryServer: 5 },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterEach(() => {
|
|
152
|
+
jest.restoreAllMocks();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("registerRecoveryServerSigners", () => {
|
|
156
|
+
it("rejects a signer set containing the same address twice", async () => {
|
|
157
|
+
await expect(
|
|
158
|
+
wallet.recovery({ servers: {} }).registerRecoveryServerSigners(
|
|
159
|
+
testingAccountKp,
|
|
160
|
+
[
|
|
161
|
+
{ address: recoverySigner1Kp, weight: 5 },
|
|
162
|
+
{ address: recoverySigner1Kp, weight: 5 },
|
|
163
|
+
{ address: deviceKp, weight: 10 },
|
|
164
|
+
],
|
|
165
|
+
threshold,
|
|
166
|
+
),
|
|
167
|
+
).rejects.toThrow(DuplicateAccountSignerError);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("names the duplicated address", async () => {
|
|
171
|
+
await expect(
|
|
172
|
+
wallet.recovery({ servers: {} }).registerRecoveryServerSigners(
|
|
173
|
+
testingAccountKp,
|
|
174
|
+
[
|
|
175
|
+
{ address: recoverySigner1Kp, weight: 5 },
|
|
176
|
+
{ address: recoverySigner1Kp, weight: 5 },
|
|
177
|
+
],
|
|
178
|
+
threshold,
|
|
179
|
+
),
|
|
180
|
+
).rejects.toThrow(recoverySigner1Kp.publicKey);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("rejects a signer equal to the account being locked", async () => {
|
|
184
|
+
await expect(
|
|
185
|
+
wallet.recovery({ servers: {} }).registerRecoveryServerSigners(
|
|
186
|
+
testingAccountKp,
|
|
187
|
+
[
|
|
188
|
+
{ address: testingAccountKp, weight: 5 },
|
|
189
|
+
{ address: deviceKp, weight: 10 },
|
|
190
|
+
],
|
|
191
|
+
threshold,
|
|
192
|
+
),
|
|
193
|
+
).rejects.toThrow(SignerKeyEqualsMasterKeyError);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("validates before reaching the network", async () => {
|
|
197
|
+
const getInfo = jest.spyOn(AccountService.prototype, "getInfo");
|
|
198
|
+
|
|
199
|
+
await expect(
|
|
200
|
+
wallet.recovery({ servers: {} }).registerRecoveryServerSigners(
|
|
201
|
+
testingAccountKp,
|
|
202
|
+
[
|
|
203
|
+
{ address: recoverySigner1Kp, weight: 5 },
|
|
204
|
+
{ address: recoverySigner1Kp, weight: 5 },
|
|
205
|
+
],
|
|
206
|
+
threshold,
|
|
207
|
+
),
|
|
208
|
+
).rejects.toThrow(DuplicateAccountSignerError);
|
|
209
|
+
|
|
210
|
+
expect(getInfo).not.toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("accepts a distinct signer set", async () => {
|
|
214
|
+
const transaction = await wallet
|
|
215
|
+
.recovery({ servers: {} })
|
|
216
|
+
.registerRecoveryServerSigners(
|
|
217
|
+
testingAccountKp,
|
|
218
|
+
[
|
|
219
|
+
{ address: recoverySigner1Kp, weight: 5 },
|
|
220
|
+
{ address: recoverySigner2Kp, weight: 5 },
|
|
221
|
+
{ address: deviceKp, weight: 10 },
|
|
222
|
+
],
|
|
223
|
+
threshold,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(transaction.toXDR()).toBeTruthy();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("createRecoverableWallet", () => {
|
|
231
|
+
it("rejects two servers returning the same signer key", async () => {
|
|
232
|
+
mockEnrollment([
|
|
233
|
+
{ serverKey: "server1", signerKey: recoverySigner1Kp.publicKey },
|
|
234
|
+
{ serverKey: "server2", signerKey: recoverySigner1Kp.publicKey },
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
await expect(
|
|
238
|
+
wallet
|
|
239
|
+
.recovery({ servers: {} })
|
|
240
|
+
.createRecoverableWallet(recoverableWalletConfig()),
|
|
241
|
+
).rejects.toThrow(DuplicateRecoverySignerError);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("names both offending servers and the shared key", async () => {
|
|
245
|
+
mockEnrollment([
|
|
246
|
+
{ serverKey: "server1", signerKey: recoverySigner1Kp.publicKey },
|
|
247
|
+
{ serverKey: "server2", signerKey: recoverySigner1Kp.publicKey },
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
const error = await wallet
|
|
251
|
+
.recovery({ servers: {} })
|
|
252
|
+
.createRecoverableWallet(recoverableWalletConfig())
|
|
253
|
+
.catch((e) => e);
|
|
254
|
+
|
|
255
|
+
expect(error.message).toContain("server1");
|
|
256
|
+
expect(error.message).toContain("server2");
|
|
257
|
+
expect(error.message).toContain(recoverySigner1Kp.publicKey);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("rejects a server returning the device key", async () => {
|
|
261
|
+
mockEnrollment([
|
|
262
|
+
{ serverKey: "server1", signerKey: recoverySigner1Kp.publicKey },
|
|
263
|
+
{ serverKey: "server2", signerKey: deviceKp.publicKey },
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
await expect(
|
|
267
|
+
wallet
|
|
268
|
+
.recovery({ servers: {} })
|
|
269
|
+
.createRecoverableWallet(recoverableWalletConfig()),
|
|
270
|
+
).rejects.toThrow(RecoverySignerEqualsDeviceKeyError);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("rejects a server returning the account key", async () => {
|
|
274
|
+
mockEnrollment([
|
|
275
|
+
{ serverKey: "server1", signerKey: recoverySigner1Kp.publicKey },
|
|
276
|
+
{ serverKey: "server2", signerKey: testingAccountKp.publicKey },
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
await expect(
|
|
280
|
+
wallet
|
|
281
|
+
.recovery({ servers: {} })
|
|
282
|
+
.createRecoverableWallet(recoverableWalletConfig()),
|
|
283
|
+
).rejects.toThrow(SignerKeyEqualsMasterKeyError);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("installs one distinct signer per server plus the device", async () => {
|
|
287
|
+
mockEnrollment([
|
|
288
|
+
{ serverKey: "server1", signerKey: recoverySigner1Kp.publicKey },
|
|
289
|
+
{ serverKey: "server2", signerKey: recoverySigner2Kp.publicKey },
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
const { transaction, signers } = await wallet
|
|
293
|
+
.recovery({ servers: {} })
|
|
294
|
+
.createRecoverableWallet(recoverableWalletConfig());
|
|
295
|
+
|
|
296
|
+
expect(signers).toEqual([
|
|
297
|
+
recoverySigner1Kp.publicKey,
|
|
298
|
+
recoverySigner2Kp.publicKey,
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
// This is the assertion that would have caught the original bug: the
|
|
302
|
+
// signer set reaching the ledger must have the cardinality we intended,
|
|
303
|
+
// because SetOptions overwrites rather than accumulates.
|
|
304
|
+
const signerAddresses = transaction.operations
|
|
305
|
+
.filter((op) => op.type === "setOptions" && op.signer)
|
|
306
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
307
|
+
.map((op: any) => op.signer.ed25519PublicKey);
|
|
308
|
+
|
|
309
|
+
expect(new Set(signerAddresses).size).toBe(3);
|
|
310
|
+
expect(signerAddresses).toEqual(
|
|
311
|
+
expect.arrayContaining([
|
|
312
|
+
recoverySigner1Kp.publicKey,
|
|
313
|
+
recoverySigner2Kp.publicKey,
|
|
314
|
+
deviceKp.publicKey,
|
|
315
|
+
]),
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("produces the weight profile the integration test asserts on-chain", async () => {
|
|
320
|
+
mockEnrollment([
|
|
321
|
+
{ serverKey: "server1", signerKey: recoverySigner1Kp.publicKey },
|
|
322
|
+
{ serverKey: "server2", signerKey: recoverySigner2Kp.publicKey },
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
const { transaction } = await wallet
|
|
326
|
+
.recovery({ servers: {} })
|
|
327
|
+
.createRecoverableWallet(recoverableWalletConfig());
|
|
328
|
+
|
|
329
|
+
// Mirrors test/integration/recovery.test.ts, which asserts the submitted
|
|
330
|
+
// account ends up with signer weights [0, 5, 5, 10]. Guards against the
|
|
331
|
+
// new validation false-rejecting a correct two-server enrolment.
|
|
332
|
+
const weights = transaction.operations
|
|
333
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
334
|
+
.filter((op: any) => op.type === "setOptions" && op.signer)
|
|
335
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
336
|
+
.map((op: any) => op.signer.weight)
|
|
337
|
+
.concat(0) // the locked master key
|
|
338
|
+
.sort((a: number, b: number) => a - b);
|
|
339
|
+
|
|
340
|
+
expect(weights).toEqual([0, 5, 5, 10]);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|