@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.
@@ -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
- describe("Recovery / Register Signers", () => {
21
- beforeAll(async () => {
22
- wallet = Wallet.TestNet();
23
- const stellar = wallet.stellar();
24
-
25
- // make sure testing accounts exist
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
- try {
35
- await stellar.server.loadAccount(sponsorAccountKp.publicKey);
36
- } catch (e) {
37
- await axios.get(
38
- "https://friendbot.stellar.org/?addr=" + sponsorAccountKp.publicKey,
39
- );
40
- }
41
- }, 60000);
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
+ });