@stellar/typescript-wallet-sdk 5.0.0 → 5.1.0

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
+ });
@@ -11,7 +11,7 @@ describe("SEP-38", () => {
11
11
  anchor = wallet.anchor({ homeDomain: "testanchor.stellar.org" });
12
12
  sep38 = anchor.sep38();
13
13
  accountKp = accountKp = wallet.stellar().account().createKeypair();
14
- }, 10000);
14
+ }, 30000);
15
15
 
16
16
  it("should get Sep-38 anchor info", async () => {
17
17
  const resp = await sep38.info();
package/test/sep6.test.ts CHANGED
@@ -16,7 +16,7 @@ describe("SEP-6", () => {
16
16
  await axios.get(
17
17
  "https://friendbot.stellar.org/?addr=" + accountKp.publicKey,
18
18
  );
19
- }, 10000);
19
+ }, 30000);
20
20
 
21
21
  it("should get anchor info", async () => {
22
22
  const resp = await sep6.info();
@@ -172,7 +172,7 @@ describe("SEP-6", () => {
172
172
  resp = await anchor.sep6().getTransactionBy({ authToken, id });
173
173
 
174
174
  expect(resp.id).toEqual(id);
175
- }, 10000);
175
+ }, 30000);
176
176
 
177
177
  let txId;
178
178
 
@@ -45,7 +45,7 @@ describe("Stellar", () => {
45
45
  } catch (e) {
46
46
  await stellar.fundTestnetAccount(kp.publicKey);
47
47
  }
48
- }, 10000);
48
+ }, 30000);
49
49
 
50
50
  it("should create and submit a transaction", async () => {
51
51
  const now = Math.floor(Date.now() / 1000) - 5;
@@ -130,7 +130,7 @@ describe("Stellar", () => {
130
130
  const feeBumpTx = stellar.makeFeeBump({ feeAddress: kp, transaction });
131
131
  kp.sign(feeBumpTx);
132
132
  await stellar.submitTransaction(feeBumpTx);
133
- }, 10000);
133
+ }, 30000);
134
134
 
135
135
  it("should be able to give a signing keypair", async () => {
136
136
  // mock 1 failed response and then 1 successful to test retry
@@ -242,7 +242,7 @@ describe("Stellar", () => {
242
242
  (b) => (b as Horizon.HorizonApi.BalanceLineAsset).asset_code === "USDC",
243
243
  );
244
244
  expect(balance).toBeFalsy();
245
- }, 20000);
245
+ }, 30000);
246
246
 
247
247
  it("should import and sign a transaction from xdr", () => {
248
248
  const txnXdr =
@@ -373,7 +373,7 @@ describe("Stellar", () => {
373
373
  .build();
374
374
  expect(txn).toBeTruthy();
375
375
  }
376
- }, 20000);
376
+ }, 30000);
377
377
 
378
378
  it("should throw if tx not in pending_user_transfer_start status", async () => {
379
379
  const walletTransaction = {
@@ -402,7 +402,7 @@ describe("Stellar", () => {
402
402
  .transferWithdrawalTransaction(walletTransaction, asset)
403
403
  .build();
404
404
  }).toThrowError(WithdrawalTxNotPendingUserTransferStartError);
405
- }, 5000);
405
+ }, 30000);
406
406
 
407
407
  it("should throw if tx is missing withdraw_anchor_account", async () => {
408
408
  const walletTransaction = {
@@ -430,7 +430,7 @@ describe("Stellar", () => {
430
430
  .transferWithdrawalTransaction(walletTransaction, asset)
431
431
  .build();
432
432
  }).toThrowError(WithdrawalTxMissingDestinationError);
433
- }, 5000);
433
+ }, 30000);
434
434
 
435
435
  it("should throw if tx is missing withdraw_memo_type", async () => {
436
436
  const walletTransaction = {
@@ -459,7 +459,7 @@ describe("Stellar", () => {
459
459
  .transferWithdrawalTransaction(walletTransaction, asset)
460
460
  .build();
461
461
  }).toThrowError(WithdrawalTxMissingMemoError);
462
- }, 5000);
462
+ }, 30000);
463
463
 
464
464
  it("should throw if tx is missing withdraw_memo", async () => {
465
465
  const walletTransaction = {
@@ -488,7 +488,7 @@ describe("Stellar", () => {
488
488
  .transferWithdrawalTransaction(walletTransaction, asset)
489
489
  .build();
490
490
  }).toThrowError(WithdrawalTxMissingMemoError);
491
- }, 5000);
491
+ }, 30000);
492
492
  });
493
493
  });
494
494
 
@@ -509,7 +509,7 @@ describe("SponsoringBuilder", () => {
509
509
  await axios.get(
510
510
  "https://friendbot.stellar.org/?addr=" + txnSourceKp.publicKey,
511
511
  );
512
- }, 15000);
512
+ }, 30000);
513
513
 
514
514
  it("should sponsor creating an account", async () => {
515
515
  const wal = Wallet.TestNet();
@@ -535,7 +535,7 @@ describe("SponsoringBuilder", () => {
535
535
  newKp.publicKey,
536
536
  )) as any;
537
537
  expect(sponsoredLoaded.num_sponsored).toBe(2);
538
- }, 15000);
538
+ }, 30000);
539
539
 
540
540
  it("should sponsor adding trustlines", async () => {
541
541
  const txBuilder = await stellar.transaction({
@@ -560,7 +560,7 @@ describe("SponsoringBuilder", () => {
560
560
  sponsorKp.publicKey,
561
561
  )) as any;
562
562
  expect(sponsorLoaded.num_sponsoring).toBe(3);
563
- }, 15000);
563
+ }, 30000);
564
564
 
565
565
  it("should allow sponsoring and regular operations in same transaction", async () => {
566
566
  const txBuilder = await stellar.transaction({
@@ -588,7 +588,7 @@ describe("SponsoringBuilder", () => {
588
588
  sponsorKp.publicKey,
589
589
  )) as any;
590
590
  expect(sponsorLoaded.num_sponsoring).toBe(3);
591
- }, 15000);
591
+ }, 30000);
592
592
  it("should sponsor account modification", async () => {
593
593
  const txBuilder = await stellar.transaction({
594
594
  sourceAddress: txnSourceKp,
@@ -610,7 +610,7 @@ describe("SponsoringBuilder", () => {
610
610
  sourceLoaded.signers.find((signer) => signer.key === otherKp.publicKey)
611
611
  .weight,
612
612
  ).toBe(2);
613
- }, 15000);
613
+ }, 30000);
614
614
  });
615
615
 
616
616
  describe("Asset", () => {
@@ -293,7 +293,7 @@ describe("Path Payment", () => {
293
293
  sourceKp.sign(txn);
294
294
  await stellar.submitTransaction(txn);
295
295
  }
296
- }, 20000);
296
+ }, 30000);
297
297
 
298
298
  it("should use path payment send", async () => {
299
299
  const txBuilder = await stellar.transaction({
@@ -309,7 +309,7 @@ describe("Path Payment", () => {
309
309
  })
310
310
  .build();
311
311
  expect(txn.operations[0].type).toBe("pathPaymentStrictSend");
312
- }, 15000);
312
+ }, 30000);
313
313
 
314
314
  it("should use path payment receive", async () => {
315
315
  const txBuilder = await stellar.transaction({
@@ -325,7 +325,7 @@ describe("Path Payment", () => {
325
325
  })
326
326
  .build();
327
327
  expect(txn.operations[0].type).toBe("pathPaymentStrictReceive");
328
- }, 15000);
328
+ }, 30000);
329
329
 
330
330
  it("should swap", async () => {
331
331
  const txBuilder = await stellar.transaction({
@@ -337,7 +337,7 @@ describe("Path Payment", () => {
337
337
  sourceKp.sign(txn);
338
338
  const success = await stellar.submitTransaction(txn);
339
339
  expect(success).toBe(true);
340
- }, 15000);
340
+ }, 30000);
341
341
  });
342
342
 
343
343
  describe("Path Payment bounds", () => {
@@ -2054,7 +2054,7 @@ describe("AuthHeaderSigner", () => {
2054
2054
  });
2055
2055
 
2056
2056
  expect(authToken).toBeTruthy();
2057
- }, 15000);
2057
+ }, 30000);
2058
2058
 
2059
2059
  it("should match example implementation generated JWT", async () => {
2060
2060
  const generatedAuthToken =