@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.
- package/CHANGELOG.MD +39 -0
- package/examples/sep10/sep10Server.ts +1 -1
- package/examples/sep24/sep24.ts +1 -1
- package/examples/tsconfig.json +1 -0
- package/jest.e2e.config.js +2 -1
- package/jest.integration.config.js +2 -1
- package/lib/bundle.js +58608 -22154
- package/lib/bundle.js.map +1 -1
- package/lib/bundle_browser.js +49296 -12803
- package/lib/bundle_browser.js.map +1 -1
- package/lib/walletSdk/Customer/index.d.ts +4 -4
- package/lib/walletSdk/Exceptions/index.d.ts +12 -0
- package/lib/walletSdk/Horizon/AccountService.d.ts +2 -3
- package/lib/walletSdk/Horizon/Transaction/TransactionBuilder.d.ts +17 -14
- package/lib/walletSdk/Server/index.d.ts +17 -1
- package/lib/walletSdk/Types/auth.d.ts +5 -0
- package/lib/walletSdk/Types/horizon.d.ts +11 -5
- package/lib/walletSdk/Types/sep12.d.ts +1 -2
- package/lib/walletSdk/Uri/Sep7Base.d.ts +1 -1
- package/package.json +7 -6
- package/src/walletSdk/Auth/AuthHeaderSigner.ts +11 -5
- package/src/walletSdk/Auth/WalletSigner.ts +1 -1
- package/src/walletSdk/Auth/index.ts +4 -3
- package/src/walletSdk/Customer/index.ts +6 -6
- package/src/walletSdk/Exceptions/index.ts +27 -0
- package/src/walletSdk/Horizon/AccountService.ts +2 -2
- package/src/walletSdk/Horizon/Stellar.ts +1 -1
- package/src/walletSdk/Horizon/Transaction/TransactionBuilder.ts +31 -19
- package/src/walletSdk/Recovery/AccountRecover.ts +1 -1
- package/src/walletSdk/Server/index.ts +55 -12
- package/src/walletSdk/Types/auth.ts +5 -0
- package/src/walletSdk/Types/horizon.ts +16 -5
- package/src/walletSdk/Types/sep12.ts +1 -1
- package/src/walletSdk/Uri/Sep7Base.ts +10 -9
- package/src/walletSdk/Uri/Sep7Tx.ts +1 -1
- package/test/auth.test.ts +42 -0
- package/test/e2e/reactNative.test.ts +190 -0
- package/test/sep7.test.ts +72 -7
- package/test/server.test.ts +219 -1
- package/test/transaction.test.ts +107 -4
- package/test/tsconfig.json +1 -0
- 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
|
-
|
|
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
|
|
63
|
+
const parsedTx = TransactionBuilder.fromXdr(
|
|
45
64
|
challengeTx,
|
|
46
65
|
networkPassphrase,
|
|
47
66
|
) as Transaction;
|
|
48
67
|
|
|
49
|
-
if (parseInt(
|
|
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
|
|
55
|
-
const anchorKp = Keypair.fromPublicKey(parsedToml.signingKey);
|
|
73
|
+
const { signingKey, webAuthEndpoint } = parseToml(tomlResp);
|
|
56
74
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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;
|
|
@@ -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)
|
|
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,
|
|
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 {
|
|
304
|
+
* @returns {Uint8Array} array of bytes to be signed with given keypair on
|
|
304
305
|
* the 'addSignature' method.
|
|
305
306
|
*/
|
|
306
|
-
private createSignaturePayload():
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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().
|
|
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 {
|
|
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().
|
|
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().
|
|
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().
|
|
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().
|
|
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
|
+
});
|