@stellar/typescript-wallet-sdk 4.0.1 → 5.0.0-beta.1788982897683

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.
Files changed (41) hide show
  1. package/CHANGELOG.MD +27 -0
  2. package/examples/sep10/sep10Server.ts +1 -1
  3. package/examples/sep24/sep24.ts +1 -1
  4. package/examples/tsconfig.json +1 -0
  5. package/jest.e2e.config.js +2 -1
  6. package/jest.integration.config.js +2 -1
  7. package/lib/bundle.js +58580 -22168
  8. package/lib/bundle.js.map +1 -1
  9. package/lib/bundle_browser.js +49268 -12817
  10. package/lib/bundle_browser.js.map +1 -1
  11. package/lib/walletSdk/Customer/index.d.ts +4 -4
  12. package/lib/walletSdk/Exceptions/index.d.ts +3 -0
  13. package/lib/walletSdk/Horizon/AccountService.d.ts +2 -3
  14. package/lib/walletSdk/Horizon/Transaction/TransactionBuilder.d.ts +17 -14
  15. package/lib/walletSdk/Types/horizon.d.ts +11 -5
  16. package/lib/walletSdk/Types/sep12.d.ts +1 -2
  17. package/lib/walletSdk/Uri/Sep7Base.d.ts +1 -1
  18. package/lib/walletSdk/Watcher/index.d.ts +1 -1
  19. package/package.json +7 -6
  20. package/src/walletSdk/Auth/AuthHeaderSigner.ts +11 -5
  21. package/src/walletSdk/Auth/WalletSigner.ts +1 -1
  22. package/src/walletSdk/Auth/index.ts +4 -3
  23. package/src/walletSdk/Customer/index.ts +6 -6
  24. package/src/walletSdk/Exceptions/index.ts +8 -0
  25. package/src/walletSdk/Horizon/AccountService.ts +2 -2
  26. package/src/walletSdk/Horizon/Stellar.ts +1 -1
  27. package/src/walletSdk/Horizon/Transaction/TransactionBuilder.ts +31 -19
  28. package/src/walletSdk/Recovery/AccountRecover.ts +1 -1
  29. package/src/walletSdk/Server/index.ts +2 -2
  30. package/src/walletSdk/Types/horizon.ts +16 -5
  31. package/src/walletSdk/Types/sep12.ts +1 -1
  32. package/src/walletSdk/Uri/Sep7Base.ts +10 -9
  33. package/src/walletSdk/Uri/Sep7Tx.ts +1 -1
  34. package/src/walletSdk/Watcher/index.ts +5 -3
  35. package/test/auth.test.ts +42 -0
  36. package/test/e2e/reactNative.test.ts +190 -0
  37. package/test/sep7.test.ts +72 -7
  38. package/test/transaction.test.ts +107 -4
  39. package/test/tsconfig.json +1 -0
  40. package/test/wallet.test.ts +122 -0
  41. package/webpack.config.js +0 -4
@@ -168,7 +168,8 @@ export class Watcher {
168
168
  const newTransactions = transactions.filter((transaction) => {
169
169
  const isInProgress =
170
170
  transaction.status.indexOf("pending") === 0 ||
171
- transaction.status === TransactionStatus.incomplete;
171
+ transaction.status === TransactionStatus.incomplete ||
172
+ transaction.status === TransactionStatus.on_hold;
172
173
  const registeredTransaction =
173
174
  this._transactionsRegistry[assetCode][transaction.id];
174
175
 
@@ -275,7 +276,7 @@ export class Watcher {
275
276
 
276
277
  /**
277
278
  * Watch a transaction until it stops pending. Takes three callbacks:
278
- * * onMessage - When the transaction comes back as pending_ or incomplete.
279
+ * * onMessage - When the transaction comes back as pending_, incomplete, or on_hold.
279
280
  * * onSuccess - When the transaction comes back as completed / refunded / expired.
280
281
  * * onError - When there's a runtime error, or the transaction comes back as
281
282
  * no_market / too_small / too_large / error.
@@ -368,7 +369,8 @@ export class Watcher {
368
369
 
369
370
  if (
370
371
  transaction.status.indexOf("pending") === 0 ||
371
- transaction.status === TransactionStatus.incomplete
372
+ transaction.status === TransactionStatus.incomplete ||
373
+ transaction.status === TransactionStatus.on_hold
372
374
  ) {
373
375
  if (this._oneTransactionWatcher[assetCode][id]) {
374
376
  clearTimeout(this._oneTransactionWatcher[assetCode][id]);
package/test/auth.test.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  MuxedAccount,
8
8
  Networks,
9
9
  StellarToml,
10
+ StrKey,
10
11
  Transaction,
11
12
  TransactionBuilder as SdkTransactionBuilder,
12
13
  Operation,
@@ -15,8 +16,12 @@ import {
15
16
  import { randomBytes } from "crypto";
16
17
  import axios from "axios";
17
18
  import sinon from "sinon";
19
+ import { base64ToString, base64ToUint8Array } from "uint8array-extras";
20
+ import { encode as utf8Encode } from "@stablelib/utf8";
21
+ import nacl from "tweetnacl";
18
22
 
19
23
  import { validateToken, Sep10, type WalletSigner } from "../src/walletSdk/Auth";
24
+ import { DefaultAuthHeaderSigner } from "../src/walletSdk/Auth/AuthHeaderSigner";
20
25
  import {
21
26
  Config,
22
27
  StellarConfiguration,
@@ -837,3 +842,40 @@ describe("Anchor.sep10() signing key handling", () => {
837
842
  expect(sep10).toBeDefined();
838
843
  });
839
844
  });
845
+
846
+ describe("auth header JWT encoding", () => {
847
+ it("signs with unpadded base64url and verifies against the public key", async () => {
848
+ const kp = SigningKeypair.fromSecret(Keypair.random().secret());
849
+ const signer = new DefaultAuthHeaderSigner();
850
+
851
+ const jwt = await signer.createToken({
852
+ claims: {
853
+ account: kp.publicKey,
854
+ home_domain: "test.com",
855
+ web_auth_endpoint: "https://test.com/auth",
856
+ },
857
+ issuer: kp,
858
+ });
859
+
860
+ const [header, payload, signature] = jwt.split(".");
861
+
862
+ // base64url: no +, / or = padding in any segment.
863
+ for (const segment of [header, payload, signature]) {
864
+ expect(segment).not.toMatch(/[+/=]/);
865
+ }
866
+
867
+ // The header must still decode to real JSON.
868
+ expect(JSON.parse(base64ToString(header))).toEqual({ alg: "EdDSA" });
869
+
870
+ // The signature must verify over "<header>.<payload>".
871
+ const naclKP = nacl.sign.keyPair.fromSeed(
872
+ StrKey.decodeEd25519SecretSeed(kp.secretKey),
873
+ );
874
+ const verified = nacl.sign.detached.verify(
875
+ utf8Encode(`${header}.${payload}`),
876
+ base64ToUint8Array(signature),
877
+ naclKP.publicKey,
878
+ );
879
+ expect(verified).toBe(true);
880
+ });
881
+ });
@@ -0,0 +1,190 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as vm from "vm";
4
+
5
+ /**
6
+ * React Native's Hermes runtime provides none of Buffer, Event, EventTarget or
7
+ * process. Loading each published bundle in a context without them catches the
8
+ * two failure modes this SDK has actually shipped: a reliance on the Buffer
9
+ * global, and dependencies that subclass the DOM Event globals at module scope.
10
+ *
11
+ * The sandbox also *grants* a few globals that a real RN app must supply
12
+ * itself, so this file is explicit about what environment it is modelling:
13
+ *
14
+ * - `crypto`: Hermes/RN does not provide this either. A real RN app needs
15
+ * `react-native-get-random-values` (or equivalent) for stellar-sdk's RNG
16
+ * path. This was already true under v16 and is not new to this migration.
17
+ * - `btoa`/`atob`: see the dedicated comment below.
18
+ * - `TextEncoder`/`TextDecoder`: see the dedicated comment below.
19
+ */
20
+ const BANNED = ["Buffer", "Event", "EventTarget", "process"];
21
+
22
+ const makeHermesLikeContext = ({
23
+ includeBase64 = true,
24
+ includeTextCodec = true,
25
+ } = {}) => {
26
+ const sandbox: Record<string, unknown> = {
27
+ console,
28
+ URL,
29
+ URLSearchParams,
30
+ setTimeout,
31
+ clearTimeout,
32
+ setInterval,
33
+ clearInterval,
34
+ crypto: globalThis.crypto,
35
+ fetch: globalThis.fetch,
36
+ module: { exports: {} },
37
+ exports: {},
38
+ };
39
+
40
+ if (includeTextCodec) {
41
+ // TextEncoder/TextDecoder are a real consumer requirement, not a test
42
+ // convenience, and the requirement is unavoidable: uint8array-extras@1.5.0
43
+ // — a direct dependency of ours and a transitive dependency of
44
+ // stellar-sdk — constructs both at module initialization, unguarded:
45
+ // `new globalThis.TextDecoder('utf8')` (index.js:113, inside a
46
+ // module-scope object literal) and `new globalThis.TextEncoder()`
47
+ // (index.js:128). By contrast, @exodus/bytes DOES guard this —
48
+ // fallback/platform.native.js only constructs them when
49
+ // `isNative(globalThis.TextDecoder)` holds, falling back to pure-JS
50
+ // decoding otherwise — so the requirement traces specifically to
51
+ // uint8array-extras, not to @exodus/bytes. Granting them here models an
52
+ // environment that satisfies this prerequisite. We cannot verify from
53
+ // here whether React Native itself provides them (this file makes no
54
+ // claim either way), but RN apps commonly polyfill TextEncoder/TextDecoder
55
+ // already for other libraries. Remove them and every bundle throws
56
+ // "TypeError: globalThis.TextDecoder is not a constructor" at load —
57
+ // before any of our own code runs. See the test below that enforces this.
58
+ sandbox.TextEncoder = TextEncoder;
59
+ sandbox.TextDecoder = TextDecoder;
60
+ }
61
+
62
+ if (includeBase64) {
63
+ // btoa/atob are a real consumer requirement of this release, not a test
64
+ // convenience. stellar-sdk v17's xdr.encodeBytes(bytes, "base64") routes
65
+ // through its own lib/cjs/base/util/base64.js, which calls the bare `btoa`
66
+ // identifier with no fallback, and Task 2 removed the browser bundle's
67
+ // Buffer polyfill that used to cover it. Hermes provides btoa/atob
68
+ // natively as of React Native 0.74 (facebook/hermes#1255, #1256); React
69
+ // Native's own JS layer has never defined them, so RN < 0.74 and
70
+ // non-Hermes engines (JSC, V8) need a polyfill such as `base-64`.
71
+ // Granting them here models a supported RN >= 0.74 Hermes app. Remove
72
+ // them and every base64 path throws "ReferenceError: btoa is not
73
+ // defined"; hex and base32/StrKey still work, being pure JS.
74
+ sandbox.btoa = globalThis.btoa;
75
+ sandbox.atob = globalThis.atob;
76
+ }
77
+
78
+ sandbox.global = sandbox;
79
+ sandbox.globalThis = sandbox;
80
+
81
+ const context = vm.createContext(sandbox);
82
+ for (const name of BANNED) {
83
+ vm.runInContext(
84
+ `if (typeof ${name} !== "undefined") { throw new Error("${name} leaked into the sandbox"); }`,
85
+ context,
86
+ );
87
+ }
88
+ return context;
89
+ };
90
+
91
+ const bundles = [
92
+ ["core SDK", "../../lib/bundle_browser.js"],
93
+ ["key manager", "../../../typescript-wallet-sdk-km/lib/bundle_browser.js"],
94
+ ["soroban", "../../../typescript-wallet-sdk-soroban/lib/bundle_browser.js"],
95
+ ] as const;
96
+
97
+ describe("React Native (Hermes-like) bundle compatibility", () => {
98
+ for (const [name, relativePath] of bundles) {
99
+ it(`loads the ${name} bundle without Buffer/Event/EventTarget/process`, () => {
100
+ const bundlePath = path.resolve(__dirname, relativePath);
101
+ expect(fs.existsSync(bundlePath)).toBe(true);
102
+
103
+ const context = makeHermesLikeContext();
104
+ const code = fs.readFileSync(bundlePath, "utf8");
105
+
106
+ expect(() =>
107
+ vm.runInContext(code, context, { filename: `${name}.js` }),
108
+ ).not.toThrow();
109
+ });
110
+ }
111
+
112
+ for (const [name, relativePath] of bundles) {
113
+ it(`fails to load the ${name} bundle without TextEncoder/TextDecoder`, () => {
114
+ const bundlePath = path.resolve(__dirname, relativePath);
115
+ const context = makeHermesLikeContext({ includeTextCodec: false });
116
+ const code = fs.readFileSync(bundlePath, "utf8");
117
+
118
+ // uint8array-extras constructs its TextDecoder/TextEncoder at module
119
+ // initialization (see the comment in makeHermesLikeContext), so the
120
+ // failure happens at load time, before any of our own code runs. This
121
+ // turns the assumption granted above into an enforced, documented
122
+ // contract rather than a silent one.
123
+ expect(() =>
124
+ vm.runInContext(code, context, { filename: `${name}.js` }),
125
+ ).toThrow(/TextDecoder is not a constructor/);
126
+ });
127
+ }
128
+
129
+ it("exercises the core SDK public API in the sandbox", () => {
130
+ const context = makeHermesLikeContext();
131
+ const code = fs.readFileSync(
132
+ path.resolve(__dirname, "../../lib/bundle_browser.js"),
133
+ "utf8",
134
+ );
135
+ vm.runInContext(code, context, { filename: "bundle_browser.js" });
136
+
137
+ // Sep7Pay.addSignature is the densest no-Buffer path in our own code: it
138
+ // runs concatUint8Arrays + stringToUint8Array to build the payload, signs
139
+ // it, then base64-encodes via xdr.encodeBytes.
140
+ const result = vm.runInContext(
141
+ `(() => {
142
+ const { Wallet, Sep7Pay } = module.exports;
143
+ const kp = Wallet.TestNet().stellar().account().createKeypair();
144
+ const uri = Sep7Pay.forDestination(kp.publicKey);
145
+ return { publicKey: kp.publicKey, signature: uri.addSignature(kp.keypair) };
146
+ })()`,
147
+ context,
148
+ ) as { publicKey: string; signature: string };
149
+
150
+ expect(result.publicKey).toMatch(/^G[A-Z2-7]{55}$/);
151
+ // The decisive assertion: base64 encoding worked without Buffer. A broken
152
+ // encoder yields comma-joined decimals.
153
+ expect(result.signature).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
154
+ expect(result.signature).toHaveLength(88);
155
+ });
156
+
157
+ it("requires btoa/atob for base64 paths, while base32/StrKey keypair creation stays pure JS", () => {
158
+ const context = makeHermesLikeContext({ includeBase64: false });
159
+ const code = fs.readFileSync(
160
+ path.resolve(__dirname, "../../lib/bundle_browser.js"),
161
+ "utf8",
162
+ );
163
+ vm.runInContext(code, context, { filename: "bundle_browser.js" });
164
+
165
+ // Keypair generation and the public key's StrKey encoding are pure JS
166
+ // (base32, not base64), so they must work even without btoa/atob.
167
+ const publicKey = vm.runInContext(
168
+ `(() => {
169
+ const { Wallet } = module.exports;
170
+ return Wallet.TestNet().stellar().account().createKeypair().publicKey;
171
+ })()`,
172
+ context,
173
+ ) as string;
174
+ expect(publicKey).toMatch(/^G[A-Z2-7]{55}$/);
175
+
176
+ // Sep7Pay.addSignature's base64 encode step has no fallback: without
177
+ // btoa this must fail loudly rather than silently degrade.
178
+ expect(() =>
179
+ vm.runInContext(
180
+ `(() => {
181
+ const { Wallet, Sep7Pay } = module.exports;
182
+ const kp = Wallet.TestNet().stellar().account().createKeypair();
183
+ const uri = Sep7Pay.forDestination(kp.publicKey);
184
+ return uri.addSignature(kp.keypair);
185
+ })()`,
186
+ context,
187
+ ),
188
+ ).toThrow(/btoa is not defined/);
189
+ });
190
+ });
package/test/sep7.test.ts CHANGED
@@ -1,4 +1,13 @@
1
- import { Keypair, Networks, StellarToml } from "@stellar/stellar-sdk";
1
+ import {
2
+ Account,
3
+ Asset,
4
+ BASE_FEE,
5
+ Keypair,
6
+ Networks,
7
+ Operation,
8
+ StellarToml,
9
+ TransactionBuilder,
10
+ } from "@stellar/stellar-sdk";
2
11
  import {
3
12
  NativeAssetId,
4
13
  Sep7Pay,
@@ -360,7 +369,7 @@ describe("Sep7Tx", () => {
360
369
  txBuilder.transfer(testKp2.publicKey, new NativeAssetId(), "1");
361
370
  const tx = txBuilder.build();
362
371
 
363
- const xdr = tx.toEnvelope().toXDR().toString("base64");
372
+ const xdr = tx.toEnvelope().toXdr("base64");
364
373
 
365
374
  const uri = Sep7Tx.forTransaction(tx);
366
375
  expect(uri.operationType).toBe("tx");
@@ -488,7 +497,7 @@ describe("Sep7Tx", () => {
488
497
  txBuilder.transfer(testKp2.publicKey, new NativeAssetId(), "1");
489
498
  const tx = txBuilder.build();
490
499
 
491
- const xdr = tx.toEnvelope().toXDR().toString("base64");
500
+ const xdr = tx.toEnvelope().toXdr("base64");
492
501
 
493
502
  const uri = Sep7Tx.forTransaction(tx);
494
503
 
@@ -553,13 +562,11 @@ describe("Sep7Tx", () => {
553
562
  txBuilder.transfer(testKp2.publicKey, new NativeAssetId(), "1");
554
563
  const tx = txBuilder.build();
555
564
 
556
- const xdr = tx.toEnvelope().toXDR().toString("base64");
565
+ const xdr = tx.toEnvelope().toXdr("base64");
557
566
 
558
567
  const uri = new Sep7Tx(`web+stellar:tx?xdr=${encodeURIComponent(xdr)}`);
559
568
 
560
- expect(uri.getTransaction().toEnvelope().toXDR().toString("base64")).toBe(
561
- xdr,
562
- );
569
+ expect(uri.getTransaction().toEnvelope().toXdr("base64")).toBe(xdr);
563
570
  });
564
571
  });
565
572
 
@@ -1002,3 +1009,61 @@ describe("sep7Parser", () => {
1002
1009
  expect(actual).toBe(expected);
1003
1010
  });
1004
1011
  });
1012
+
1013
+ describe("SEP-7 byte encoding regressions", () => {
1014
+ const BASE64_ONLY = /^[A-Za-z0-9+/]+={0,2}$/;
1015
+
1016
+ it("produces a base64 signature, not comma-joined decimals", () => {
1017
+ const keypair = Keypair.random();
1018
+ const uri = Sep7Pay.forDestination(
1019
+ "GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO",
1020
+ );
1021
+
1022
+ const signature = uri.addSignature(keypair);
1023
+
1024
+ expect(signature).toMatch(BASE64_ONLY);
1025
+ expect(signature).not.toContain(",");
1026
+ // An ed25519 signature is 64 bytes => 88 base64 characters with padding.
1027
+ expect(signature).toHaveLength(88);
1028
+ });
1029
+
1030
+ it("round-trips a signature through verification", async () => {
1031
+ const keypair = Keypair.random();
1032
+ const uri = Sep7Pay.forDestination(
1033
+ "GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO",
1034
+ );
1035
+ uri.originDomain = "place.domain.com";
1036
+ uri.addSignature(keypair);
1037
+
1038
+ jest
1039
+ .spyOn(StellarToml.Resolver, "resolve")
1040
+ .mockResolvedValue({ URI_REQUEST_SIGNING_KEY: keypair.publicKey() });
1041
+
1042
+ await expect(uri.verifySignature()).resolves.toBe(true);
1043
+ });
1044
+
1045
+ it("puts parseable XDR in the uri, not comma-joined decimals", () => {
1046
+ const keypair = Keypair.random();
1047
+ const account = new Account(keypair.publicKey(), "1");
1048
+ const tx = new TransactionBuilder(account, {
1049
+ fee: BASE_FEE,
1050
+ networkPassphrase: Networks.TESTNET,
1051
+ })
1052
+ .addOperation(
1053
+ Operation.payment({
1054
+ destination: keypair.publicKey(),
1055
+ asset: Asset.native(),
1056
+ amount: "1",
1057
+ }),
1058
+ )
1059
+ .setTimeout(30)
1060
+ .build();
1061
+
1062
+ const uri = Sep7Tx.forTransaction(tx);
1063
+
1064
+ expect(uri.xdr).toMatch(BASE64_ONLY);
1065
+ expect(uri.xdr).not.toContain(",");
1066
+ // The decisive check: it must parse back into the same transaction.
1067
+ expect(uri.getTransaction().toXdr()).toEqual(tx.toXdr());
1068
+ });
1069
+ });
@@ -1,8 +1,24 @@
1
1
  import axios from "axios";
2
- import { Horizon, MuxedAccount } from "@stellar/stellar-sdk";
3
-
4
- import { AccountService, SigningKeypair, Stellar, Wallet } from "../src";
2
+ import {
3
+ Account,
4
+ Horizon,
5
+ Keypair,
6
+ MuxedAccount,
7
+ Operation,
8
+ } from "@stellar/stellar-sdk";
9
+
10
+ import {
11
+ AccountService,
12
+ ApplicationConfiguration,
13
+ Config,
14
+ SigningKeypair,
15
+ Stellar,
16
+ StellarConfiguration,
17
+ TransactionBuilder,
18
+ Wallet,
19
+ } from "../src";
5
20
  import { IssuedAssetId, NativeAssetId } from "../src/walletSdk/Asset";
21
+ import { PathPayBoundRequiredError } from "../src/walletSdk/Exceptions";
6
22
 
7
23
  describe("Muxed Transactions", () => {
8
24
  let wallet: Wallet;
@@ -289,6 +305,7 @@ describe("Path Payment", () => {
289
305
  sendAsset: new NativeAssetId(),
290
306
  destAsset: usdcAsset,
291
307
  sendAmount: "5",
308
+ destMin: "4.9",
292
309
  })
293
310
  .build();
294
311
  expect(txn.operations[0].type).toBe("pathPaymentStrictSend");
@@ -304,6 +321,7 @@ describe("Path Payment", () => {
304
321
  sendAsset: new NativeAssetId(),
305
322
  destAsset: usdcAsset,
306
323
  destAmount: "5",
324
+ sendMax: "5.1",
307
325
  })
308
326
  .build();
309
327
  expect(txn.operations[0].type).toBe("pathPaymentStrictReceive");
@@ -314,10 +332,95 @@ describe("Path Payment", () => {
314
332
  sourceAddress: sourceKp,
315
333
  });
316
334
  const txn = txBuilder
317
- .swap(new NativeAssetId(), new NativeAssetId(), ".1")
335
+ .swap(new NativeAssetId(), new NativeAssetId(), ".1", ".1")
318
336
  .build();
319
337
  sourceKp.sign(txn);
320
338
  const success = await stellar.submitTransaction(txn);
321
339
  expect(success).toBe(true);
322
340
  }, 15000);
323
341
  });
342
+
343
+ describe("Path Payment bounds", () => {
344
+ const usdcAsset = new IssuedAssetId(
345
+ "USDC",
346
+ "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
347
+ );
348
+ const destination = Keypair.random().publicKey();
349
+
350
+ // Builds a TransactionBuilder offline so these tests never touch Horizon
351
+ const newBuilder = () =>
352
+ new TransactionBuilder(
353
+ new Config({
354
+ stellarConfiguration: StellarConfiguration.TestNet(),
355
+ applicationConfiguration: new ApplicationConfiguration(),
356
+ }),
357
+ new Account(Keypair.random().publicKey(), "0"),
358
+ );
359
+
360
+ it("throws when sendAmount is given without destMin", () => {
361
+ expect(() =>
362
+ newBuilder().pathPay({
363
+ destinationAddress: destination,
364
+ sendAsset: new NativeAssetId(),
365
+ destAsset: usdcAsset,
366
+ sendAmount: "5",
367
+ } as any),
368
+ ).toThrow(PathPayBoundRequiredError);
369
+ });
370
+
371
+ it("throws when destAmount is given without sendMax", () => {
372
+ expect(() =>
373
+ newBuilder().pathPay({
374
+ destinationAddress: destination,
375
+ sendAsset: new NativeAssetId(),
376
+ destAsset: usdcAsset,
377
+ destAmount: "5",
378
+ } as any),
379
+ ).toThrow(PathPayBoundRequiredError);
380
+ });
381
+
382
+ it("throws when swap is called without destMin", () => {
383
+ expect(() =>
384
+ (newBuilder() as any).swap(new NativeAssetId(), usdcAsset, "1"),
385
+ ).toThrow(PathPayBoundRequiredError);
386
+ });
387
+
388
+ it("uses the destMin given to pathPay", () => {
389
+ const txn = newBuilder()
390
+ .pathPay({
391
+ destinationAddress: destination,
392
+ sendAsset: new NativeAssetId(),
393
+ destAsset: usdcAsset,
394
+ sendAmount: "5",
395
+ destMin: "4.9",
396
+ })
397
+ .build();
398
+ const op = txn.operations[0] as Operation.PathPaymentStrictSend;
399
+ expect(op.type).toBe("pathPaymentStrictSend");
400
+ expect(op.destMin).toBe("4.9000000");
401
+ });
402
+
403
+ it("uses the sendMax given to pathPay", () => {
404
+ const txn = newBuilder()
405
+ .pathPay({
406
+ destinationAddress: destination,
407
+ sendAsset: new NativeAssetId(),
408
+ destAsset: usdcAsset,
409
+ destAmount: "5",
410
+ sendMax: "5.1",
411
+ })
412
+ .build();
413
+ const op = txn.operations[0] as Operation.PathPaymentStrictReceive;
414
+ expect(op.type).toBe("pathPaymentStrictReceive");
415
+ expect(op.sendMax).toBe("5.1000000");
416
+ });
417
+
418
+ it("uses the destMin given to swap", () => {
419
+ const txn = newBuilder()
420
+ .swap(new NativeAssetId(), usdcAsset, "1", "0.9")
421
+ .build();
422
+ const op = txn.operations[0] as Operation.PathPaymentStrictSend;
423
+ expect(op.type).toBe("pathPaymentStrictSend");
424
+ expect(op.destMin).toBe("0.9000000");
425
+ });
426
+ });
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "esModuleInterop": true,
4
+ "skipLibCheck": true,
4
5
  "baseUrl": "./",
5
6
  "outDir": "lib",
6
7
  "declaration": true,
@@ -930,6 +930,83 @@ describe("Anchor", () => {
930
930
  // stops watching
931
931
  stop();
932
932
  });
933
+
934
+ test("on_hold is transient: not ignored on first poll", async () => {
935
+ const onMessage = sinon.spy();
936
+ const onError = sinon.spy();
937
+
938
+ const onHoldTransaction = makeTransaction(0, TransactionStatus.on_hold);
939
+ const completedTransaction = {
940
+ ...onHoldTransaction,
941
+ status: TransactionStatus.completed,
942
+ };
943
+
944
+ // first poll returns on_hold, second returns completed
945
+ jest
946
+ .spyOn(Sep24.prototype, "getTransactionsForAsset")
947
+ .mockResolvedValueOnce([onHoldTransaction])
948
+ .mockResolvedValueOnce([completedTransaction]);
949
+
950
+ const { stop } = watcher.watchAllTransactions({
951
+ authToken,
952
+ assetCode: "SRT",
953
+ lang: "en-US",
954
+ onMessage,
955
+ onError,
956
+ timeout: 1,
957
+ });
958
+
959
+ await sleep(1);
960
+
961
+ // on_hold must be treated as in-progress, not silently ignored
962
+ expect(onError.callCount).toBe(0);
963
+ expect(onMessage.callCount).toBe(1);
964
+
965
+ clock.next();
966
+ await sleep(1);
967
+
968
+ // now completed, reported as a status change
969
+ expect(onError.callCount).toBe(0);
970
+ expect(onMessage.callCount).toBe(2);
971
+
972
+ stop();
973
+ });
974
+
975
+ test("on_hold reported once when status unchanged across polls", async () => {
976
+ const onMessage = sinon.spy();
977
+ const onError = sinon.spy();
978
+
979
+ const onHoldTransaction = makeTransaction(0, TransactionStatus.on_hold);
980
+
981
+ // two consecutive polls return the same on_hold status
982
+ jest
983
+ .spyOn(Sep24.prototype, "getTransactionsForAsset")
984
+ .mockResolvedValueOnce([onHoldTransaction])
985
+ .mockResolvedValueOnce([onHoldTransaction]);
986
+
987
+ const { stop } = watcher.watchAllTransactions({
988
+ authToken,
989
+ assetCode: "SRT",
990
+ lang: "en-US",
991
+ onMessage,
992
+ onError,
993
+ timeout: 1,
994
+ });
995
+
996
+ await sleep(1);
997
+
998
+ // first poll: reported
999
+ expect(onMessage.callCount).toBe(1);
1000
+
1001
+ clock.next();
1002
+ await sleep(1);
1003
+
1004
+ // second poll: same status, not reported again
1005
+ expect(onMessage.callCount).toBe(1);
1006
+ expect(onError.callCount).toBe(0);
1007
+
1008
+ stop();
1009
+ });
933
1010
  });
934
1011
 
935
1012
  describe("watchOneTransaction", () => {
@@ -946,6 +1023,51 @@ describe("Anchor", () => {
946
1023
  clock.restore();
947
1024
  });
948
1025
 
1026
+ test("on_hold is transient: keeps watching until completed", async () => {
1027
+ const onMessage = sinon.spy();
1028
+ const onSuccess = sinon.spy();
1029
+ const onError = sinon.spy();
1030
+
1031
+ const onHoldTransaction = makeTransaction(0, TransactionStatus.on_hold);
1032
+ const completedTransaction = {
1033
+ ...onHoldTransaction,
1034
+ status: TransactionStatus.completed,
1035
+ };
1036
+
1037
+ // first poll returns on_hold, the next returns completed
1038
+ jest
1039
+ .spyOn(Sep24.prototype, "getTransactionBy")
1040
+ .mockResolvedValueOnce(onHoldTransaction)
1041
+ .mockResolvedValueOnce(completedTransaction);
1042
+
1043
+ const { stop } = watcher.watchOneTransaction({
1044
+ authToken,
1045
+ assetCode: "SRT",
1046
+ id: onHoldTransaction.id,
1047
+ onMessage,
1048
+ onSuccess,
1049
+ onError,
1050
+ timeout: 1,
1051
+ lang: "en-US",
1052
+ });
1053
+
1054
+ // first poll: on_hold is transient, so it must keep watching, not error out
1055
+ await sleep(1);
1056
+ expect(onError.callCount).toBe(0);
1057
+ expect(onSuccess.callCount).toBe(0);
1058
+ expect(onMessage.callCount).toBe(1);
1059
+
1060
+ clock.next();
1061
+ await sleep(1);
1062
+
1063
+ // second poll: now completed, so onSuccess fires and onError never does
1064
+ expect(onError.callCount).toBe(0);
1065
+ expect(onSuccess.callCount).toBe(1);
1066
+ expect(onMessage.callCount).toBe(1);
1067
+
1068
+ stop();
1069
+ });
1070
+
949
1071
  test("One completed / refunded / expired successes", async () => {
950
1072
  const onMessage = sinon.spy(() => {
951
1073
  expect(onMessage.callCount).toBe(0);
package/webpack.config.js CHANGED
@@ -40,7 +40,6 @@ module.exports = (env = { NODE: false }) => {
40
40
  util: require.resolve("util"),
41
41
  vm: require.resolve("vm-browserify"),
42
42
  "process/browser": require.resolve("process/browser"),
43
- buffer: require.resolve("buffer"),
44
43
  }
45
44
  : {},
46
45
  },
@@ -60,9 +59,6 @@ module.exports = (env = { NODE: false }) => {
60
59
  new webpack.ProvidePlugin({
61
60
  process: "process/browser",
62
61
  }),
63
- new webpack.ProvidePlugin({
64
- Buffer: ["buffer", "Buffer"],
65
- }),
66
62
  ]
67
63
  : [],
68
64
  };