@stellar/typescript-wallet-sdk 4.0.0 → 5.0.0-beta.1788566740000

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 (42) hide show
  1. package/CHANGELOG.MD +39 -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 +58608 -22154
  8. package/lib/bundle.js.map +1 -1
  9. package/lib/bundle_browser.js +49296 -12803
  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 +12 -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/Server/index.d.ts +17 -1
  16. package/lib/walletSdk/Types/auth.d.ts +5 -0
  17. package/lib/walletSdk/Types/horizon.d.ts +11 -5
  18. package/lib/walletSdk/Types/sep12.d.ts +1 -2
  19. package/lib/walletSdk/Uri/Sep7Base.d.ts +1 -1
  20. package/package.json +7 -6
  21. package/src/walletSdk/Auth/AuthHeaderSigner.ts +11 -5
  22. package/src/walletSdk/Auth/WalletSigner.ts +1 -1
  23. package/src/walletSdk/Auth/index.ts +4 -3
  24. package/src/walletSdk/Customer/index.ts +6 -6
  25. package/src/walletSdk/Exceptions/index.ts +27 -0
  26. package/src/walletSdk/Horizon/AccountService.ts +2 -2
  27. package/src/walletSdk/Horizon/Stellar.ts +1 -1
  28. package/src/walletSdk/Horizon/Transaction/TransactionBuilder.ts +31 -19
  29. package/src/walletSdk/Recovery/AccountRecover.ts +1 -1
  30. package/src/walletSdk/Server/index.ts +55 -12
  31. package/src/walletSdk/Types/auth.ts +5 -0
  32. package/src/walletSdk/Types/horizon.ts +16 -5
  33. package/src/walletSdk/Types/sep12.ts +1 -1
  34. package/src/walletSdk/Uri/Sep7Base.ts +10 -9
  35. package/src/walletSdk/Uri/Sep7Tx.ts +1 -1
  36. package/test/auth.test.ts +42 -0
  37. package/test/e2e/reactNative.test.ts +190 -0
  38. package/test/sep7.test.ts +72 -7
  39. package/test/server.test.ts +219 -1
  40. package/test/transaction.test.ts +107 -4
  41. package/test/tsconfig.json +1 -0
  42. package/webpack.config.js +0 -4
@@ -6,8 +6,9 @@
6
6
  import {
7
7
  Transaction,
8
8
  TransactionBuilder,
9
- Keypair,
10
9
  StellarToml,
10
+ WebAuth,
11
+ extractBaseAddress,
11
12
  } from "@stellar/stellar-sdk";
12
13
 
13
14
  import { parseToml } from "../Utils";
@@ -21,18 +22,35 @@ import {
21
22
  } from "../Types";
22
23
  import {
23
24
  ChallengeTxnIncorrectSequenceError,
24
- ChallengeTxnInvalidSignatureError,
25
+ ChallengeTxnClientAccountMismatchError,
26
+ ChallengeValidationFailedError,
25
27
  UnknownAnchorTransactionError,
26
28
  InvalidJsonError,
27
29
  } from "../Exceptions";
28
30
 
29
31
  /**
30
32
  * Helper method for signing a SEP-10 challenge transaction if valid.
33
+ *
34
+ * The challenge is validated with `WebAuth.readChallengeTx` from
35
+ * `@stellar/stellar-sdk` — the same validator used by `Sep10.sign()` — which
36
+ * checks the full SEP-10 structure and binds the challenge to the expected
37
+ * anchor: the transaction source must be the anchor's `SIGNING_KEY`, the
38
+ * `<home domain> auth` operation must match the expected home domain, and any
39
+ * `web_auth_domain` operation must match the host of the anchor's
40
+ * `WEB_AUTH_ENDPOINT` (falling back to `anchorDomain` when the TOML omits it).
41
+ * The client account the challenge was issued for is then checked against
42
+ * `accountKp`, so this helper only signs challenges meant for it. Muxed client
43
+ * accounts are supported: the comparison is made on the underlying `G...`
44
+ * address, since that is the key that signs. The muxed id itself is not pinned
45
+ * — a caller that needs to bind a specific subaccount should check the
46
+ * challenge's client account itself.
47
+ *
31
48
  * @param {SignChallengeTxnParams} params - The Authentication params.
32
49
  * @param {AccountKeypair} params.accountKp - Keypair for the Stellar account signing the transaction.
33
50
  * @param {string} [params.challengeTx] - The challenge transaction given by an anchor for authentication.
34
51
  * @param {string} [params.networkPassphrase] - The network passphrase for the network authenticating on.
35
52
  * @param {string} [params.anchorDomain] - Domain hosting stellar.toml file containing `SIGNING_KEY`.
53
+ * @param {string|string[]} [params.homeDomain] - Expected home domain(s) of the challenge. Defaults to `anchorDomain`.
36
54
  * @returns {Promise<SignChallengeTxnResponse>} The signed transaction.
37
55
  */
38
56
  export const signChallengeTransaction = async ({
@@ -40,30 +58,55 @@ export const signChallengeTransaction = async ({
40
58
  challengeTx,
41
59
  networkPassphrase,
42
60
  anchorDomain,
61
+ homeDomain,
43
62
  }: SignChallengeTxnParams): Promise<SignChallengeTxnResponse> => {
44
- const tx = TransactionBuilder.fromXDR(
63
+ const parsedTx = TransactionBuilder.fromXdr(
45
64
  challengeTx,
46
65
  networkPassphrase,
47
66
  ) as Transaction;
48
67
 
49
- if (parseInt(tx.sequence) !== 0) {
68
+ if (parseInt(parsedTx.sequence) !== 0) {
50
69
  throw new ChallengeTxnIncorrectSequenceError();
51
70
  }
52
71
 
53
72
  const tomlResp = await StellarToml.Resolver.resolve(anchorDomain);
54
- const parsedToml = parseToml(tomlResp);
55
- const anchorKp = Keypair.fromPublicKey(parsedToml.signingKey);
73
+ const { signingKey, webAuthEndpoint } = parseToml(tomlResp);
56
74
 
57
- const isValid =
58
- tx.signatures.length &&
59
- anchorKp.verify(tx.hash(), tx.signatures[0].signature());
60
- if (!isValid) {
61
- throw new ChallengeTxnInvalidSignatureError();
75
+ let tx: Transaction;
76
+ let clientAccountID: string;
77
+ try {
78
+ // Kept inside the try so a malformed WEB_AUTH_ENDPOINT surfaces as a
79
+ // ChallengeValidationFailedError rather than a raw TypeError.
80
+ const webAuthDomain = webAuthEndpoint
81
+ ? new URL(webAuthEndpoint).hostname
82
+ : anchorDomain;
83
+
84
+ ({ tx, clientAccountID } = WebAuth.readChallengeTx(
85
+ challengeTx,
86
+ signingKey,
87
+ networkPassphrase,
88
+ homeDomain ?? anchorDomain,
89
+ webAuthDomain,
90
+ ));
91
+ } catch (e) {
92
+ throw new ChallengeValidationFailedError(
93
+ e instanceof Error ? e : new Error(String(e)),
94
+ );
95
+ }
96
+
97
+ // readChallengeTx returns the operation source verbatim, which is an `M...`
98
+ // address for a muxed client account. accountKp.publicKey is always the
99
+ // underlying `G...` key, so compare base addresses.
100
+ if (extractBaseAddress(clientAccountID) !== accountKp.publicKey) {
101
+ throw new ChallengeTxnClientAccountMismatchError(
102
+ accountKp.publicKey,
103
+ clientAccountID,
104
+ );
62
105
  }
63
106
 
64
107
  accountKp.sign(tx);
65
108
  return {
66
- transaction: tx.toXDR(),
109
+ transaction: tx.toXdr(),
67
110
  networkPassphrase,
68
111
  };
69
112
  };
@@ -94,6 +94,11 @@ export type SignChallengeTxnParams = {
94
94
  challengeTx: string;
95
95
  networkPassphrase: string;
96
96
  anchorDomain: string;
97
+ /**
98
+ * The home domain(s) the challenge is expected to be scoped to. Defaults to
99
+ * `anchorDomain`. Pass an array for anchors that serve multiple home domains.
100
+ */
101
+ homeDomain?: string | string[];
97
102
  };
98
103
 
99
104
  export type SignChallengeTxnResponse = {
@@ -44,10 +44,21 @@ export type PathPayParams = {
44
44
  destinationAddress: string;
45
45
  sendAsset: StellarAssetId;
46
46
  destAsset: StellarAssetId;
47
- sendAmount?: string;
48
- destAmount?: string;
49
- destMin?: string;
50
- sendMax?: string;
51
- };
47
+ } & (
48
+ | {
49
+ // Strict send: spend exactly sendAmount, receive at least destMin
50
+ sendAmount: string;
51
+ destMin: string;
52
+ destAmount?: never;
53
+ sendMax?: never;
54
+ }
55
+ | {
56
+ // Strict receive: deliver exactly destAmount, spend at most sendMax
57
+ destAmount: string;
58
+ sendMax: string;
59
+ sendAmount?: never;
60
+ destMin?: never;
61
+ }
62
+ );
52
63
 
53
64
  export type CommonBuilder = TransactionBuilder | SponsoringBuilder;
@@ -3,7 +3,7 @@ export type CustomerInfoMap = {
3
3
  };
4
4
 
5
5
  export type CustomerBinaryInfoMap = {
6
- [key: string]: Buffer;
6
+ [key: string]: Uint8Array;
7
7
  };
8
8
 
9
9
  export enum Sep12Status {
@@ -1,4 +1,5 @@
1
- import { Keypair, Networks, StellarToml } from "@stellar/stellar-sdk";
1
+ import { Keypair, Networks, StellarToml, xdr } from "@stellar/stellar-sdk";
2
+ import { concatUint8Arrays, stringToUint8Array } from "uint8array-extras";
2
3
  import {
3
4
  Sep7OperationType,
4
5
  URI_MSG_MAX_LENGTH,
@@ -214,7 +215,7 @@ export abstract class Sep7Base {
214
215
  */
215
216
  addSignature(keypair: Keypair): string {
216
217
  const payload = this.createSignaturePayload();
217
- const signature = keypair.sign(payload).toString("base64");
218
+ const signature = xdr.encodeBytes(keypair.sign(payload), "base64");
218
219
  this.setParam("signature", signature);
219
220
  return signature;
220
221
  }
@@ -256,7 +257,7 @@ export abstract class Sep7Base {
256
257
  }
257
258
  const keypair = Keypair.fromPublicKey(signingKey);
258
259
  const payload = this.createSignaturePayload();
259
- return keypair.verify(payload, Buffer.from(signature, "base64"));
260
+ return keypair.verify(payload, xdr.decodeBytes(signature, "base64"));
260
261
  } catch (e) {
261
262
  // if something fails we assume signature verification failed
262
263
  return false;
@@ -300,10 +301,10 @@ export abstract class Sep7Base {
300
301
  *
301
302
  * @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md#request-signing
302
303
  *
303
- * @returns {Buffer} array of bytes to be signed with given keypair on
304
+ * @returns {Uint8Array} array of bytes to be signed with given keypair on
304
305
  * the 'addSignature' method.
305
306
  */
306
- private createSignaturePayload(): Buffer {
307
+ private createSignaturePayload(): Uint8Array {
307
308
  let data = this.toString();
308
309
 
309
310
  const signature = this.signature;
@@ -315,10 +316,10 @@ export abstract class Sep7Base {
315
316
  // The first 35 bytes of the payload are all 0, the 36th byte is 4.
316
317
  // Then we concatenate the URI request with the prefix 'stellar.sep.7 - URI Scheme'
317
318
  // (no delimiter) and convert that to bytes to give use the final payload to be signed.
318
- return Buffer.concat([
319
- Buffer.alloc(35, 0),
320
- Buffer.alloc(1, 4),
321
- Buffer.from(`stellar.sep.7 - URI Scheme${data}`),
319
+ return concatUint8Arrays([
320
+ new Uint8Array(35),
321
+ Uint8Array.of(4),
322
+ stringToUint8Array(`stellar.sep.7 - URI Scheme${data}`),
322
323
  ]);
323
324
  }
324
325
  }
@@ -30,7 +30,7 @@ export class Sep7Tx extends Sep7Base {
30
30
  */
31
31
  static forTransaction(transaction: Transaction): Sep7Tx {
32
32
  const uri = new Sep7Tx();
33
- uri.xdr = transaction.toEnvelope().toXDR().toString("base64");
33
+ uri.xdr = transaction.toEnvelope().toXdr("base64");
34
34
  uri.networkPassphrase = transaction.networkPassphrase as Networks;
35
35
  return uri;
36
36
  }
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
+ });