diff --git a/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD b/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD index 3a40696b..3a3a530c 100644 --- a/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD +++ b/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD @@ -1,3 +1,13 @@ +# Release notes - Typescript Wallet SDK Key Manager - 6.0.0 + +### BREAKING CHANGES +* `KeyManager.fetchAuthToken` rejects a challenge whose `client_domain` operation has no source account or uses the server account as its source, throwing stellar-sdk's `InvalidChallengeError`. stellar-sdk 17.3's `WebAuth.readChallengeTx` added this check, since SEP-10 requires that source to be the client domain's signing key. 5.1.0 accepted such a challenge + +### Added +* Protocol 30 support: upgraded `@stellar/stellar-sdk` from `17.0.1` to `17.4.0`, which adds CAP-84 muxed contract (`W…`) addresses. The Albedo and Freighter handlers read back a signed transaction that contains one. Before, decoding the signed envelope threw + +**Consumer note:** the key-store surface (`KeyManager`'s `registerEncrypter`, `storeKey`, `loadKey`, `loadAllKeyIds`, `removeKey` and `changePassword`, the keystores and the encrypters) doesn't use stellar-sdk and is unchanged, so upgrading from 5.x needs no code changes for it. Consumers that also depend on `@stellar/stellar-sdk` directly should align their pin to `17.4.0`, since this package bundles that exact version. + # Release notes - Typescript Wallet SDK Key Manager - 5.1.0 ### Changed diff --git a/@stellar/typescript-wallet-sdk-km/package.json b/@stellar/typescript-wallet-sdk-km/package.json index 886cda8b..53e4e4ba 100644 --- a/@stellar/typescript-wallet-sdk-km/package.json +++ b/@stellar/typescript-wallet-sdk-km/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/typescript-wallet-sdk-km", - "version": "5.1.0", + "version": "6.0.0", "engines": { "node": ">=22.12.0" }, @@ -40,7 +40,7 @@ "@stablelib/base64": "^2.0.0", "@stablelib/utf8": "^2.0.0", "@stellar/freighter-api": "6.0.1", - "@stellar/stellar-sdk": "17.0.1", + "@stellar/stellar-sdk": "17.4.0", "@trezor/connect-plugin-stellar": "^9.0.2", "bignumber.js": "^9.1.2", "scrypt-async": "^2.0.1", diff --git a/@stellar/typescript-wallet-sdk-km/test/albedo.test.ts b/@stellar/typescript-wallet-sdk-km/test/albedo.test.ts index c3b62fd3..b14520a6 100644 --- a/@stellar/typescript-wallet-sdk-km/test/albedo.test.ts +++ b/@stellar/typescript-wallet-sdk-km/test/albedo.test.ts @@ -12,6 +12,11 @@ import { import albedo from "@albedo-link/intent"; import { albedoSigningWith } from "./fixtures/albedo"; +import { + MUXED_CONTRACT_MAX_ID, + buildTransferToMuxedContract, + transferRecipient, +} from "./fixtures/muxedContract"; import { albedoHandler } from "../src/Handlers/albedo"; import { Key, KeyType } from "../src/Types"; @@ -154,6 +159,22 @@ describe("albedoHandler", () => { }, ); + it("returns the signed transaction with its muxed contract (W) recipient intact", async () => { + const tx = buildTransferToMuxedContract( + signer.publicKey(), + Networks.TESTNET, + ); + const key = makeAlbedoKey(signer, { network: Networks.TESTNET }); + + const result = await albedoHandler.signTransaction({ + transaction: tx, + key, + }); + + expect(transferRecipient(result)).toBe(MUXED_CONTRACT_MAX_ID); + expect(WebAuth.verifyTxSignedBy(result, signer.publicKey())).toBe(true); + }); + it("refuses a key that holds a private key without opening Albedo", async () => { const tx = buildTestTransaction(signer, Networks.TESTNET); const key = makeAlbedoKey(signer, { privateKey: signer.secret() }); diff --git a/@stellar/typescript-wallet-sdk-km/test/fixtures/muxedContract.ts b/@stellar/typescript-wallet-sdk-km/test/fixtures/muxedContract.ts new file mode 100644 index 00000000..ca28e5cb --- /dev/null +++ b/@stellar/typescript-wallet-sdk-km/test/fixtures/muxedContract.ts @@ -0,0 +1,54 @@ +import { + Account, + Address, + Asset, + BASE_FEE, + Operation, + Transaction, + TransactionBuilder, + nativeToScVal, +} from "@stellar/stellar-sdk"; + +// A Protocol 30 (CAP-84) muxed contract address: SEP-23's test vector +// contract with the largest mux ID, 2^64 - 1. +export const MUXED_CONTRACT_MAX_ID = + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJV7777777777777GMO"; + +// A native XLM Stellar Asset Contract transfer from `source` to +// MUXED_CONTRACT_MAX_ID. +export function buildTransferToMuxedContract( + source: string, + networkPassphrase: string, +): Transaction { + return new TransactionBuilder(new Account(source, "100"), { + fee: BASE_FEE, + networkPassphrase, + }) + .addOperation( + Operation.invokeContractFunction({ + contract: Asset.native().contractId(networkPassphrase), + function: "transfer", + args: [ + new Address(source).toScVal(), + new Address(MUXED_CONTRACT_MAX_ID).toScVal(), + nativeToScVal(BigInt(12345), { type: "i128" }), + ], + }), + ) + .setTimeout(30) + .build(); +} + +// Reads the `to` argument of a transaction built by +// buildTransferToMuxedContract, as a strkey. +export function transferRecipient(tx: Transaction): string { + const { func } = tx.operations[0] as Operation.InvokeHostFunction; + if (func.type !== "hostFunctionTypeInvokeContract") { + throw new Error(`expected a contract invocation, not ${func.type}`); + } + const to = func.invokeContract.args[1]; + if (to.type !== "scvAddress") { + throw new Error(`expected an address, not ${to.type}`); + } + return Address.fromScAddress(to.address).toString(); +} diff --git a/@stellar/typescript-wallet-sdk-km/test/freighter.test.ts b/@stellar/typescript-wallet-sdk-km/test/freighter.test.ts index 3302ee22..72d3b396 100644 --- a/@stellar/typescript-wallet-sdk-km/test/freighter.test.ts +++ b/@stellar/typescript-wallet-sdk-km/test/freighter.test.ts @@ -7,9 +7,15 @@ import { Transaction, Asset, BASE_FEE, + WebAuth, } from "@stellar/stellar-sdk"; import freighterApi from "@stellar/freighter-api"; +import { + MUXED_CONTRACT_MAX_ID, + buildTransferToMuxedContract, + transferRecipient, +} from "./fixtures/muxedContract"; import { freighterHandler } from "../src/Handlers/freighter"; import { Key, KeyType } from "../src/Types"; @@ -88,6 +94,36 @@ describe("freighterHandler", () => { }); }); + it("returns the signed transaction with its muxed contract (W) recipient intact", async () => { + const networkPassphrase = Networks.TESTNET; + const signer = Keypair.random(); + const tx = buildTransferToMuxedContract( + signer.publicKey(), + networkPassphrase, + ); + const key = makeFreighterKey({ publicKey: signer.publicKey() }); + + // Freighter answers with the envelope it signed + const signed = TransactionBuilder.fromXdr( + tx.toXdr(), + networkPassphrase, + ) as Transaction; + signed.sign(signer); + mockSignTransaction.mockResolvedValue({ + signedTxXdr: signed.toXdr(), + signerAddress: signer.publicKey(), + }); + + const result = await freighterHandler.signTransaction({ + transaction: tx, + key, + custom: { networkPassphrase }, + }); + + expect(transferRecipient(result)).toBe(MUXED_CONTRACT_MAX_ID); + expect(WebAuth.verifyTxSignedBy(result, signer.publicKey())).toBe(true); + }); + it("passes custom.address through to freighter", async () => { const networkPassphrase = Networks.TESTNET; const tx = buildTestTransaction(networkPassphrase); diff --git a/@stellar/typescript-wallet-sdk-km/test/keyManager.test.ts b/@stellar/typescript-wallet-sdk-km/test/keyManager.test.ts index b7eacf18..109d9450 100644 --- a/@stellar/typescript-wallet-sdk-km/test/keyManager.test.ts +++ b/@stellar/typescript-wallet-sdk-km/test/keyManager.test.ts @@ -999,6 +999,104 @@ describe("fetchAuthToken", () => { expect(fetchSpy).toHaveBeenCalledTimes(1); }); + // WebAuth.readChallengeTx checks these since stellar-sdk 17.3. SEP-10 + // requires the client_domain operation's source to be the client domain's + // signing key. + test.each([ + ["has no source account", "none"], + ["uses the server account as its source", "server"], + ])( + "Rejects a challenge whose client_domain operation %s", + async (_, clientDomainSource) => { + const authServer = "https://www.stellar.org/auth"; + const password = "very secure password"; + + const keyNetwork = Networks.TESTNET; + + const accountKey = Keypair.random(); + const account = new Account(accountKey.publicKey(), "-1"); + + // set up the manager + const testStore = new MemoryKeyStore(); + const testKeyManager = new KeyManager({ + keyStore: testStore, + }); + + testKeyManager.registerEncrypter(IdentityEncrypter); + + const keypair = Keypair.master(keyNetwork); + + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: keyNetwork, + }) + .addOperation( + Operation.manageData({ + name: `stellar.org auth`, + value: randomBytes(48).toString("base64"), + source: keypair.publicKey(), + }), + ) + .addOperation( + Operation.manageData({ + name: "web_auth_domain", + value: new URL(authServer).hostname, + source: account.accountId(), + }), + ) + .addOperation( + Operation.manageData({ + name: "client_domain", + value: "wallet.example.com", + source: + clientDomainSource === "server" ? account.accountId() : undefined, + }), + ) + .setTimeout(300) + .build(); + + tx.sign(accountKey); + + const fetchSpy = jest.spyOn(global, "fetch").mockResolvedValueOnce( + new Response( + JSON.stringify({ + transaction: tx.toXDR(), + network_passphrase: keyNetwork, + }), + ), + ); + + const keyMetadata = await testKeyManager.storeKey({ + key: { + type: KeyType.plaintextKey, + publicKey: keypair.publicKey(), + privateKey: keypair.secret(), + network: keyNetwork, + }, + password, + encrypterName: "IdentityEncrypter", + }); + + const onChallengeTransactionSignature = jest.fn(); + + const result = testKeyManager.fetchAuthToken({ + id: keyMetadata.id, + password, + authServer, + authServerKey: account.accountId(), + authServerHomeDomains: ["stellar.org"], + clientDomain: "wallet.example.com", + onChallengeTransactionSignature, + }); + await expect(result).rejects.toThrow(WebAuth.InvalidChallengeError); + await expect(result).rejects.toThrow(/client_domain/); + + // The challenge was fetched, but nothing was signed or posted back. + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(onChallengeTransactionSignature).not.toHaveBeenCalled(); + }, + ); + test("Can use a challenge token", async () => { const authServer = "https://www.stellar.org/auth"; const password = "very secure password"; diff --git a/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD b/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD index 44f7a004..7f3c3e3b 100644 --- a/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD +++ b/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD @@ -1,3 +1,14 @@ +# Release notes - Typescript Wallet SDK Soroban - 6.0.0 + +### BREAKING CHANGES +* `scValByType` throws a `TypeError` for a map, at any depth, with two keys that convert to the same JSON key, such as `u32 1` and `symbol "1"`. It used to render the map without one of those entries. The change comes from stellar-sdk 17.3's `scValToNative` + +### Added +* Protocol 30 support: upgraded `@stellar/stellar-sdk` from `17.0.1` to `17.4.0`, which adds CAP-84 muxed contract (`W…`) addresses. `scValByType` renders one as its `W…` address, and `getTokenInvocationArgs` returns it as `to` for a Stellar Asset Contract `transfer` or `mint` to it. Before, a transaction that contained one failed to decode + +### Fixed +* `getInvocationDetails` shows a Stellar Asset Contract's asset with its code as encoded, so an asset whose code is `xlm` shows as `xlm:G…`. It used to show as `XLM:G…`, the same as an asset whose code is `XLM`. The fix comes from stellar-sdk 17.3 + # Release notes - Typescript Wallet SDK Soroban - 5.1.0 * Version bump diff --git a/@stellar/typescript-wallet-sdk-soroban/README.md b/@stellar/typescript-wallet-sdk-soroban/README.md index b2e6b19d..1322f90e 100644 --- a/@stellar/typescript-wallet-sdk-soroban/README.md +++ b/@stellar/typescript-wallet-sdk-soroban/README.md @@ -52,6 +52,9 @@ const args = getTokenInvocationArgs(op); to: "GDUBMXMABE7UOZSGYJ5ONE7UYAEHKK3JOX7HZQGNZ7NYTZPPP4AJ2GQJ", amount: 5 } + + `to` is the recipient as the invocation names it, so it can also be a muxed + account (M...) or, since Protocol 30, a muxed contract (W...) address. */ const formattedAmount = formatTokenAmount("10000123", 3); @@ -77,4 +80,6 @@ const accountAddress = xdr.ScVal.scvAddress( const addressString = scValByType(accountAddress); // converts smart contract complex value into a simple string // addressString = "GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB" +// Every address type renders as its strkey, including Protocol 30 muxed +// contract (W...) addresses. ``` diff --git a/@stellar/typescript-wallet-sdk-soroban/package.json b/@stellar/typescript-wallet-sdk-soroban/package.json index 3020d54b..65dd4467 100644 --- a/@stellar/typescript-wallet-sdk-soroban/package.json +++ b/@stellar/typescript-wallet-sdk-soroban/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/typescript-wallet-sdk-soroban", - "version": "5.1.0", + "version": "6.0.0", "engines": { "node": ">=22.12.0" }, @@ -30,7 +30,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@stellar/stellar-sdk": "17.0.1" + "@stellar/stellar-sdk": "17.4.0" }, "scripts": { "prepare": "husky install", diff --git a/@stellar/typescript-wallet-sdk-soroban/src/Helpers/scValByType.ts b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/scValByType.ts index 09aad81d..322b6880 100644 --- a/@stellar/typescript-wallet-sdk-soroban/src/Helpers/scValByType.ts +++ b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/scValByType.ts @@ -11,6 +11,10 @@ import { Address, scValToNative, xdr } from "@stellar/stellar-sdk"; * @returns the smart contract value converted to a common/simpler * value like string, array, hex string, JSON string, etc. * + * @throws {TypeError} when a map, at any depth, has two keys that convert to + * the same JSON key, such as u32 1 and symbol "1", instead of rendering it + * without one of them. + * * @example * const accountAddress = xdr.ScVal.scvAddress( * new Address("GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB").toScAddress(), @@ -21,8 +25,8 @@ import { Address, scValToNative, xdr } from "@stellar/stellar-sdk"; export const scValByType = (scVal: xdr.ScVal) => { switch (scVal.type) { case "scvAddress": - // Address.fromScAddress covers all five ScAddress variants, so no - // per-variant branching is needed here. + // Address.fromScAddress covers every ScAddress variant, muxed contract + // (W...) addresses included, so no per-variant branching is needed here. return Address.fromScAddress(scVal.address).toString(); case "scvBool": diff --git a/@stellar/typescript-wallet-sdk-soroban/src/Types/index.ts b/@stellar/typescript-wallet-sdk-soroban/src/Types/index.ts index 780cf513..3374ad4d 100644 --- a/@stellar/typescript-wallet-sdk-soroban/src/Types/index.ts +++ b/@stellar/typescript-wallet-sdk-soroban/src/Types/index.ts @@ -8,6 +8,8 @@ export enum SorobanTokenInterface { export type ArgsForTokenInvocation = { from: string; + // The recipient as the invocation names it, so it can also be a muxed + // account (M...) or, since Protocol 30, a muxed contract (W...) address. to: string; amount: bigint | number; }; diff --git a/@stellar/typescript-wallet-sdk-soroban/test/helpers.test.ts b/@stellar/typescript-wallet-sdk-soroban/test/helpers.test.ts index 4cabec71..6bd03586 100644 --- a/@stellar/typescript-wallet-sdk-soroban/test/helpers.test.ts +++ b/@stellar/typescript-wallet-sdk-soroban/test/helpers.test.ts @@ -22,6 +22,16 @@ import { } from "../src"; import { makeInvocation, randomContracts, randomKey } from "./utils"; +// Protocol 30 (CAP-84) muxed contract addresses. SEP-23's test vector is this +// contract with mux ID 0. The other is the same contract with the largest mux +// ID, 2^64 - 1, which a JS number can't hold exactly. +const SEP23_CONTRACT = + "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"; +const SEP23_MUXED_CONTRACT = + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC"; +const MUXED_CONTRACT_MAX_ID = + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJV7777777777777GMO"; + const transactions = { classic: "AAAAAgAAAACCMXQVfkjpO2gAJQzKsUsPfdBCyfrvy7sr8+35cOxOSwAAAGQABqQMAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAACCMXQVfkjpO2gAJQzKsUsPfdBCyfrvy7sr8+35cOxOSwAAAAAAmJaAAAAAAAAAAAFw7E5LAAAAQBu4V+/lttEONNM6KFwdSf5TEEogyEBy0jTOHJKuUzKScpLHyvDJGY+xH9Ri4cIuA7AaB8aL+VdlucCfsNYpKAY=", @@ -29,6 +39,9 @@ const transactions = { "AAAAAgAAAACM6IR9GHiRoVVAO78JJNksy2fKDQNs2jBn8bacsRLcrDucaFsAAAWIAAAAMQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABHkEVdJ+UfDnWpBr/qF582IEoDQ0iW0WPzO9CEUdvvh8AAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAAjOiEfRh4kaFVQDu/CSTZLMtnyg0DbNowZ/G2nLES3KwAAAASAAAAAAAAAADoFl2ACT9HZkbCeuaT9MAIdStpdf58wM3P24nl738AnQAAAAoAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAAAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAACHRyYW5zZmVyAAAAAwAAABIAAAAAAAAAAIzohH0YeJGhVUA7vwkk2SzLZ8oNA2zaMGfxtpyxEtysAAAAEgAAAAAAAAAA6BZdgAk/R2ZGwnrmk/TACHUraXX+fMDNz9uJ5e9/AJ0AAAAKAAAAAAAAAAAAAAAAAAAABQAAAAAAAAABAAAAAAAAAAIAAAAGAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAAFAAAAAEAAAAHa35L+/RxV6EuJOVk78H5rCN+eubXBWtsKrRxeLnnpRAAAAACAAAABgAAAAEeQRV0n5R8OdakGv+oXnzYgSgNDSJbRY/M70IRR2++HwAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAACM6IR9GHiRoVVAO78JJNksy2fKDQNs2jBn8bacsRLcrAAAAAEAAAAGAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAAEAAAAAEAAAACAAAADwAAAAdCYWxhbmNlAAAAABIAAAAAAAAAAOgWXYAJP0dmRsJ65pP0wAh1K2l1/nzAzc/bieXvfwCdAAAAAQBkcwsAACBwAAABKAAAAAAAAB1kAAAAAA==", sorobanMint: "AAAAAgAAAACM6IR9GHiRoVVAO78JJNksy2fKDQNs2jBn8bacsRLcrDucQIQAAAWIAAAAMQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABHkEVdJ+UfDnWpBr/qF582IEoDQ0iW0WPzO9CEUdvvh8AAAAEbWludAAAAAIAAAASAAAAAAAAAADoFl2ACT9HZkbCeuaT9MAIdStpdf58wM3P24nl738AnQAAAAoAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAAAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAABG1pbnQAAAACAAAAEgAAAAAAAAAA6BZdgAk/R2ZGwnrmk/TACHUraXX+fMDNz9uJ5e9/AJ0AAAAKAAAAAAAAAAAAAAAAAAAABQAAAAAAAAABAAAAAAAAAAIAAAAGAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAAFAAAAAEAAAAHa35L+/RxV6EuJOVk78H5rCN+eubXBWtsKrRxeLnnpRAAAAABAAAABgAAAAEeQRV0n5R8OdakGv+oXnzYgSgNDSJbRY/M70IRR2++HwAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAADoFl2ACT9HZkbCeuaT9MAIdStpdf58wM3P24nl738AnQAAAAEAYpBIAAAfrAAAAJQAAAAAAAAdYwAAAAA=", + // Testnet native XLM SAC transfer(GDVEU3..., MUXED_CONTRACT_MAX_ID, 12345) + sorobanTransferToMuxedContract: + "AAAAAgAAAADqSmxj4pxSCr71UHsTLsX5lUd2rr6+e5JCHuppFEbSLAAAAGQAAAAAAAAAZAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAAB15KLcsJwPM/q9+uf9O9NUEpVqLl5/JtFDqLIQrTRzmEAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAA6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iwAAAASAAAABf//////////Pww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAAKAAAAAAAAAAAAAAAAAAAwOQAAAAAAAAAAAAAAAA==", }; describe("getTokenInvocationArgs for different function names", () => { @@ -85,6 +98,22 @@ describe("getTokenInvocationArgs for different function names", () => { expect(args).toBe(null); }); + + it("get token invocation args for a Soroban transfer to a muxed contract (W) address", () => { + const transaction = TransactionBuilder.fromXDR( + transactions.sorobanTransferToMuxedContract, + Networks.TESTNET, + ) as Transaction; + const op = transaction.operations[0] as Operation.InvokeHostFunction; + + expect(getTokenInvocationArgs(op)).toEqual({ + fnName: SorobanTokenInterface.transfer, + contractId: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", + from: "GDVEU3DD4KOFECV66VIHWEZOYX4ZKR3WV27L464SIIPOU2IUI3JCZA57", + to: MUXED_CONTRACT_MAX_ID, + amount: BigInt(12345), + }); + }); }); describe("Token formatting and parsing functions", () => { @@ -300,6 +329,22 @@ describe("scValByType should render expected common types", () => { ); }); + it("should throw, not drop an entry, when two map keys render as the same JSON key", () => { + // u32 1 and symbol "1" both become the JSON key "1" + const xdrMap = xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvU32(1), + val: xdr.ScVal.scvU32(10), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("1"), + val: xdr.ScVal.scvU32(20), + }), + ]); + + expect(() => scValByType(xdrMap)).toThrow(TypeError); + }); + it("should possibly render strings and symbols as strings", () => { const scvString = xdr.ScVal.scvString("any string"); const parsedString = scValByType(scvString); @@ -995,7 +1040,7 @@ describe("scValByType Protocol 28 and address coverage", () => { expect(renderedA).not.toEqual(renderedB); }); - it("renders all five ScAddress variants", () => { + it("renders all six ScAddress variants", () => { const account = Keypair.random().publicKey(); const contract = StrKey.encodeContract(new Uint8Array(32).fill(3)); @@ -1026,6 +1071,16 @@ describe("scValByType Protocol 28 and address coverage", () => { ), "L", ], + [ + xdr.ScAddress.scAddressTypeMuxedContract( + new xdr.MuxedContract({ + id: BigInt(1), + contractId: new xdr.ContractId(StrKey.decodeContract(contract)), + }), + ), + // Muxed contracts encode to a W-address. + "W", + ], ]; for (const [scAddress, expected] of cases) { @@ -1035,6 +1090,23 @@ describe("scValByType Protocol 28 and address coverage", () => { } }); + it.each([ + ["0", SEP23_MUXED_CONTRACT], + ["18446744073709551615", MUXED_CONTRACT_MAX_ID], + ])( + "renders a muxed contract address with mux ID %s as its exact W-address", + (id, expected) => { + const scAddress = xdr.ScAddress.scAddressTypeMuxedContract( + new xdr.MuxedContract({ + id: BigInt(id), + contractId: new xdr.ContractId(StrKey.decodeContract(SEP23_CONTRACT)), + }), + ); + + expect(scValByType(xdr.ScVal.scvAddress(scAddress))).toBe(expected); + }, + ); + it("returns null for an unhandled ScVal type", () => { expect(scValByType(xdr.ScVal.scvVoid())).toBeNull(); }); @@ -1104,4 +1176,17 @@ describe("getTokenInvocationArgs address handling", () => { expect(args.to).toEqual(account); expect(args.amount).toEqual(BigInt(5)); }); + + it("resolves a muxed contract (W) recipient in a mint", () => { + const args = getArgsForTokenInvocation(SorobanTokenInterface.mint, [ + xdr.ScVal.scvAddress(new Address(SEP23_MUXED_CONTRACT).toScAddress()), + xdr.ScVal.scvI128(new xdr.Int128Parts({ hi: BigInt(0), lo: BigInt(5) })), + ]); + + expect(args).toEqual({ + from: "", + to: SEP23_MUXED_CONTRACT, + amount: BigInt(5), + }); + }); }); diff --git a/@stellar/typescript-wallet-sdk/CHANGELOG.MD b/@stellar/typescript-wallet-sdk/CHANGELOG.MD index 38d28109..28fb8263 100644 --- a/@stellar/typescript-wallet-sdk/CHANGELOG.MD +++ b/@stellar/typescript-wallet-sdk/CHANGELOG.MD @@ -1,3 +1,17 @@ +# Release notes - Typescript Wallet SDK - 6.0.0 + +### BREAKING CHANGES +* **React Native: reading a `stellar.toml` needs a `TextDecoder` that supports `{ fatal: true }`.** stellar-sdk 17.3 and later decodes `stellar.toml` with `TextDecoder("utf-8", { fatal: true })`. Where `TextDecoder` can't be built with `fatal`, as with `fast-text-encoding` on Hermes, `Anchor.sep1()` rejects, and with it every call that needs the anchor's `stellar.toml`, and `verifySignature()` on a SEP-7 URI returns `false` for a valid signature. Both worked with such a `TextDecoder` in 5.1.0. Loading the bundle doesn't need one. Each is verified in a Hermes-like sandbox test +* `isValidSep7Uri` and `parseSep7Uri` reject a `pay` URI that has a `memo` when its destination is a contract (`C…`) or muxed contract (`W…`) address, and `Sep7Pay` won't build one: its constructor and its `destination` and `memo` setters throw `Sep7ContractMemoError`. Paying either takes a Soroban transaction, and the network rejects a Soroban transaction that has a memo, so a wallet could only drop the memo. A `C…` destination with a memo used to be accepted +* `Sep10.authenticate` and `Server.signChallengeTransaction` reject a challenge whose `client_domain` operation has no source account or uses the server account as its source, with `ChallengeValidationFailedError`. stellar-sdk 17.3's `WebAuth.readChallengeTx` added this check, since SEP-10 requires that source to be the client domain's signing key. 5.1.0 accepted such a challenge + +### Added +* Protocol 30 support: upgraded `@stellar/stellar-sdk` from `17.0.1` to `17.4.0`, which adds CAP-84 muxed contract (`W…`) addresses. `Stellar.decodeTransaction` and `Sep7Tx.getTransaction` decode a transaction that contains one, and `isValidSep7Uri` accepts a `tx` URI whose transaction contains one. Before, decoding such a transaction threw, so `isValidSep7Uri` returned `false` +* `isValidSep7Uri` and `parseSep7Uri` accept a muxed contract (`W…`) address as a `pay` destination. As with a contract (`C…`) address, a wallet can only pay it with a Stellar Asset Contract `transfer`. `TransactionBuilder`'s `transfer`, `pathPay` and `accountMerge` reject it, as before + +### Changed +* Stricter checks from stellar-sdk 17.1 to 17.4 that reach this SDK: `AccountService.getInfo` and `getHistory` reject with a `TypeError` for an empty account address, which used to query a different Horizon endpoint; `Anchor.sep1()` rejects a `homeDomain` with credentials, a path, a query or a fragment before making a request; and a `stellar.toml` that isn't valid UTF-8 fails to load. See the stellar-sdk changelog for the full list + # Release notes - Typescript Wallet SDK - 5.1.0 ### Added diff --git a/@stellar/typescript-wallet-sdk/package.json b/@stellar/typescript-wallet-sdk/package.json index 433ee578..774d7864 100644 --- a/@stellar/typescript-wallet-sdk/package.json +++ b/@stellar/typescript-wallet-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/typescript-wallet-sdk", - "version": "5.1.0", + "version": "6.0.0", "engines": { "node": ">=22.12.0" }, @@ -49,7 +49,7 @@ "dependencies": { "@stablelib/base64": "^2.0.0", "@stablelib/utf8": "^2.0.0", - "@stellar/stellar-sdk": "17.0.1", + "@stellar/stellar-sdk": "17.4.0", "axios": "^1.4.0", "https-browserify": "^1.0.0", "jws": "^4.0.0", diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts index 0528aa9a..12e23f26 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts @@ -474,6 +474,15 @@ export class Sep7LongMsgError extends Error { } } +export class Sep7ContractMemoError extends Error { + constructor() { + super( + "the 'memo' parameter is not supported for a contract ('C') or muxed contract ('W') 'destination'", + ); + Object.setPrototypeOf(this, Sep7ContractMemoError.prototype); + } +} + export class Sep7UriTypeNotSupportedError extends Error { constructor(type: string) { super( diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/Sep7Pay.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/Sep7Pay.ts index 43ea2a75..69c9f60a 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/Sep7Pay.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/Sep7Pay.ts @@ -1,6 +1,31 @@ -import { MemoType } from "@stellar/stellar-sdk"; +import { MemoType, StrKey } from "@stellar/stellar-sdk"; import { Sep7Base } from "../Uri"; import { Sep7OperationType, WEB_STELLAR_SCHEME } from "../Types"; +import { Sep7ContractMemoError } from "../Exceptions"; + +/** + * Throws if a uri would have a memo with a contract ("C...") or muxed + * contract ("W...") destination, which 'parseSep7Uri' rejects. Paying either + * takes a Soroban transaction, which can't have a memo. + * + * @param {string | undefined} destination the uri 'destination' param. + * @param {string | undefined} memo the uri 'memo' param. + * @throws {Sep7ContractMemoError} if both are set and 'destination' is a + * contract or muxed contract address. + */ +const assertNoContractMemo = ( + destination: string | undefined, + memo: string | undefined, +) => { + if ( + memo && + destination && + (StrKey.isValidContract(destination) || + StrKey.isValidMuxedContract(destination)) + ) { + throw new Sep7ContractMemoError(); + } +}; /** * The Sep-7 'pay' operation represents a request to pay a specific address @@ -27,9 +52,13 @@ export class Sep7Pay extends Sep7Base { * * @constructor * @param {URL | string} [uri] - uri to initialize the Sep7 instance. + * @throws {Sep7ContractMemoError} if 'uri' has a memo and a contract ("C...") + * or muxed contract ("W...") destination. */ constructor(uri?: URL | string) { super(uri ?? new URL(`${WEB_STELLAR_SCHEME}${Sep7OperationType.pay}`)); + + assertNoContractMemo(this.destination, this.memo); } /** @@ -45,6 +74,10 @@ export class Sep7Pay extends Sep7Base { * Gets the destination of the payment request, which should be a valid * Stellar address. * + * A contract ("C...") or muxed contract ("W...") destination can only be + * paid with a Stellar Asset Contract transfer, not a classic payment, and a + * uri with one can't have a memo. + * * @returns {string | undefined} the 'destination' uri param if present. */ get destination(): string | undefined { @@ -58,8 +91,12 @@ export class Sep7Pay extends Sep7Base { * Deletes the uri 'destination' param if set as 'undefined'. * * @param {string | undefined} destination the uri 'destination' param to be set. + * @throws {Sep7ContractMemoError} if 'destination' is a contract ("C...") or + * muxed contract ("W...") address and the uri has a memo. */ set destination(destination: string | undefined) { + assertNoContractMemo(destination, this.memo); + this.setParam("destination", destination); } @@ -142,8 +179,12 @@ export class Sep7Pay extends Sep7Base { * Deletes the uri 'memo' param if set as 'undefined'. * * @param {string | undefined} memo the uri 'memo' param to be set. + * @throws {Sep7ContractMemoError} if 'memo' is set and the uri's destination + * is a contract ("C...") or muxed contract ("W...") address. */ set memo(memo: string | undefined) { + assertNoContractMemo(this.destination, memo); + this.setParam("memo", memo); } diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/sep7Parser.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/sep7Parser.ts index 8e35a831..6ae93054 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/sep7Parser.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/sep7Parser.ts @@ -78,11 +78,15 @@ export const isValidSep7Uri = (uri: string): IsValidSep7UriResult => { } if (type === Sep7OperationType.pay && destination) { - // Checks if it's a valid "G", "M" or "C" Stellar address + const isContract = + StrKey.isValidContract(destination) || + StrKey.isValidMuxedContract(destination); + + // Checks if it's a valid "G", "M", "C" or "W" Stellar address const isValidStellarAddress = StrKey.isValidEd25519PublicKey(destination) || StrKey.isValidMed25519PublicKey(destination) || - StrKey.isValidContract(destination); + isContract; if (!isValidStellarAddress) { return { @@ -91,6 +95,18 @@ export const isValidSep7Uri = (uri: string): IsValidSep7UriResult => { "the provided 'destination' parameter is not a valid Stellar address", }; } + + // A "C" (contract) or "W" (muxed contract) address can only be paid with + // a Stellar Asset Contract transfer. That's a Soroban transaction, which + // can't have a memo, so a wallet could only drop it. A "W" address's mux + // ID already identifies the payee. + if (isContract && url.searchParams.get("memo")) { + return { + result: false, + reason: + "the 'memo' parameter is not supported for a contract ('C') or muxed contract ('W') 'destination'", + }; + } } if (msg?.length > URI_MSG_MAX_LENGTH) { diff --git a/@stellar/typescript-wallet-sdk/test/accountService.test.ts b/@stellar/typescript-wallet-sdk/test/accountService.test.ts index 96d95a22..290e616d 100644 --- a/@stellar/typescript-wallet-sdk/test/accountService.test.ts +++ b/@stellar/typescript-wallet-sdk/test/accountService.test.ts @@ -11,7 +11,8 @@ let accountAddress: string; describe("Horizon", () => { // Creates testing stellar account with USDC trustline - // in case it doesn't exist just yet + // in case it doesn't exist just yet. One friendbot call can take 25 s or + // more on testnet, so this allows 90 s. beforeAll(async () => { const wallet = Wallet.TestNet(); const stellar = wallet.stellar(); @@ -46,7 +47,7 @@ describe("Horizon", () => { await stellar.submitTransaction(addUsdcTx); accountAddress = testingAccountKp.publicKey; - }, 30000); + }, 90000); it("should return stellar account details", async () => { const response = await accountService.getInfo({ accountAddress }); diff --git a/@stellar/typescript-wallet-sdk/test/auth.test.ts b/@stellar/typescript-wallet-sdk/test/auth.test.ts index b0cd769c..e43fc12a 100644 --- a/@stellar/typescript-wallet-sdk/test/auth.test.ts +++ b/@stellar/typescript-wallet-sdk/test/auth.test.ts @@ -726,6 +726,57 @@ describe("Sep10 challenge validation", () => { }); }); + // WebAuth.readChallengeTx checks these since stellar-sdk 17.3. SEP-10 + // requires the client_domain operation's source to be the client domain's + // signing key. + describe("client_domain operation source", () => { + it.each([ + ["has no source account", "none"], + ["uses the server account as its source", "server"], + ])( + "should reject a challenge whose client_domain operation %s", + async (_, clientDomainSource) => { + const serverKeypair = Keypair.random(); + const clientKeypair = Keypair.random(); + const { xdr } = buildChallenge({ + serverKeypair, + clientKeypair, + additionalOps: [ + Operation.manageData({ + name: "client_domain", + value: "wallet.example.com", + source: + clientDomainSource === "server" + ? serverKeypair.publicKey() + : undefined, + }), + ], + }); + const { sep10, postStub } = setupSep10({ + serverSigningKey: serverKeypair.publicKey(), + challengeXdr: xdr, + token: createJwt(clientKeypair), + }); + const signWithDomainAccount = sinon + .stub() + .rejects(new Error("the challenge should be rejected first")); + + const result = sep10.authenticate({ + accountKp: SigningKeypair.fromSecret(clientKeypair.secret()), + clientDomain: "wallet.example.com", + walletSigner: { + signWithClientAccount: ({ transaction }) => transaction, + signWithDomainAccount, + }, + }); + await expect(result).rejects.toThrow(ChallengeValidationFailedError); + await expect(result).rejects.toThrow(/client_domain/); + expect(signWithDomainAccount.notCalled).toBe(true); + expect(postStub.notCalled).toBe(true); + }, + ); + }); + describe("network passphrase mismatch", () => { it("should reject when server returns a different network passphrase", async () => { const { xdr, serverKeypair, clientKeypair } = buildChallenge(); diff --git a/@stellar/typescript-wallet-sdk/test/e2e/reactNative.test.ts b/@stellar/typescript-wallet-sdk/test/e2e/reactNative.test.ts index bf8f1986..e867012d 100644 --- a/@stellar/typescript-wallet-sdk/test/e2e/reactNative.test.ts +++ b/@stellar/typescript-wallet-sdk/test/e2e/reactNative.test.ts @@ -19,9 +19,22 @@ import * as vm from "vm"; */ const BANNED = ["Buffer", "Event", "EventTarget", "process"]; +// Behaves like fast-text-encoding 1.0.6, a TextDecoder that React Native apps +// commonly install on Hermes: its constructor throws for `{ fatal: true }`. +const NonFatalTextDecoder = function ( + label?: string, + options?: TextDecoderOptions, +) { + if (options?.fatal) { + throw new RangeError("the 'fatal' option is unsupported"); + } + return new TextDecoder(label, options); +} as unknown as typeof TextDecoder; + const makeHermesLikeContext = ({ includeBase64 = true, includeTextCodec = true, + textDecoder = TextDecoder, } = {}) => { const sandbox: Record = { console, @@ -56,7 +69,7 @@ const makeHermesLikeContext = ({ // "TypeError: globalThis.TextDecoder is not a constructor" at load — // before any of our own code runs. See the test below that enforces this. sandbox.TextEncoder = TextEncoder; - sandbox.TextDecoder = TextDecoder; + sandbox.TextDecoder = textDecoder; } if (includeBase64) { @@ -126,6 +139,25 @@ describe("React Native (Hermes-like) bundle compatibility", () => { }); } + for (const [name, relativePath] of bundles) { + it(`loads the ${name} bundle with a TextDecoder that can't decode strictly`, () => { + // stellar-sdk 17.3+ builds a strict (`{ fatal: true }`) TextDecoder only + // when it decodes something: a stellar.toml or federation response, or + // an XdrString read with toStringStrict(). Loading a bundle must not + // need one, or an app whose TextDecoder can't decode strictly would + // crash on startup instead of only on those calls. + const bundlePath = path.resolve(__dirname, relativePath); + const context = makeHermesLikeContext({ + textDecoder: NonFatalTextDecoder, + }); + const code = fs.readFileSync(bundlePath, "utf8"); + + expect(() => + vm.runInContext(code, context, { filename: `${name}.js` }), + ).not.toThrow(); + }); + } + it("exercises the core SDK public API in the sandbox", () => { const context = makeHermesLikeContext(); const code = fs.readFileSync( @@ -154,6 +186,85 @@ describe("React Native (Hermes-like) bundle compatibility", () => { expect(result.signature).toHaveLength(88); }); + it("verifies a SEP-7 signature only with a TextDecoder that decodes strictly", async () => { + // Verifying reads the origin domain's stellar.toml, and stellar-sdk 17.3+ + // decodes stellar.toml and federation responses with a strict + // (`{ fatal: true }`) TextDecoder. With a TextDecoder that can't build + // one, verifySignature() fails closed and returns false for a valid + // signature, and Anchor.sep1() rejects. + const verifyWith = (textDecoder: typeof TextDecoder) => { + const context = makeHermesLikeContext({ textDecoder }); + // React Native provides these, and the SDK's stellar.toml fetch uses + // them. The fetch stub serves the toml that the code below writes. + Object.assign(context, { + Headers, + AbortController, + AbortSignal, + fetch: () => Promise.resolve(new Response(context.stellarToml)), + }); + const code = fs.readFileSync( + path.resolve(__dirname, "../../lib/bundle_browser.js"), + "utf8", + ); + vm.runInContext(code, context, { filename: "bundle_browser.js" }); + + return vm.runInContext( + `(() => { + const { Wallet, Sep7Pay } = module.exports; + const kp = Wallet.TestNet().stellar().account().createKeypair(); + globalThis.stellarToml = 'URI_REQUEST_SIGNING_KEY="' + kp.publicKey + '"'; + const uri = Sep7Pay.forDestination(kp.publicKey); + uri.originDomain = "example.com"; + uri.addSignature(kp.keypair); + return uri.verifySignature(); + })()`, + context, + ) as Promise; + }; + + expect(await verifyWith(TextDecoder)).toBe(true); + expect(await verifyWith(NonFatalTextDecoder)).toBe(false); + }); + + it("reads an anchor's stellar.toml only with a TextDecoder that decodes strictly", async () => { + // Every anchor flow starts from Anchor.sep1(), which reads the anchor's + // stellar.toml. With a TextDecoder that can't decode strictly, it rejects. + const sep1With = (textDecoder: typeof TextDecoder) => { + const context = makeHermesLikeContext({ textDecoder }); + // React Native defines `window` as the global object, and + // Wallet.anchor() reads `window.URL` through it. + Object.assign(context, { + window: context, + Headers, + AbortController, + AbortSignal, + fetch: () => + Promise.resolve( + new Response( + 'WEB_AUTH_ENDPOINT="https://example.com/auth"\n[DOCUMENTATION]\nORG_NAME="Example"\n', + ), + ), + }); + const code = fs.readFileSync( + path.resolve(__dirname, "../../lib/bundle_browser.js"), + "utf8", + ); + vm.runInContext(code, context, { filename: "bundle_browser.js" }); + + return vm.runInContext( + `module.exports.Wallet.TestNet().anchor({ homeDomain: "example.com" }).sep1()`, + context, + ) as Promise<{ webAuthEndpoint: string }>; + }; + + expect((await sep1With(TextDecoder)).webAuthEndpoint).toBe( + "https://example.com/auth", + ); + await expect(sep1With(NonFatalTextDecoder)).rejects.toThrow( + "the 'fatal' option is unsupported", + ); + }); + it("requires btoa/atob for base64 paths, while base32/StrKey keypair creation stays pure JS", () => { const context = makeHermesLikeContext({ includeBase64: false }); const code = fs.readFileSync( diff --git a/@stellar/typescript-wallet-sdk/test/fixtures/muxedContract.ts b/@stellar/typescript-wallet-sdk/test/fixtures/muxedContract.ts new file mode 100644 index 00000000..baa37dbf --- /dev/null +++ b/@stellar/typescript-wallet-sdk/test/fixtures/muxedContract.ts @@ -0,0 +1,47 @@ +import { Address, Operation, Transaction } from "@stellar/stellar-sdk"; + +// Protocol 30 (CAP-84) muxed contract addresses: a contract ID plus a uint64 +// mux ID, encoded as a "W..." strkey. + +// SEP-23's test vector: this contract with mux ID 0. +export const SEP23_CONTRACT = + "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"; +export const SEP23_MUXED_CONTRACT = + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC"; + +// The same contract with the largest mux ID, 2^64 - 1, which a JS number +// can't hold exactly. +export const MUXED_CONTRACT_MAX_ID = + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJV7777777777777GMO"; + +// SEP23_MUXED_CONTRACT with its last character changed, so the checksum +// doesn't match. +export const MUXED_CONTRACT_BAD_CHECKSUM = + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWA"; + +export const TRANSFER_SOURCE = + "GDVEU3DD4KOFECV66VIHWEZOYX4ZKR3WV27L464SIIPOU2IUI3JCZA57"; + +// The native XLM Stellar Asset Contract on testnet. +export const TESTNET_NATIVE_SAC = + "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + +// An unsigned testnet transaction with one operation: TESTNET_NATIVE_SAC's +// transfer(TRANSFER_SOURCE, MUXED_CONTRACT_MAX_ID, 12345). Its only Protocol 30 +// part is the `to` argument, an ScAddress of type SC_ADDRESS_TYPE_MUXED_CONTRACT. +export const TRANSFER_TO_MUXED_CONTRACT_XDR = + "AAAAAgAAAADqSmxj4pxSCr71UHsTLsX5lUd2rr6+e5JCHuppFEbSLAAAAGQAAAAAAAAAZAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAAB15KLcsJwPM/q9+uf9O9NUEpVqLl5/JtFDqLIQrTRzmEAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAA6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iwAAAASAAAABf//////////Pww0v5OtDZlx0EzMkPcFURyDiq2XNKSi+w16A/x/6JoAAAAKAAAAAAAAAAAAAAAAAAAwOQAAAAAAAAAAAAAAAA=="; + +// Reads the `to` argument of the transfer in a transaction like +// TRANSFER_TO_MUXED_CONTRACT_XDR, as a strkey. +export const transferRecipient = (tx: Transaction): string => { + const { func } = tx.operations[0] as Operation.InvokeHostFunction; + if (func.type !== "hostFunctionTypeInvokeContract") { + throw new Error(`expected a contract invocation, not ${func.type}`); + } + const to = func.invokeContract.args[1]; + if (to.type !== "scvAddress") { + throw new Error(`expected an address, not ${to.type}`); + } + return Address.fromScAddress(to.address).toString(); +}; diff --git a/@stellar/typescript-wallet-sdk/test/integration/anchorplatform.test.ts b/@stellar/typescript-wallet-sdk/test/integration/anchorplatform.test.ts index 779a08d3..6bbcdad4 100644 --- a/@stellar/typescript-wallet-sdk/test/integration/anchorplatform.test.ts +++ b/@stellar/typescript-wallet-sdk/test/integration/anchorplatform.test.ts @@ -9,6 +9,7 @@ let accountKp; const anchorUrl = "anchor-sep-server-dev.stellar.org"; describe("Anchor Platform Integration Tests", () => { + // One friendbot call can take 25 s or more on testnet, so this allows 90 s. beforeAll(async () => { // Setup wallet = Wallet.TestNet(); @@ -16,7 +17,7 @@ describe("Anchor Platform Integration Tests", () => { anchor = wallet.anchor({ homeDomain: anchorUrl }); accountKp = stellar.account().createKeypair(); await stellar.fundTestnetAccount(accountKp.publicKey); - }, 30000); + }, 90000); it("SEP-10 auth should work", async () => { const auth = await anchor.sep10(); diff --git a/@stellar/typescript-wallet-sdk/test/integration/recovery.test.ts b/@stellar/typescript-wallet-sdk/test/integration/recovery.test.ts index f4276341..92daa0c0 100644 --- a/@stellar/typescript-wallet-sdk/test/integration/recovery.test.ts +++ b/@stellar/typescript-wallet-sdk/test/integration/recovery.test.ts @@ -51,15 +51,15 @@ describe("Recovery Integration Tests", () => { await stellar.server.loadAccount(deviceKp.publicKey); await stellar.server.loadAccount(recoveryKp.publicKey); } catch (e) { - await axios.get( - "https://friendbot.stellar.org/?addr=" + accountKp.publicKey, - ); - await axios.get( - "https://friendbot.stellar.org/?addr=" + deviceKp.publicKey, - ); - await axios.get( - "https://friendbot.stellar.org/?addr=" + recoveryKp.publicKey, - ); + // One friendbot call can take 25 s or more on testnet, so these run in + // parallel and the test allows 120 s. + await Promise.all([ + axios.get("https://friendbot.stellar.org/?addr=" + accountKp.publicKey), + axios.get("https://friendbot.stellar.org/?addr=" + deviceKp.publicKey), + axios.get( + "https://friendbot.stellar.org/?addr=" + recoveryKp.publicKey, + ), + ]); } // Create SEP-30 identities @@ -164,5 +164,5 @@ describe("Recovery Integration Tests", () => { expect(resp.signers.find((obj) => obj.key === newKp.publicKey).weight).toBe( 10, ); - }, 60000); + }, 120000); }); diff --git a/@stellar/typescript-wallet-sdk/test/sep6.test.ts b/@stellar/typescript-wallet-sdk/test/sep6.test.ts index a82c394d..2b31a25f 100644 --- a/@stellar/typescript-wallet-sdk/test/sep6.test.ts +++ b/@stellar/typescript-wallet-sdk/test/sep6.test.ts @@ -7,6 +7,7 @@ let sep6; let accountKp; describe("SEP-6", () => { + // One friendbot call can take 25 s or more on testnet, so this allows 90 s. beforeAll(async () => { wallet = Wallet.TestNet(); anchor = wallet.anchor({ homeDomain: "testanchor.stellar.org" }); @@ -16,7 +17,7 @@ describe("SEP-6", () => { await axios.get( "https://friendbot.stellar.org/?addr=" + accountKp.publicKey, ); - }, 30000); + }, 90000); it("should get anchor info", async () => { const resp = await sep6.info(); diff --git a/@stellar/typescript-wallet-sdk/test/sep7.test.ts b/@stellar/typescript-wallet-sdk/test/sep7.test.ts index 27fa0ecf..3b7ec641 100644 --- a/@stellar/typescript-wallet-sdk/test/sep7.test.ts +++ b/@stellar/typescript-wallet-sdk/test/sep7.test.ts @@ -1,12 +1,17 @@ import { Account, + Address, Asset, BASE_FEE, Keypair, Networks, Operation, StellarToml, + Transaction, TransactionBuilder, + nativeToScVal, + rpc, + xdr, } from "@stellar/stellar-sdk"; import { NativeAssetId, @@ -25,9 +30,19 @@ import { URI_REPLACE_MAX_LENGTH, } from "../src/walletSdk/Types"; import { + Sep7ContractMemoError, Sep7InvalidUriError, Sep7LongMsgError, } from "../src/walletSdk/Exceptions"; +import { + MUXED_CONTRACT_BAD_CHECKSUM, + MUXED_CONTRACT_MAX_ID, + SEP23_CONTRACT, + SEP23_MUXED_CONTRACT, + TESTNET_NATIVE_SAC, + TRANSFER_TO_MUXED_CONTRACT_XDR, + transferRecipient, +} from "./fixtures/muxedContract"; const testKp1 = SigningKeypair.fromSecret( "SBKQDF56C5VY2YQTNQFGY7HM6R3V6QKDUEDXZQUCPQOP2EBZWG2QJ2JL", @@ -700,7 +715,14 @@ describe("sep7Parser", () => { // With Soroban Contract destination expect( isValidSep7Uri( - "web+stellar:pay?destination=CAPECFLUT6KHYOOWUQNP7KC6PTMICKANBURFWRMPZTXUEEKHN67B7UI2&amount=120.1234567&memo=skdjfasf&memo_type=MEMO_TEXT&msg=pay%20me%20with%20lumens", + "web+stellar:pay?destination=CAPECFLUT6KHYOOWUQNP7KC6PTMICKANBURFWRMPZTXUEEKHN67B7UI2&amount=120.1234567&msg=pay%20me%20with%20lumens", + ).result, + ).toBe(true); + + // With Muxed Contract destination + expect( + isValidSep7Uri( + `web+stellar:pay?destination=${SEP23_MUXED_CONTRACT}&amount=120.1234567&msg=pay%20me%20with%20lumens`, ).result, ).toBe(true); }); @@ -857,7 +879,14 @@ describe("sep7Parser", () => { // With Soroban Contract destination expect( parseSep7Uri( - "web+stellar:pay?destination=CAPECFLUT6KHYOOWUQNP7KC6PTMICKANBURFWRMPZTXUEEKHN67B7UI2&amount=120.123&asset_code=USD&asset_issuer=GCRCUE2C5TBNIPYHMEP7NK5RWTT2WBSZ75CMARH7GDOHDDCQH3XANFOB&memo=hasysda987fs&memo_type=MEMO_TEXT&callback=url%3Ahttps%3A%2F%2FsomeSigningService.com%2Fhasysda987fs%3Fasset%3DUSD", + "web+stellar:pay?destination=CAPECFLUT6KHYOOWUQNP7KC6PTMICKANBURFWRMPZTXUEEKHN67B7UI2&amount=120.123&asset_code=USD&asset_issuer=GCRCUE2C5TBNIPYHMEP7NK5RWTT2WBSZ75CMARH7GDOHDDCQH3XANFOB&callback=url%3Ahttps%3A%2F%2FsomeSigningService.com%2Fhasysda987fs%3Fasset%3DUSD", + ), + ).toBeInstanceOf(Sep7Pay); + + // With Muxed Contract destination + expect( + parseSep7Uri( + `web+stellar:pay?destination=${SEP23_MUXED_CONTRACT}&amount=120.123&asset_code=USD&asset_issuer=GCRCUE2C5TBNIPYHMEP7NK5RWTT2WBSZ75CMARH7GDOHDDCQH3XANFOB&callback=url%3Ahttps%3A%2F%2FsomeSigningService.com%2Fhasysda987fs%3Fasset%3DUSD`, ), ).toBeInstanceOf(Sep7Pay); }); @@ -1067,3 +1096,191 @@ describe("SEP-7 byte encoding regressions", () => { expect(uri.getTransaction().toXdr()).toEqual(tx.toXdr()); }); }); + +describe("SEP-7 with muxed contract (W) addresses", () => { + it.each([ + ["mux ID 0", SEP23_MUXED_CONTRACT], + ["the largest mux ID", MUXED_CONTRACT_MAX_ID], + ])( + "parses a 'pay' uri to a W address with %s and keeps the address as given", + (_, destination) => { + const uri = parseSep7Uri( + `web+stellar:pay?destination=${destination}&amount=10&asset_code=USDC&asset_issuer=GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`, + ); + + expect(uri).toBeInstanceOf(Sep7Pay); + expect((uri as Sep7Pay).destination).toBe(destination); + }, + ); + + it("rejects a 'pay' uri to a W address whose checksum doesn't match", () => { + expect( + isValidSep7Uri( + `web+stellar:pay?destination=${MUXED_CONTRACT_BAD_CHECKSUM}&amount=10`, + ), + ).toEqual({ + result: false, + reason: + "the provided 'destination' parameter is not a valid Stellar address", + }); + }); + + it("parses a 'tx' uri whose transaction pays a W address", () => { + const uri = `web+stellar:tx?xdr=${encodeURIComponent( + TRANSFER_TO_MUXED_CONTRACT_XDR, + )}&network_passphrase=${encodeURIComponent(Networks.TESTNET)}`; + + expect(isValidSep7Uri(uri)).toEqual({ result: true }); + + const tx = (parseSep7Uri(uri) as Sep7Tx).getTransaction(); + expect(transferRecipient(tx)).toBe(MUXED_CONTRACT_MAX_ID); + expect(tx.toXdr()).toBe(TRANSFER_TO_MUXED_CONTRACT_XDR); + }); +}); + +// Runs on testnet, which needs Protocol 30 for a W address to go on chain. A +// dapp builds the transfer, has RPC add its Soroban data and passes it to a +// wallet as a 'tx' uri, which the wallet signs and submits. +describe("SEP-7 'tx' uri for a transfer to a W address, on testnet", () => { + it("is signed and submitted by a wallet, and the network keeps the W", async () => { + const stellar = Wallet.TestNet().stellar(); + const sourceKp = stellar.account().createKeypair(); + await stellar.fundTestnetAccount(sourceKp.publicKey); + + const txBuilder = await stellar.transaction({ sourceAddress: sourceKp }); + const transfer = txBuilder + .addOperation( + Operation.invokeContractFunction({ + contract: TESTNET_NATIVE_SAC, + function: "transfer", + args: [ + new Address(sourceKp.publicKey).toScVal(), + new Address(MUXED_CONTRACT_MAX_ID).toScVal(), + nativeToScVal(12345, { type: "i128" }), + ], + }), + ) + .build(); + const prepared = await new rpc.Server( + "https://soroban-testnet.stellar.org", + ).prepareTransaction(transfer); + const uri = Sep7Tx.forTransaction(prepared).toString(); + + expect(isValidSep7Uri(uri)).toEqual({ result: true }); + const tx = (parseSep7Uri(uri) as Sep7Tx).getTransaction(); + expect(transferRecipient(tx)).toBe(MUXED_CONTRACT_MAX_ID); + + sourceKp.sign(tx); + expect(await stellar.submitTransaction(tx)).toBe(true); + + const record = await stellar.server + .transactions() + .transaction(xdr.encodeBytes(tx.hash(), "hex")) + .call(); + const onChain = stellar.decodeTransaction( + record.envelope_xdr, + ) as Transaction; + expect(transferRecipient(onChain)).toBe(MUXED_CONTRACT_MAX_ID); + }, 60000); +}); + +// A C or W address can only be paid with a Stellar Asset Contract transfer, and +// that's a Soroban transaction, which can't carry a memo. +describe("SEP-7 'pay' to a contract (C or W) address with a memo", () => { + it.each([ + ["C", "a MEMO_TEXT", SEP23_CONTRACT, "&memo=skdjfasf&memo_type=MEMO_TEXT"], + ["C", "a MEMO_ID", SEP23_CONTRACT, "&memo=1234&memo_type=MEMO_ID"], + ["C", "an untyped", SEP23_CONTRACT, "&memo=skdjfasf"], + [ + "W", + "a MEMO_TEXT", + SEP23_MUXED_CONTRACT, + "&memo=skdjfasf&memo_type=MEMO_TEXT", + ], + ["W", "a MEMO_ID", SEP23_MUXED_CONTRACT, "&memo=1234&memo_type=MEMO_ID"], + ["W", "an untyped", SEP23_MUXED_CONTRACT, "&memo=skdjfasf"], + ])( + "rejects a 'pay' uri to a %s address that has %s memo", + (_, __, destination, memoParams) => { + const uri = `web+stellar:pay?destination=${destination}&amount=10${memoParams}`; + + expect(isValidSep7Uri(uri)).toEqual({ + result: false, + reason: + "the 'memo' parameter is not supported for a contract ('C') or muxed contract ('W') 'destination'", + }); + expect(() => parseSep7Uri(uri)).toThrow(Sep7InvalidUriError); + }, + ); +}); + +// Sep7Pay shouldn't build a uri that parseSep7Uri rejects. +describe("Sep7Pay with a memo and a contract (C or W) destination", () => { + const ACCOUNT = "GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO"; + const MUXED_ACCOUNT = + "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAABUTGI4"; + + describe.each([ + ["C", SEP23_CONTRACT], + ["W", SEP23_MUXED_CONTRACT], + ])("to a %s address", (_, destination) => { + it("the memo setter throws and leaves the uri unchanged", () => { + const uri = Sep7Pay.forDestination(destination); + + expect(() => { + uri.memo = "skdjfasf"; + }).toThrow(Sep7ContractMemoError); + expect(uri.toString()).toBe(`web+stellar:pay?destination=${destination}`); + }); + + it("the destination setter throws on a uri that has a memo and leaves it unchanged", () => { + const uri = Sep7Pay.forDestination(ACCOUNT); + uri.memo = "skdjfasf"; + + expect(() => { + uri.destination = destination; + }).toThrow(Sep7ContractMemoError); + expect(uri.toString()).toBe( + `web+stellar:pay?destination=${ACCOUNT}&memo=skdjfasf`, + ); + }); + + it("the constructor throws on a uri that has a memo", () => { + expect( + () => + new Sep7Pay( + `web+stellar:pay?destination=${destination}&memo=skdjfasf`, + ), + ).toThrow(Sep7ContractMemoError); + }); + + it("builds a uri that parses once the memo is cleared", () => { + const uri = Sep7Pay.forDestination(ACCOUNT); + uri.memo = "skdjfasf"; + uri.memo = undefined; + uri.destination = destination; + uri.amount = "10"; + + expect(() => { + uri.memo = undefined; + }).not.toThrow(); + expect(uri.toString()).toBe( + `web+stellar:pay?destination=${destination}&amount=10`, + ); + expect(parseSep7Uri(uri.toString())).toBeInstanceOf(Sep7Pay); + }); + }); + + it.each([ + ["G", ACCOUNT], + ["M", MUXED_ACCOUNT], + ])("the memo setter accepts a %s destination", (_, destination) => { + const uri = Sep7Pay.forDestination(destination); + uri.memo = "skdjfasf"; + + expect(uri.toString()).toBe( + `web+stellar:pay?destination=${destination}&memo=skdjfasf`, + ); + expect(parseSep7Uri(uri.toString())).toBeInstanceOf(Sep7Pay); + }); +}); diff --git a/@stellar/typescript-wallet-sdk/test/server.test.ts b/@stellar/typescript-wallet-sdk/test/server.test.ts index bb0f4819..c8e0e243 100644 --- a/@stellar/typescript-wallet-sdk/test/server.test.ts +++ b/@stellar/typescript-wallet-sdk/test/server.test.ts @@ -277,6 +277,45 @@ describe("signChallengeTransaction validation", () => { ).rejects.toThrow(ChallengeValidationFailedError); }); + // WebAuth.readChallengeTx checks these since stellar-sdk 17.3. SEP-10 + // requires the client_domain operation's source to be the client domain's + // signing key. + it.each([ + ["has no source account", "none"], + ["uses the server account as its source", "server"], + ])( + "should reject a challenge whose client_domain operation %s", + async (_, clientDomainSource) => { + const challenge = TransactionBuilder.cloneFrom( + TransactionBuilder.fromXDR( + buildChallenge(anchorAKp, anchorA, accountKp.publicKey), + networkPassphrase, + ) as Transaction, + ) + .addOperation( + Operation.manageData({ + name: "client_domain", + value: "wallet.example.com", + source: + clientDomainSource === "server" + ? anchorAKp.publicKey() + : undefined, + }), + ) + .build(); + challenge.sign(anchorAKp); + + const result = Server.signChallengeTransaction({ + accountKp, + challengeTx: challenge.toXDR(), + networkPassphrase, + anchorDomain: anchorA, + }); + await expect(result).rejects.toThrow(ChallengeValidationFailedError); + await expect(result).rejects.toThrow(/client_domain/); + }, + ); + it("should accept an explicit homeDomain that differs from anchorDomain", async () => { const homeDomain = "wallet-home.example.com"; const challengeTx = buildChallenge( diff --git a/@stellar/typescript-wallet-sdk/test/stellar.test.ts b/@stellar/typescript-wallet-sdk/test/stellar.test.ts index 8b98411d..50f60797 100644 --- a/@stellar/typescript-wallet-sdk/test/stellar.test.ts +++ b/@stellar/typescript-wallet-sdk/test/stellar.test.ts @@ -21,6 +21,11 @@ import { Assets, } from "../src/walletSdk/Asset"; import { TransactionStatus, WithdrawTransaction } from "../src/walletSdk/Types"; +import { + MUXED_CONTRACT_MAX_ID, + TRANSFER_TO_MUXED_CONTRACT_XDR, + transferRecipient, +} from "./fixtures/muxedContract"; import { TransactionSubmitWithFeeIncreaseFailedError, WithdrawalTxMemoError, @@ -496,6 +501,8 @@ let txnSourceKp; let sponsorKp; let newKp; describe("SponsoringBuilder", () => { + // One friendbot call can take 25 s or more on testnet, so this funds both + // accounts in parallel and allows 90 s. beforeAll(async () => { wal = Wallet.TestNet(); stellar = wal.stellar(); @@ -503,13 +510,11 @@ describe("SponsoringBuilder", () => { txnSourceKp = new SigningKeypair(Keypair.random()); sponsorKp = new SigningKeypair(Keypair.random()); newKp = new SigningKeypair(Keypair.random()); - await axios.get( - "https://friendbot.stellar.org/?addr=" + sponsorKp.publicKey, - ); - await axios.get( - "https://friendbot.stellar.org/?addr=" + txnSourceKp.publicKey, - ); - }, 30000); + await Promise.all([ + axios.get("https://friendbot.stellar.org/?addr=" + sponsorKp.publicKey), + axios.get("https://friendbot.stellar.org/?addr=" + txnSourceKp.publicKey), + ]); + }, 90000); it("should sponsor creating an account", async () => { const wal = Wallet.TestNet(); @@ -644,6 +649,8 @@ describe("Asset", () => { }); }); +// These tests wait on testnet, where one friendbot call can take 25 s or more, +// so they fund their accounts in parallel and allow 90 s. describe("Account Modifying", () => { it("should modify account ", async () => { const wallet = Wallet.TestNet(); @@ -651,10 +658,10 @@ describe("Account Modifying", () => { const sourceKp = new SigningKeypair(Keypair.random()); const otherKp = new SigningKeypair(Keypair.random()); - await axios.get( - "https://friendbot.stellar.org/?addr=" + sourceKp.publicKey, - ); - await axios.get("https://friendbot.stellar.org/?addr=" + otherKp.publicKey); + await Promise.all([ + axios.get("https://friendbot.stellar.org/?addr=" + sourceKp.publicKey), + axios.get("https://friendbot.stellar.org/?addr=" + otherKp.publicKey), + ]); // Add account signer let txBuilder = await stellar.transaction({ @@ -718,7 +725,7 @@ describe("Account Modifying", () => { resp = await stellar.server.loadAccount(sourceKp.publicKey); expect(resp.signers[0].weight).toBe(0); - }, 45000); + }, 90000); it("should merge account", async () => { const wallet = Wallet.TestNet(); @@ -727,8 +734,10 @@ describe("Account Modifying", () => { const accountKp = account.createKeypair(); const sourceKp = account.createKeypair(); - await stellar.fundTestnetAccount(accountKp.publicKey); - await stellar.fundTestnetAccount(sourceKp.publicKey); + await Promise.all([ + stellar.fundTestnetAccount(accountKp.publicKey), + stellar.fundTestnetAccount(sourceKp.publicKey), + ]); const txBuilder = await stellar.transaction({ sourceAddress: accountKp, @@ -753,7 +762,7 @@ describe("Account Modifying", () => { expect(found).toBeFalsy(); const accResp = await stellar.server.loadAccount(accountKp.publicKey); expect(parseInt(accResp.balances[0].balance)).toBeGreaterThan(19998); - }, 30000); + }, 90000); }); // Hermetic (no network): stellar-sdk v16 tightened Memo validation, so a @@ -818,3 +827,14 @@ describe("transferWithdrawalTransaction memo validation", () => { ).toThrow(WithdrawalTxMemoError); }); }); + +describe("decodeTransaction with a muxed contract (W) address", () => { + it("decodes a transfer to a W address and re-encodes it unchanged", () => { + const tx = Wallet.TestNet() + .stellar() + .decodeTransaction(TRANSFER_TO_MUXED_CONTRACT_XDR) as Transaction; + + expect(transferRecipient(tx)).toBe(MUXED_CONTRACT_MAX_ID); + expect(tx.toXdr()).toBe(TRANSFER_TO_MUXED_CONTRACT_XDR); + }); +}); diff --git a/@stellar/typescript-wallet-sdk/test/transaction.test.ts b/@stellar/typescript-wallet-sdk/test/transaction.test.ts index f5fb2e19..65e69408 100644 --- a/@stellar/typescript-wallet-sdk/test/transaction.test.ts +++ b/@stellar/typescript-wallet-sdk/test/transaction.test.ts @@ -19,6 +19,7 @@ import { } from "../src"; import { IssuedAssetId, NativeAssetId } from "../src/walletSdk/Asset"; import { PathPayBoundRequiredError } from "../src/walletSdk/Exceptions"; +import { MUXED_CONTRACT_MAX_ID } from "./fixtures/muxedContract"; describe("Muxed Transactions", () => { let wallet: Wallet; @@ -424,3 +425,45 @@ describe("Path Payment bounds", () => { expect(op.destMin).toBe("0.9000000"); }); }); + +// A muxed contract (W) address can only receive a Stellar Asset Contract +// transfer, so no classic operation may take one as its destination. +describe("classic operations to a muxed contract (W) address", () => { + const newBuilder = () => + new TransactionBuilder( + new Config({ + stellarConfiguration: StellarConfiguration.TestNet(), + applicationConfiguration: new ApplicationConfiguration(), + }), + new Account(Keypair.random().publicKey(), "0"), + ); + + it.each([ + [ + "transfer", + (builder: TransactionBuilder) => + builder.transfer(MUXED_CONTRACT_MAX_ID, new NativeAssetId(), "1"), + ], + [ + "pathPay", + (builder: TransactionBuilder) => + builder.pathPay({ + destinationAddress: MUXED_CONTRACT_MAX_ID, + sendAsset: new NativeAssetId(), + destAsset: new IssuedAssetId( + "USDC", + "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + ), + sendAmount: "5", + destMin: "4.9", + }), + ], + [ + "accountMerge", + (builder: TransactionBuilder) => + builder.accountMerge(MUXED_CONTRACT_MAX_ID), + ], + ])("%s refuses a W destination", (_, addOperation) => { + expect(() => addOperation(newBuilder())).toThrow("destination is invalid"); + }); +}); diff --git a/@stellar/typescript-wallet-sdk/test/wallet.test.ts b/@stellar/typescript-wallet-sdk/test/wallet.test.ts index 412c205e..ab625bc1 100644 --- a/@stellar/typescript-wallet-sdk/test/wallet.test.ts +++ b/@stellar/typescript-wallet-sdk/test/wallet.test.ts @@ -2033,6 +2033,7 @@ describe("AuthHeaderSigner", () => { beforeAll(() => { jest.resetAllMocks(); }); + // One friendbot call can take 25 s or more on testnet, so this allows 90 s. it("full sep-10 auth using header token should work", async () => { const wallet = Wallet.TestNet(); const accountKp = wallet.stellar().account().createKeypair(); @@ -2054,7 +2055,7 @@ describe("AuthHeaderSigner", () => { }); expect(authToken).toBeTruthy(); - }, 30000); + }, 90000); it("should match example implementation generated JWT", async () => { const generatedAuthToken = diff --git a/yarn.lock b/yarn.lock index c76de3fd..ef00fddf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2564,17 +2564,17 @@ resolved "https://registry.yarnpkg.com/@stellar/prettier-config/-/prettier-config-1.0.1.tgz#498a66dc13c66859e3787dabdf958233ddbe9253" integrity sha512-w9OPycQp1XGfmHC2VUHe5shpZjNFRlmsRBaK7IHvOvVpglzV2QNJsVFh8RdLREWA0mzF59AWvQbyUCCJLPfdWw== -"@stellar/stellar-sdk@17.0.1": - version "17.0.1" - resolved "https://registry.yarnpkg.com/@stellar/stellar-sdk/-/stellar-sdk-17.0.1.tgz#4ea091bce35a011ed2715f88ad9831a50a828b96" - integrity sha512-fsHHbzJ14N5Ttq5Qpz4AX0ytmPSLCzcy4XC3uIgSQz0cBkEgv0Zb4KE6tWEd3nDQUk/1qP8ZX9cRonIaUXO3Sw== +"@stellar/stellar-sdk@17.4.0": + version "17.4.0" + resolved "https://registry.yarnpkg.com/@stellar/stellar-sdk/-/stellar-sdk-17.4.0.tgz#13d8db3553b906a0ea4778a29c8b676f44b6acec" + integrity sha512-IX6gi62yWQ+6Oz56LsVEEeSViSOWJL3BkttftwVmVsu+62nLTWNVsCmTWEzwgNLT/q5HGP25OnlU+hhy2E4wwA== dependencies: "@exodus/bytes" "^1.15.1" "@noble/ed25519" "^3.1.0" "@noble/hashes" "^2.2.0" "@stellar/js-xdr" "^5.0.0" "@types/json-schema" "^7.0.15" - axios "1.18.0" + axios "1.20.0" bignumber.js "^11.1.4" commander "^14.0.3" eventsource "^4.1.0" @@ -3297,13 +3297,13 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@1.18.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.18.0.tgz#8a7f8854af280fcaae063272df2ed9f3837d2398" - integrity sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw== +axios@1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.20.0.tgz#515513445aa60e71d04b6521ca6210829ccb4786" + integrity sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg== dependencies: follow-redirects "^1.16.0" - form-data "^4.0.5" + form-data "^4.0.6" https-proxy-agent "^5.0.1" proxy-from-env "^2.1.0" @@ -4810,16 +4810,16 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" - integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== +form-data@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827" + integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.12" + hasown "^2.0.4" + mime-types "^2.1.35" forwarded@0.2.0: version "0.2.0" @@ -5131,6 +5131,13 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hasown@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -6590,7 +6597,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==