diff --git a/@shared/api/helpers/soroban.ts b/@shared/api/helpers/soroban.ts index 512b7048e1..180f86af1a 100644 --- a/@shared/api/helpers/soroban.ts +++ b/@shared/api/helpers/soroban.ts @@ -585,7 +585,10 @@ export const getIsTokenSpec = async (contractId: string, serverUrl: string) => { return isTokenSpec(spec); }; -export const isContractId = (contractId: string) => { +export const isContractId = (contractId?: string) => { + if (!contractId) { + return false; + } try { StrKey.decodeContract(contractId); return true; diff --git a/@shared/api/package.json b/@shared/api/package.json index 00aefaab5a..879b8fb6f7 100644 --- a/@shared/api/package.json +++ b/@shared/api/package.json @@ -8,8 +8,8 @@ "@stellar/js-xdr": "4.0.0", "bignumber.js": "9.3.0", "prettier": "3.5.3", - "stellar-sdk": "npm:@stellar/stellar-sdk@15.0.1", - "stellar-sdk-next": "npm:@stellar/stellar-sdk@15.0.1", + "stellar-sdk": "npm:@stellar/stellar-sdk@16.0.0-rc.1", + "stellar-sdk-next": "npm:@stellar/stellar-sdk@16.0.0-rc.1", "typescript": "5.8.3", "webextension-polyfill": "0.12.0" }, diff --git a/@shared/constants/package.json b/@shared/constants/package.json index e810ba2333..9f1f5e0377 100644 --- a/@shared/constants/package.json +++ b/@shared/constants/package.json @@ -3,8 +3,8 @@ "prettier": "../../.prettierrc.yaml", "version": "1.0.0", "dependencies": { - "stellar-sdk": "npm:@stellar/stellar-sdk@15.0.1", - "stellar-sdk-next": "npm:@stellar/stellar-sdk@15.0.1", + "stellar-sdk": "npm:@stellar/stellar-sdk@16.0.0-rc.1", + "stellar-sdk-next": "npm:@stellar/stellar-sdk@16.0.0-rc.1", "typescript": "5.8.3" }, "devDependencies": { diff --git a/@shared/helpers/package.json b/@shared/helpers/package.json index 0afb49195f..1eefcce788 100644 --- a/@shared/helpers/package.json +++ b/@shared/helpers/package.json @@ -4,8 +4,8 @@ "version": "1.0.0", "dependencies": { "bignumber.js": "9.3.0", - "stellar-sdk": "npm:@stellar/stellar-sdk@15.0.1", - "stellar-sdk-next": "npm:@stellar/stellar-sdk@15.0.1", + "stellar-sdk": "npm:@stellar/stellar-sdk@16.0.0-rc.1", + "stellar-sdk-next": "npm:@stellar/stellar-sdk@16.0.0-rc.1", "typescript": "5.8.3" } } diff --git a/@shared/helpers/soroban/server.ts b/@shared/helpers/soroban/server.ts index 5a83a9d538..1a053c1d1a 100644 --- a/@shared/helpers/soroban/server.ts +++ b/@shared/helpers/soroban/server.ts @@ -1,8 +1,5 @@ import { Transaction, - Memo, - MemoType, - Operation, rpc as SorobanRpc, scValToNative, BASE_FEE, @@ -11,7 +8,7 @@ import { NetworkDetails } from "@shared/constants/stellar"; import { getSdk } from "@shared/helpers/stellar"; export const simulateTx = async ( - tx: Transaction, Operation[]>, + tx: Transaction, server: SorobanRpc.Server, ): Promise => { const simulatedTX = await server.simulateTransaction(tx); diff --git a/config/jest/setupTests.tsx b/config/jest/setupTests.tsx index 1149401afd..565b13e4be 100755 --- a/config/jest/setupTests.tsx +++ b/config/jest/setupTests.tsx @@ -16,6 +16,13 @@ global.ResizeObserver = class ResizeObserver { disconnect() {} }; +// stellar-sdk v16 hashes/signs via @noble/hashes v2, which strictly rejects +// anything that is not `instanceof Uint8Array`. `jsdom-global` swaps the global +// Uint8Array for jsdom's, so Node Buffers (which extend Node's Uint8Array) fail +// that check. Restore the Node Uint8Array (the constructor Buffer extends) so +// hashing accepts Buffers in tests. +global.Uint8Array = Object.getPrototypeOf(Buffer.prototype).constructor; + // make a JSDOM thing so we can fuck with mount const jsdom = new JSDOM(""); const { window } = jsdom; diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts index 0f056a3ee1..6ef3c9b46d 100644 --- a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts +++ b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts @@ -27,6 +27,12 @@ const SIGNED_AUTH_ENTRY = const NON_SOROBAN_AUTH_ENTRY = "AAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA=="; +// A CAP-71 (protocol 27) ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS +// preimage on TestNet, bound to account GAAQCAIB… (ed25519 0x01*32) — never the +// e2e test account. Used to exercise the bound-address mismatch block. +const V2_AUTH_ENTRY_WRONG_ADDRESS = + "AAAACs7gMC1ZhE0yvcqRXIID3USzP7t+3BkFHqN6vt8o7NRyAAAAAAAAACoAD0JAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAABAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAIdHJhbnNmZXIAAAAAAAAAAA=="; + const MSG_TO_SIGN = "Hello, World!"; const SIGNED_MSG = "dxdeMTXPabzkvpVyTFFvPyiQ1soAJVf55NLkzgQ1a5HihB0wGi78P6p4Qac3YJa9pOVD9YeKGeUPZVNCM/f8Cg=="; @@ -954,6 +960,45 @@ test("should show network mismatch warning when signing auth entry for wrong net ).toBeVisible(); }); +test("should block signing an auth entry bound to a different account", async ({ + page, + extensionId, + context, +}) => { + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); + // Stay on TestNet so the embedded networkId matches (passes the network + // check) and we reach the bound-address mismatch check. The preimage is + // bound to GAAQCAIB… which is not the test account. + await allowDapp({ page }); + + const pageTwo = await page.context().newPage(); + await pageTwo.waitForLoadState(); + + const popupPromise = page.context().waitForEvent("page"); + await pageTwo.goto( + "https://play.freighter.app/#/extension/playground/signAuthEntry", + ); + await pageTwo.getByRole("textbox").first().fill(V2_AUTH_ENTRY_WRONG_ADDRESS); + await pageTwo + .getByRole("textbox") + .nth(1) + .fill("Test SDF Network ; September 2015"); + await pageTwo + .getByText("Sign Authorization Entry XDR") + .click({ force: true }); + + const popup = await popupPromise; + + await expect( + popup.getByText("Freighter is set to a different account"), + ).toBeVisible(); + await expect( + popup.getByText( + "Signing this authorization is not possible at the moment.", + ), + ).toBeVisible(); +}); + test("should show invalid entry warning when auth entry XDR cannot be parsed", async ({ page, extensionId, diff --git a/extension/package.json b/extension/package.json index 7abf97e428..3c320b0259 100644 --- a/extension/package.json +++ b/extension/package.json @@ -84,8 +84,8 @@ "sonner": "2.0.7", "soroswap-router-sdk": "1.4.6", "stellar-hd-wallet": "1.0.2", - "stellar-sdk": "npm:@stellar/stellar-sdk@15.0.1", - "stellar-sdk-next": "npm:@stellar/stellar-sdk@15.0.1", + "stellar-sdk": "npm:@stellar/stellar-sdk@16.0.0-rc.1", + "stellar-sdk-next": "npm:@stellar/stellar-sdk@16.0.0-rc.1", "svg-url-loader": "8.0.0", "tailwindcss": "4.1.18", "tsconfig-paths-webpack-plugin": "4.2.0", diff --git a/extension/src/helpers/stellar.ts b/extension/src/helpers/stellar.ts index 1a7ad7b297..aea67e39ef 100644 --- a/extension/src/helpers/stellar.ts +++ b/extension/src/helpers/stellar.ts @@ -82,7 +82,7 @@ export const getTransactionInfo = (search: string) => { }; export function isAsset( - value: Asset | { code: string; issuer: string }, + value: Asset | { code: string; issuer?: string }, ): value is Asset { return (value as Asset).getIssuer !== undefined; } diff --git a/extension/src/popup/components/AssetTile/index.tsx b/extension/src/popup/components/AssetTile/index.tsx index f13981f6b0..74a3413a03 100644 --- a/extension/src/popup/components/AssetTile/index.tsx +++ b/extension/src/popup/components/AssetTile/index.tsx @@ -9,7 +9,7 @@ interface AssetTileProps { asset: { code: string; canonical: string; - issuer: string; + issuer?: string; } | null; assetIcon: string | null; balance?: string; diff --git a/extension/src/popup/components/AuthEntry/index.tsx b/extension/src/popup/components/AuthEntry/index.tsx index b88bbc570d..05406500e4 100644 --- a/extension/src/popup/components/AuthEntry/index.tsx +++ b/extension/src/popup/components/AuthEntry/index.tsx @@ -13,11 +13,21 @@ import { truncateString } from "helpers/stellar"; import "./styles.scss"; +export interface AuthEntryDisplay { + invocation: xdr.SorobanAuthorizedInvocation; + /** + * The address whose authorization the entry's credentials represent. + * Present for address credentials (incl. CAP-71 ADDRESS_V2 / + * ADDRESS_WITH_DELEGATES); absent for source-account credentials. + */ + boundAddress?: string; +} + interface AuthEntriesProps { - invocations: xdr.SorobanAuthorizedInvocation[]; + entries: AuthEntryDisplay[]; } -export const AuthEntries = ({ invocations }: AuthEntriesProps) => { +export const AuthEntries = ({ entries }: AuthEntriesProps) => { const { t } = useTranslation(); const [expandedIndex, setExpandedIndex] = useState(null); @@ -25,7 +35,7 @@ export const AuthEntries = ({ invocations }: AuthEntriesProps) => { setExpandedIndex((prev) => (prev === index ? null : index)); }; - const renderAuthEntry = (invocation: xdr.SorobanAuthorizedInvocation) => { + const renderAuthEntry = ({ invocation, boundAddress }: AuthEntryDisplay) => { const details = getInvocationDetails(invocation); const renderDetailTitle = (detail: InvocationArgs) => { @@ -146,6 +156,22 @@ export const AuthEntries = ({ invocations }: AuthEntriesProps) => { return ( <> + {boundAddress && ( +
+ + } + /> +
+ )} {details.map((detail, ind) => (
{ {t("Authorizations")}
- {invocations.map((invocation) => ( - - {renderAuthEntry(invocation)} + {entries.map((entry) => ( + + {renderAuthEntry(entry)} ))} diff --git a/extension/src/popup/components/__tests__/AuthEntry.test.tsx b/extension/src/popup/components/__tests__/AuthEntry.test.tsx index 65bb3bbae6..52745c2fc1 100644 --- a/extension/src/popup/components/__tests__/AuthEntry.test.tsx +++ b/extension/src/popup/components/__tests__/AuthEntry.test.tsx @@ -73,7 +73,7 @@ describe("AuthEntry", () => { }, }} > - + , ); await waitFor(() => screen.getAllByTestId("AuthEntryContainer")); @@ -130,7 +130,7 @@ describe("AuthEntry", () => { }, }} > - + , ); await waitFor(() => screen.getAllByTestId("AuthEntryContainer")); @@ -195,7 +195,7 @@ describe("AuthEntry", () => { }, }} > - + , ); await waitFor(() => screen.getAllByTestId("AuthEntryContainer")); diff --git a/extension/src/popup/components/account/AccountAssets/index.tsx b/extension/src/popup/components/account/AccountAssets/index.tsx index cd34addb85..96ebd00cce 100644 --- a/extension/src/popup/components/account/AccountAssets/index.tsx +++ b/extension/src/popup/components/account/AccountAssets/index.tsx @@ -51,7 +51,7 @@ export const SorobanTokenIcon = ({ noMargin }: { noMargin?: boolean }) => ( interface AssetIconProps { assetIcons: AssetIcons; code: string; - issuerKey: string; + issuerKey?: string; retryAssetIconFetch?: (arg: { key: string; code: string }) => void; isLPShare?: boolean; isSorobanToken?: boolean; @@ -157,7 +157,7 @@ export const AssetIcon = memo( src={isXlm ? StellarLogo : imgSrc} onError={() => { if (retryAssetIconFetch) { - retryAssetIconFetch({ key: issuerKey, code }); + retryAssetIconFetch({ key: issuerKey ?? "", code }); } // we tried to load an image path but it failed, so show the broken image icon here setHasError(true); diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx index a241857dbc..4df5cf6f89 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx @@ -313,7 +313,9 @@ export const ChangeTrustInternal = ({ memo={{ value: memo, type: "text" }} xdr={xdrDefined} operationNames={operations.map( - (op) => OPERATION_TYPES[op.type] || op.type, + (op) => + OPERATION_TYPES[op.type as keyof typeof OPERATION_TYPES] || + op.type, )} /> diff --git a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx index 3d332fef86..56ea4fb625 100644 --- a/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx +++ b/extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx @@ -4,7 +4,6 @@ import { useDispatch, useSelector, useStore } from "react-redux"; import BigNumber from "bignumber.js"; import { captureException } from "@sentry/browser"; import { - Account, Asset, BASE_FEE, extractBaseAddress, @@ -258,7 +257,7 @@ const getBuiltTx = async ( networkDetails.networkUrl, networkDetails.networkPassphrase, ); - const sourceAccount: Account = await server.loadAccount(publicKey); + const sourceAccount = await server.loadAccount(publicKey); try { const operation = getOperation( sourceAsset, diff --git a/extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx b/extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx index f464530b71..0db24ff6b0 100644 --- a/extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx @@ -9,7 +9,6 @@ import { nativeToScVal, Operation, Signer, - SignerKeyOptions, StrKey, xdr, } from "stellar-sdk"; @@ -230,8 +229,8 @@ export const KeyValueLine = ({ operationKey={t("Asset Issuer")} operationValue={ } />{" "} @@ -348,11 +347,7 @@ export const KeyValueClaimants = ({ claimants }: { claimants: Claimant[] }) => { ); }; -export const KeyValueSignerKeyOptions = ({ - signer, -}: { - signer: SignerKeyOptions; -}) => { +export const KeyValueSignerKeyOptions = ({ signer }: { signer: Signer }) => { const { t } = useTranslation(); if ("ed25519PublicKey" in signer) { @@ -368,7 +363,7 @@ export const KeyValueSignerKeyOptions = ({ return ( ); } @@ -377,7 +372,7 @@ export const KeyValueSignerKeyOptions = ({ return ( ); } diff --git a/extension/src/popup/components/signTransaction/Operations/index.tsx b/extension/src/popup/components/signTransaction/Operations/index.tsx index 57d55dde33..3c1c49f04e 100644 --- a/extension/src/popup/components/signTransaction/Operations/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { Icon, IconButton } from "@stellar/design-system"; import { useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; -import { Operation, xdr } from "stellar-sdk"; +import { OperationRecord, Signer, xdr } from "stellar-sdk"; import { FLAG_TYPES, @@ -78,11 +78,11 @@ const DestinationWarning = ({ export const Operations = ({ flaggedKeys, isMemoRequired, - operations = [] as Operation[], + operations = [] as OperationRecord[], }: { flaggedKeys: FlaggedKeys; isMemoRequired: boolean; - operations: Operation[]; + operations: OperationRecord[]; }) => { const { t } = useTranslation(); @@ -96,7 +96,7 @@ export const Operations = ({ "8": "Authorization Clawback Enabled", }; - const RenderOpByType = ({ op }: { op: Operation }) => { + const RenderOpByType = ({ op }: { op: OperationRecord }) => { const networkDetails = useSelector(settingsNetworkDetailsSelector); useEffect(() => { @@ -330,7 +330,11 @@ export const Operations = ({ } = op; return ( <> - {signer && } + {signer && ( + // v16 types the parsed setOptions signer as the builder opts + // type; at runtime it is a parsed Signer (Buffer-backed fields). + + )} {inflationDest && ( ); } - if (type === "revokeAccountSponsorship") { - const _op = op as unknown as Operation.RevokeAccountSponsorship; - const { account } = _op; + if (op.type === "revokeAccountSponsorship") { + const { account } = op; return ( ); } - if (type === "revokeOfferSponsorship") { - const _op = op as unknown as Operation.RevokeOfferSponsorship; - const { seller, offerId } = _op; + if (op.type === "revokeOfferSponsorship") { + const { seller, offerId } = op; return ( <> ); } - if (type === "revokeDataSponsorship") { - const _op = op as unknown as Operation.RevokeDataSponsorship; - const { account, name } = _op; + if (op.type === "revokeDataSponsorship") { + const { account, name } = op; return ( <> ); } - if (type === "revokeClaimableBalanceSponsorship") { - const _op = - op as unknown as Operation.RevokeClaimableBalanceSponsorship; - const { balanceId } = _op; + if (op.type === "revokeClaimableBalanceSponsorship") { + const { balanceId } = op; return ( ); } - if (type === "revokeSignerSponsorship") { - const _op = op as unknown as Operation.RevokeSignerSponsorship; - const { account, signer } = _op; + if (op.type === "revokeSignerSponsorship") { + const { account, signer } = op; return ( <> - + { + const RenderOpArgsByType = ({ op }: { op: OperationRecord }) => { const networkDetails = useSelector(settingsNetworkDetailsSelector); useEffect(() => { @@ -853,7 +847,9 @@ export const Operations = ({ >
- {OPERATION_TYPES[type] || type} + + {OPERATION_TYPES[type as keyof typeof OPERATION_TYPES] || type} +
{sourceVal && ( diff --git a/extension/src/popup/components/swap/SwapAmount/hooks/useSimulateSwapData.tsx b/extension/src/popup/components/swap/SwapAmount/hooks/useSimulateSwapData.tsx index 5ea0d1afcd..9d4aea6f53 100644 --- a/extension/src/popup/components/swap/SwapAmount/hooks/useSimulateSwapData.tsx +++ b/extension/src/popup/components/swap/SwapAmount/hooks/useSimulateSwapData.tsx @@ -2,7 +2,6 @@ import { useReducer } from "react"; import { useDispatch, useSelector } from "react-redux"; import BigNumber from "bignumber.js"; import { - Account, Asset, BASE_FEE, Memo, @@ -45,8 +44,8 @@ const UNKNOWN_ERROR_DISPLAY = export const getSwapErrorMessage = ( error: unknown, - sourceAsset: { issuer: string }, - destAsset: { issuer: string }, + sourceAsset: { issuer?: string }, + destAsset: { issuer?: string }, ): string => { // Surface known error messages first, regardless of asset type const errorStr = @@ -132,7 +131,7 @@ const getBuiltTx = async ( networkDetails.networkUrl, networkDetails.networkPassphrase, ); - const sourceAccount: Account = await server.loadAccount(publicKey); + const sourceAccount = await server.loadAccount(publicKey); const operation = getOperation( sourceAsset, diff --git a/extension/src/popup/helpers/__tests__/parseAuthEntryPreimage.test.ts b/extension/src/popup/helpers/__tests__/parseAuthEntryPreimage.test.ts new file mode 100644 index 0000000000..75273c175b --- /dev/null +++ b/extension/src/popup/helpers/__tests__/parseAuthEntryPreimage.test.ts @@ -0,0 +1,168 @@ +import { Address, hash, xdr } from "stellar-sdk"; + +import { getAuthEntryBoundAddress, parseAuthEntryPreimage } from "../soroban"; + +const TESTNET_PASSPHRASE = "Test SDF Network ; September 2015"; + +// Valid HashIdPreimage of type ENVELOPE_TYPE_SOROBAN_AUTHORIZATION +// (same fixture used by the e2e freighterApiIntegration suite) +const AUTH_ENTRY_TO_SIGN = + "AAAACc7gMC1ZhE0yvcqRXIID3USzP7t+3BkFHqN6vt8o7NRyGVzFh1h1V3oANBPZAAAAAAAAAAGhRTk9qFLakLcWsi5wS6hhHr80ka5WABdo/8hF7QmS3QAAAARzd2FwAAAABAAAABIAAAAB0kc/9lM7RuxEsaiiUFR+T89kG7IOUk1U0cXCIDkTDesAAAASAAAAAZ+9o35h9wEnNl2hiVZHRJxsDoO3altsu023K1kAex/nAAAACgAAAAAAAAAAAAAAAAADDUAAAAAKAAAAAAAAAAAAAAAAAAGGoAAAAAEAAAAAAAAAAdJHP/ZTO0bsRLGoolBUfk/PZBuyDlJNVNHFwiA5Ew3rAAAACHRyYW5zZmVyAAAAAwAAABIAAAAAAAAAAFVmR/NPwhQJzrxxqVrqFZ83Hy9HmP4trSdB/dX7sAZjAAAAEgAAAAGhRTk9qFLakLcWsi5wS6hhHr80ka5WABdo/8hF7QmS3QAAAAoAAAAAAAAAAAAAAAAAAw1AAAAAAA=="; + +// Valid HashIdPreimage of type ENVELOPE_TYPE_OP_ID (6), NOT a Soroban +// authorization (same fixture used by the e2e suite) +const NON_SOROBAN_AUTH_ENTRY = + "AAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA=="; + +// --------------------------------------------------------------------------- +// CAP-71 (protocol 27) fixtures, generated from the js-stellar-sdk +// `modernization` branch XDR definitions. +// +// Bound account = ed25519 0x01*32, delegate = 0x02*32, contract = 0x03*32, +// network = TESTNET, nonce = 42, signatureExpirationLedger = 1000000, +// invocation = transfer() on the contract with no args. +// --------------------------------------------------------------------------- + +// ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS HashIdPreimage +const V2_AUTH_ENTRY_PREIMAGE = + "AAAACs7gMC1ZhE0yvcqRXIID3USzP7t+3BkFHqN6vt8o7NRyAAAAAAAAACoAD0JAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAABAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAIdHJhbnNmZXIAAAAAAAAAAA=="; + +// SorobanAuthorizationEntry with SOROBAN_CREDENTIALS_ADDRESS_V2 credentials +const V2_CREDENTIAL_AUTH_ENTRY = + "AAAAAgAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAKgAPQkAAAAABAAAAAAAAAAEDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAh0cmFuc2ZlcgAAAAAAAAAA"; + +// SorobanAuthorizationEntry with SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES +const WITH_DELEGATES_AUTH_ENTRY = + "AAAAAwAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAKgAPQkAAAAABAAAAAQAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAABAAAAAAAAAAAAAAABAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAIdHJhbnNmZXIAAAAAAAAAAA=="; + +// G-address of the 0x01*32 bound account key +const BOUND_ADDRESS = + "GAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQDZ7H"; + +describe("parseAuthEntryPreimage", () => { + it("parses a Soroban authorization preimage", () => { + const preimage = xdr.HashIdPreimage.fromXDR(AUTH_ENTRY_TO_SIGN, "base64"); + const parsed = parseAuthEntryPreimage(preimage); + + // matches the values reachable through the raw accessor + const sorobanAuth = preimage.sorobanAuthorization(); + expect(parsed.networkId().equals(sorobanAuth.networkId())).toBe(true); + expect(parsed.networkId().length).toBe(32); + expect(parsed.invocation().toXDR("base64")).toEqual( + sorobanAuth.invocation().toXDR("base64"), + ); + + // classic (non address-bound) preimages carry no bound address + expect( + parsed instanceof xdr.HashIdPreimageSorobanAuthorizationWithAddress, + ).toBe(false); + }); + + it("throws for a non-Soroban preimage variant (OP_ID)", () => { + const preimage = xdr.HashIdPreimage.fromXDR( + NON_SOROBAN_AUTH_ENTRY, + "base64", + ); + + expect(() => parseAuthEntryPreimage(preimage)).toThrow( + /unsupported authorization envelope type/, + ); + }); + + it("throws for any other envelope variant", () => { + // Build a contract ID preimage — a valid HashIdPreimage that is not a + // Soroban authorization, guarding the default branch against all + // non-authorization variants. + const preimage = xdr.HashIdPreimage.envelopeTypeContractId( + new xdr.HashIdPreimageContractId({ + networkId: hash(Buffer.from(TESTNET_PASSPHRASE)), + contractIdPreimage: + xdr.ContractIdPreimage.contractIdPreimageFromAddress( + new xdr.ContractIdPreimageFromAddress({ + address: Address.contract(Buffer.alloc(32)).toScAddress(), + salt: Buffer.alloc(32), + }), + ), + }), + ); + + expect(() => parseAuthEntryPreimage(preimage)).toThrow( + /unsupported authorization envelope type/, + ); + }); + + it("parses a CAP-71 address-bound (V2) preimage", () => { + const preimage = xdr.HashIdPreimage.fromXDR( + V2_AUTH_ENTRY_PREIMAGE, + "base64", + ); + const parsed = parseAuthEntryPreimage(preimage); + + expect( + parsed.networkId().equals(hash(Buffer.from(TESTNET_PASSPHRASE))), + ).toBe(true); + expect(parsed.invocation()).toBeDefined(); + + expect( + parsed instanceof xdr.HashIdPreimageSorobanAuthorizationWithAddress, + ).toBe(true); + const withAddress = + parsed as xdr.HashIdPreimageSorobanAuthorizationWithAddress; + expect(Address.fromScAddress(withAddress.address()).toString()).toBe( + BOUND_ADDRESS, + ); + }); +}); + +describe("getAuthEntryBoundAddress", () => { + const legacyAddressEntry = () => + new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress( + new xdr.SorobanAddressCredentials({ + address: new Address(BOUND_ADDRESS).toScAddress(), + nonce: new xdr.Int64(42), + signatureExpirationLedger: 1000000, + signature: xdr.ScVal.scvVoid(), + }), + ), + rootInvocation: new xdr.SorobanAuthorizedInvocation({ + function: + xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( + new xdr.InvokeContractArgs({ + contractAddress: Address.contract( + Buffer.alloc(32, 3), + ).toScAddress(), + functionName: "transfer", + args: [], + }), + ), + subInvocations: [], + }), + }); + + it("returns the address for legacy ADDRESS credentials", () => { + expect(getAuthEntryBoundAddress(legacyAddressEntry())).toBe(BOUND_ADDRESS); + }); + + it("returns undefined for source-account credentials", () => { + const entry = legacyAddressEntry(); + entry.credentials(xdr.SorobanCredentials.sorobanCredentialsSourceAccount()); + expect(getAuthEntryBoundAddress(entry)).toBeUndefined(); + }); + + it("returns the address for CAP-71 ADDRESS_V2 credentials", () => { + const entry = xdr.SorobanAuthorizationEntry.fromXDR( + V2_CREDENTIAL_AUTH_ENTRY, + "base64", + ); + expect(getAuthEntryBoundAddress(entry)).toBe(BOUND_ADDRESS); + }); + + it("returns the top-level address for CAP-71 ADDRESS_WITH_DELEGATES credentials", () => { + const entry = xdr.SorobanAuthorizationEntry.fromXDR( + WITH_DELEGATES_AUTH_ENTRY, + "base64", + ); + expect(getAuthEntryBoundAddress(entry)).toBe(BOUND_ADDRESS); + }); +}); diff --git a/extension/src/popup/helpers/balance.ts b/extension/src/popup/helpers/balance.ts index 83e1a1c6a6..310e2be418 100644 --- a/extension/src/popup/helpers/balance.ts +++ b/extension/src/popup/helpers/balance.ts @@ -33,7 +33,7 @@ export const isNativeBalance = (balance: AssetType): balance is NativeAsset => export const findAssetBalance = ( balances: AssetType[], - asset: Asset | { issuer: string; code: string }, + asset: Asset | { issuer?: string; code: string }, ) => { if (isAsset(asset) && asset.isNative()) { return balances.find( @@ -61,7 +61,7 @@ export const findAssetBalance = ( }; export const getBalanceByAsset = ( - asset: Asset | { issuer: string; code: string }, + asset: Asset | { issuer?: string; code: string }, balances: AssetType[], ) => { const code = asset.code; diff --git a/extension/src/popup/helpers/getManageAssetXDR.ts b/extension/src/popup/helpers/getManageAssetXDR.ts index ed376abbd9..c6c758c869 100644 --- a/extension/src/popup/helpers/getManageAssetXDR.ts +++ b/extension/src/popup/helpers/getManageAssetXDR.ts @@ -26,7 +26,7 @@ export const getManageAssetXDR = async ({ memo?: string; }) => { const changeParams = addTrustline ? {} : { limit: "0" }; - const sourceAccount: StellarSdk.Account = await server.loadAccount(publicKey); + const sourceAccount = await server.loadAccount(publicKey); const Sdk = getSdk(networkDetails.networkPassphrase); diff --git a/extension/src/popup/helpers/hardwareConnect.ts b/extension/src/popup/helpers/hardwareConnect.ts index 91d9867776..5b8ac4bcc3 100644 --- a/extension/src/popup/helpers/hardwareConnect.ts +++ b/extension/src/popup/helpers/hardwareConnect.ts @@ -1,11 +1,4 @@ -import { - FeeBumpTransaction, - Memo, - MemoType, - Operation, - Transaction, - StrKey, -} from "stellar-sdk"; +import { FeeBumpTransaction, Transaction, StrKey } from "stellar-sdk"; import { ConfigurableWalletType, WalletType, @@ -78,7 +71,7 @@ export const getWalletPublicKey: GetWalletPublicKey = { interface HardwareSignParams { bipPath?: string; - tx: Transaction, Operation[]> | FeeBumpTransaction; + tx: Transaction | FeeBumpTransaction; isHashSigningEnabled?: boolean; } diff --git a/extension/src/popup/helpers/soroban.ts b/extension/src/popup/helpers/soroban.ts index 07c39f1824..2b333b6f00 100644 --- a/extension/src/popup/helpers/soroban.ts +++ b/extension/src/popup/helpers/soroban.ts @@ -2,8 +2,6 @@ import BigNumber from "bignumber.js"; import { Address, Asset, - Memo, - MemoType, Operation, StrKey, Transaction, @@ -456,9 +454,11 @@ export const getAttrsFromSorobanHorizonOp = ( const txEnvelope = TransactionBuilder.fromXDR( operation.transaction_attr.envelope_xdr as string, networkDetails.networkPassphrase, - ) as Transaction, Operation.InvokeHostFunction[]>; + ) as Transaction; - const invokeHostFn = txEnvelope.operations[0]; // only one op per tx in Soroban right now + // only one op per tx in Soroban right now; isSorobanOp above guarantees it + // is an invokeHostFunction operation + const invokeHostFn = txEnvelope.operations[0] as Operation.InvokeHostFunction; return getInvocationArgsFromInvokeHostFn(invokeHostFn); }; @@ -474,9 +474,8 @@ export function buildInvocationTree(root: xdr.SorobanAuthorizedInvocation) { const output = {} as InvocationTree; const inner = fn.value(); - switch (fn.switch().value) { - // sorobanAuthorizedFunctionTypeContractFn - case 0: { + switch (fn.switch().name) { + case "sorobanAuthorizedFunctionTypeContractFn": { const _inner = inner as xdr.InvokeContractArgs; output.type = "execute"; output.args = { @@ -487,9 +486,11 @@ export function buildInvocationTree(root: xdr.SorobanAuthorizedInvocation) { break; } - // sorobanAuthorizedFunctionTypeCreateContractHostFn - case 2: - case 1: { + case "sorobanAuthorizedFunctionTypeCreateContractHostFn": + case "sorobanAuthorizedFunctionTypeCreateContractV2HostFn": { + const isCreateV2 = + fn.switch().name === + "sorobanAuthorizedFunctionTypeCreateContractV2HostFn"; const _inner = inner as xdr.CreateContractArgs | xdr.CreateContractArgsV2; output.type = "create"; output.args = {} as { @@ -497,29 +498,21 @@ export function buildInvocationTree(root: xdr.SorobanAuthorizedInvocation) { wasm: any; }; - // If the executable is a WASM, the preimage MUST be an address. If it's a - // token, the preimage MUST be an asset. This is a cheeky way to check - // that, because wasm=0, token=1 and address=0, asset=1 in the XDR switch - // values. - // - // The first part may not be true in V2, but we'd need to update this code - // anyway so it can still be an error. const [exec, preimage] = [ _inner.executable(), _inner.contractIdPreimage(), ]; - if (!!exec.switch().value !== !!preimage.switch().value) { - throw new Error( - `creation function appears invalid: ${JSON.stringify( - inner, - )} (should be wasm+address or token+asset)`, - ); - } - switch (exec.switch().value) { - // contractExecutableWasm - case 0: { - /** @type {xdr.ContractIdPreimageFromAddress} */ + switch (exec.switch().name) { + case "contractExecutableWasm": { + // A WASM executable must be paired with an address preimage. + if (preimage.switch().name !== "contractIdPreimageFromAddress") { + throw new Error( + `creation function appears invalid: ${JSON.stringify( + inner, + )} (should be wasm+address or token+asset)`, + ); + } const details = preimage.fromAddress(); output.args.type = "wasm"; @@ -528,26 +521,32 @@ export function buildInvocationTree(root: xdr.SorobanAuthorizedInvocation) { hash: exec.wasmHash().toString("hex"), address: Address.fromScAddress(details.address()).toString(), }; - // create contract V2 - if (fn.switch().value === 2) { + if (isCreateV2) { const v2Args = _inner as xdr.CreateContractArgsV2; output.args.constructorArgs = v2Args.constructorArgs(); } break; } - // contractExecutableStellarAsset - case 1: + case "contractExecutableStellarAsset": { + // A SAC executable must be paired with an asset preimage. + if (preimage.switch().name !== "contractIdPreimageFromAsset") { + throw new Error( + `creation function appears invalid: ${JSON.stringify( + inner, + )} (should be wasm+address or token+asset)`, + ); + } output.args.type = "sac"; output.args.asset = Asset.fromOperation( preimage.fromAsset(), ).toString(); - // create contract V2 - if (fn.switch().value === 2) { + if (isCreateV2) { const v2Args = _inner as xdr.CreateContractArgsV2; output.args.constructorArgs = v2Args.constructorArgs(); } break; + } default: throw new Error(`unknown creation type: ${JSON.stringify(exec)}`); @@ -648,6 +647,82 @@ export const scValByType = (scVal: xdr.ScVal) => { } }; +/** + * Extracts the Soroban authorization payload struct from a HashIdPreimage a + * dApp has asked us to sign. Both arms expose `networkId()` and + * `invocation()`; the CAP-71 (protocol 27) address-bound arm + * (ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS) additionally exposes + * `address()` — narrow with + * `instanceof xdr.HashIdPreimageSorobanAuthorizationWithAddress`. + * + * @throws if the preimage is not a Soroban authorization envelope + */ +export function parseAuthEntryPreimage( + preimage: xdr.HashIdPreimage, +): + | xdr.HashIdPreimageSorobanAuthorization + | xdr.HashIdPreimageSorobanAuthorizationWithAddress { + switch (preimage.switch()) { + case xdr.EnvelopeType.envelopeTypeSorobanAuthorization(): + return preimage.sorobanAuthorization(); + + // CAP-71 (protocol 27): same payload plus the SCAddress the signature + // is bound to + case xdr.EnvelopeType.envelopeTypeSorobanAuthorizationWithAddress(): + return preimage.sorobanAuthorizationWithAddress(); + + default: + throw new Error( + `unsupported authorization envelope type: ${preimage.switch().name}`, + ); + } +} + +/** + * Extracts the address credentials carried by any address-based Soroban + * credential, regardless of which credential type variant is used. + * + * This unifies access across SOROBAN_CREDENTIALS_ADDRESS, + * SOROBAN_CREDENTIALS_ADDRESS_V2 and + * SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES (CAP-71 / protocol 27). + * + * Mirror of the SDK-internal helper of the same name (js-stellar-sdk + * src/base/auth.ts) — delete this in favor of the upstream export if it is + * ever made public. + * + * @returns the inner address credentials, or null for source-account + * credentials (which carry no address payload) + */ +export function getAddressCredentials( + credentials: xdr.SorobanCredentials, +): xdr.SorobanAddressCredentials | null { + switch (credentials.switch().value) { + case xdr.SorobanCredentialsType.sorobanCredentialsAddress().value: + return credentials.address(); + case xdr.SorobanCredentialsType.sorobanCredentialsAddressV2().value: + return credentials.addressV2(); + case xdr.SorobanCredentialsType.sorobanCredentialsAddressWithDelegates() + .value: + return credentials.addressWithDelegates().addressCredentials(); + default: + return null; + } +} + +/** + * Returns the address whose authorization a SorobanAuthorizationEntry's + * credentials represent (as a display string), or undefined for + * source-account credentials. + */ +export function getAuthEntryBoundAddress( + entry: xdr.SorobanAuthorizationEntry, +): string | undefined { + const addressCredentials = getAddressCredentials(entry.credentials()); + return addressCredentials + ? Address.fromScAddress(addressCredentials.address()).toString() + : undefined; +} + export function getInvocationDetails( invocation: xdr.SorobanAuthorizedInvocation, ) { @@ -697,9 +772,8 @@ export function getInvocationArgs( ): InvocationArgs | undefined { const fn = invocation.function(); - switch (fn.switch().value) { - // sorobanAuthorizedFunctionTypeContractFn - case 0: { + switch (fn.switch().name) { + case "sorobanAuthorizedFunctionTypeContractFn": { const _invocation = fn.contractFn(); const contractId = addressToString(_invocation.contractAddress()); const fnName = _invocation.functionName().toString(); @@ -707,22 +781,21 @@ export function getInvocationArgs( return { fnName, contractId, args, type: "invoke" }; } - // sorobanAuthorizedFunctionTypeCreateContractV2HostFn - // sorobanAuthorizedFunctionTypeCreateContractHostFn - case 2: - case 1: { - const _invocation = - fn.switch().value === 2 - ? fn.createContractV2HostFn() - : fn.createContractHostFn(); + case "sorobanAuthorizedFunctionTypeCreateContractHostFn": + case "sorobanAuthorizedFunctionTypeCreateContractV2HostFn": { + const isCreateV2 = + fn.switch().name === + "sorobanAuthorizedFunctionTypeCreateContractV2HostFn"; + const _invocation = isCreateV2 + ? fn.createContractV2HostFn() + : fn.createContractHostFn(); const [exec, preimage] = [ _invocation.executable(), _invocation.contractIdPreimage(), ]; - switch (exec.switch().value) { - // contractExecutableWasm - case 0: { + switch (exec.switch().name) { + case "contractExecutableWasm": { const details = preimage.fromAddress(); const contractDetails = { @@ -732,7 +805,7 @@ export function getInvocationArgs( address: Address.fromScAddress(details.address()).toString(), } as FnArgsCreateWasm; - if (fn.switch().value === 2) { + if (isCreateV2) { contractDetails.args = ( _invocation as xdr.CreateContractArgsV2 ).constructorArgs(); @@ -741,14 +814,13 @@ export function getInvocationArgs( return contractDetails; } - // contractExecutableStellarAsset - case 1: { + case "contractExecutableStellarAsset": { const sacDetails = { type: "sac", asset: Asset.fromOperation(preimage.fromAsset()).toString(), } as FnArgsCreateSac; - if (fn.switch().value === 2) { + if (isCreateV2) { sacDetails.args = ( _invocation as xdr.CreateContractArgsV2 ).constructorArgs(); diff --git a/extension/src/popup/helpers/useChangeTrustline.ts b/extension/src/popup/helpers/useChangeTrustline.ts index e7946a386e..3056a25d74 100644 --- a/extension/src/popup/helpers/useChangeTrustline.ts +++ b/extension/src/popup/helpers/useChangeTrustline.ts @@ -49,11 +49,6 @@ export const useChangeTrustline = ({ const isHardwareWallet = !!walletType; - const server = stellarSdkServer( - networkDetails.networkUrl, - networkDetails.networkPassphrase, - ); - const { fetchData: fetchFees } = useNetworkFees(); const canonicalAsset = getCanonicalFromAsset(assetCode, assetIssuer); @@ -101,6 +96,13 @@ export const useChangeTrustline = ({ successfulCallback?: () => Promise, ) => { setAssetSubmitting?.(canonicalAsset); + // Build the server here (on submit) rather than during render: the dApp + // popup mounts before network details hydrate, so constructing it at render + // time can hit an empty networkUrl (v16's Server throws on that). + const server = stellarSdkServer( + networkDetails.networkUrl, + networkDetails.networkPassphrase, + ); const fees = await fetchFees(); const transactionXDR: string = await getManageAssetXDR({ publicKey, diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index 9dad7219f6..52339ec4bb 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -76,6 +76,7 @@ "At the end of this process, Freighter will only display accounts related to the new backup phrase.": "At the end of this process, Freighter will only display accounts related to the new backup phrase.", "Authorizations": "Authorizations", "Authorize": "Authorize", + "Authorized address": "Authorized address", "available": "available", "Back": "Back", "Balance": "Balance", @@ -248,6 +249,7 @@ "Freighter does not endorse any listed items.": "Freighter does not endorse any listed items.", "Freighter is a non-custodial wallet for the Stellar blockchain": "Freighter is a non-custodial wallet for the Stellar blockchain", "Freighter is set to": "Freighter is set to", + "Freighter is set to a different account": "Freighter is set to a different account", "Freighter logo": "Freighter logo", "Freighter provides access to third-party dApps, protocols, and tokens for informational purposes only.": "Freighter provides access to third-party dApps, protocols, and tokens for informational purposes only.", "Freighter uses asset lists to check assets you interact with.": "Freighter uses asset lists to check assets you interact with.", @@ -581,7 +583,7 @@ "Tags": "Tags", "Terms of Service": "Terms of Service", "Terms of Use": "Terms of Use", - "The authorization entry is for": "The authorization entry is for", + "The authorization entry is for {{network}}.": "The authorization entry is for {{network}}.", "The authorization entry is for a different network than the one you are connected to.": "The authorization entry is for a different network than the one you are connected to.", "The authorization entry is malformed or contains invalid data.": "The authorization entry is malformed or contains invalid data.", "The authorization entry XDR could not be parsed.": "The authorization entry XDR could not be parsed.", @@ -612,6 +614,7 @@ "This asset was flagged as spam": "This asset was flagged as spam", "This asset was flagged as suspicious": "This asset was flagged as suspicious", "This asset was flagged as suspicious (override active)": "This asset was flagged as suspicious (override active)", + "This authorization is for {{address}}.": "This authorization is for {{address}}.", "This can be used to sign arbitrary transaction hashes without having to decode them first.": "This can be used to sign arbitrary transaction hashes without having to decode them first.", "This collectible is hidden": "This collectible is hidden", "This is not a valid contract id.": "This is not a valid contract id.", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 90dd818068..db2c569c1c 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -76,17 +76,18 @@ "At the end of this process, Freighter will only display accounts related to the new backup phrase.": "No final deste processo, o Freighter exibirá apenas contas relacionadas à nova frase de backup.", "Authorizations": "Autorizações", "Authorize": "Autorizar", + "Authorized address": "Endereço autorizado", "available": "disponível", "Back": "Voltar", "Balance": "Saldo", "Balance ID": "ID do Saldo", - "Before we start with migration, please read": "Antes de começarmos com a migration, por favor leia", + "Before we start with migration, please read": "Antes de começarmos com a migração, por favor leia", "Blockaid": "Blockaid", "Blockaid Response Override": "Substituição de Resposta do Blockaid", "Blockaid unfunded destination": "Esta é uma nova conta e precisa de 1 XLM para começar. Qualquer transação para enviar não-XLM para uma conta não financiada falhará.", "Blockaid unfunded destination native": "Esta é uma nova conta e precisa de pelo menos 1 XLM para ser criada. Enviar menos de 1 XLM para criá-la falhará.", "Bump To": "Bump Para", - "Buy Amount": "Amount de Compra", + "Buy Amount": "Quantia de Compra", "Buy with Coinbase": "Comprar com Coinbase", "Buy XLM with Coinbase": "Comprar XLM com Coinbase", "Buying": "Comprando", @@ -213,9 +214,9 @@ "Failed to fetch your account data.": "Falha ao buscar os dados da sua conta.", "Failed to fetch your transaction details": "Falha ao buscar os detalhes da sua transação", "Failed to fetch your wallets.": "Falha ao buscar suas carteiras.", - "Failed to load assets.": "Failed to load assets.", - "Failed to load send data.": "Failed to load send data.", - "Failed to load swap data.": "Failed to load swap data.", + "Failed to load assets.": "Falha ao carregar ativos.", + "Failed to load send data.": "Falha ao carregar dados de envio.", + "Failed to load swap data.": "Falha ao carregar dados de troca.", "Failed to resolve federated address": "Falha ao resolver endereço federado.", "failed to sign transaction": "falha ao assinar transação", "failed to simulate token transfer": "falha ao simular transferência de token", @@ -248,6 +249,7 @@ "Freighter does not endorse any listed items.": "O Freighter não endossa nenhum item listado.", "Freighter is a non-custodial wallet for the Stellar blockchain": "O Freighter é uma carteira não-custodial para o blockchain Stellar", "Freighter is set to": "O Freighter está configurado para", + "Freighter is set to a different account": "O Freighter está configurado para uma conta diferente", "Freighter logo": "Logo Freighter", "Freighter provides access to third-party dApps, protocols, and tokens for informational purposes only.": "O Freighter fornece acesso a dApps, protocolos e tokens de terceiros apenas para fins informativos.", "Freighter uses asset lists to check assets you interact with.": "O Freighter usa listas de ativos para verificar os ativos com os quais você interage.", @@ -283,7 +285,7 @@ "I already have a wallet": "Já tenho uma carteira", "I have read and agree to": "Li e concordo com", "I understand, continue": "Entendo, continuar", - "I understand, start migration": "Entendo, iniciar migration", + "I understand, start migration": "Entendo, iniciar migração", "I’m aware Freighter can’t recover the imported secret key": "Estou ciente de que o Freighter não pode recuperar a chave secreta importada", "I’ve saved my phrase somewhere safe": "Salvei minha frase em um local seguro", "icon add": "ícone adicionar", @@ -307,8 +309,8 @@ "Inclusion Fee": "Taxa de Inclusão", "Inflation Destination": "Destino de Inflação", "Insufficient Balance": "Saldo Insuficiente", - "Insufficient Fee": "Fee Insuficiente", - "INSUFFICIENT FUNDS FOR FEE": "FUNDOS INSUFICIENTES PARA FEE", + "Insufficient Fee": "Taxa Insuficiente", + "INSUFFICIENT FUNDS FOR FEE": "FUNDOS INSUFICIENTES PARA TAXA", "Introducing Freighter Mobile": "Apresentando Freighter Mobile", "Invalid address": "Endereço inválido", "Invalid Authorization Entry": "Entrada de autorização inválida", @@ -327,7 +329,7 @@ "Learn more about account reserves": "Saiba mais sobre reservas de conta", "Learn more about assets domains": "Saiba mais sobre domínios de ativos", "Learn more about conversion rates": "Saiba mais sobre taxas de conversão", - "Learn more about fees": "Saiba mais sobre fees", + "Learn more about fees": "Saiba mais sobre taxas", "Learn more about transaction fees": "Saiba mais sobre taxas de transação", "Learn more about trustlines": "Saiba mais sobre trustlines", "Learn more about using Ledger": "Saiba mais sobre como usar o Ledger", @@ -353,10 +355,10 @@ "Manage tokens": "Gerenciar tokens", "Master Weight": "Peso Mestre", "Max": "Máx", - "Max Amount A": "Amount Máximo A", - "Max Amount B": "Amount Máximo B", + "Max Amount A": "Quantia Máxima A", + "Max Amount B": "Quantia Máxima B", "max of 24 characters allowed": "máximo de 24 caracteres permitidos", - "Max Price": "Price Máximo", + "Max Price": "Preço Máximo", "Medium": "Média", "Medium Threshold": "Limite Médio", "Memo": "Memo", @@ -372,10 +374,10 @@ "Merging is optional and will allow you to send your current account’s funding lumens to the new accounts.": "A mesclagem é opcional e permitirá que você envie os lumens de financiamento da sua conta atual para as novas contas.", "Migrated": "Migrado", "Migrating...": "Migrando...", - "Migration complete": "Migration concluída", - "Min Amount A": "Amount Mínimo A", - "Min Amount B": "Amount Mínimo B", - "Min Price": "Price Mínimo", + "Migration complete": "Migração concluída", + "Min Amount A": "Quantia Mínima A", + "Min Amount B": "Quantia Mínima B", + "Min Price": "Preço Mínimo", "Minimum XLM needed": "XLM mínimo necessário", "Minted": "Cunhado", "Multiple assets": "Múltiplos ativos", @@ -581,13 +583,13 @@ "Tags": "Categorias", "Terms of Service": "Termos de Serviço", "Terms of Use": "Termos de Uso", - "The authorization entry is for": "A entrada de autorização é para", + "The authorization entry is for {{network}}.": "A entrada de autorização é para {{network}}.", "The authorization entry is for a different network than the one you are connected to.": "A entrada de autorização é para uma rede diferente da que você está conectado.", "The authorization entry is malformed or contains invalid data.": "A entrada de autorização está malformada ou contém dados inválidos.", "The authorization entry XDR could not be parsed.": "O XDR da entrada de autorização não pôde ser interpretado.", "The destination account does not accept the asset you’re sending": "A conta de destino não aceita o ativo que você está enviando", "The destination account doesn't exist": "A conta de destino não existe", - "The destination account doesn’t exist": "The destination account doesn’t exist", + "The destination account doesn’t exist": "A conta de destino não existe", "The destination account must opt to accept this asset before receiving it.": "A conta de destino deve optar por aceitar este ativo antes de recebê-lo.", "The requester expects you to sign this message on": "O solicitante espera que você assine esta mensagem em", "The secret phrase you entered is incorrect.": "A frase secreta que você inseriu está incorreta.", @@ -612,6 +614,7 @@ "This asset was flagged as spam": "Este ativo foi marcado como spam", "This asset was flagged as suspicious": "Este ativo foi marcado como suspeito", "This asset was flagged as suspicious (override active)": "Este ativo foi sinalizado como suspeito (substituição ativa)", + "This authorization is for {{address}}.": "Esta autorização é para {{address}}.", "This can be used to sign arbitrary transaction hashes without having to decode them first.": "Isso pode ser usado para assinar hashes de transação arbitrários sem precisar decodificá-los primeiro.", "This collectible is hidden": "Este colecionável está oculto", "This is not a valid contract id.": "Este não é um ID de contrato válido.", @@ -752,16 +755,16 @@ "Your account balances could not be fetched at this time.": "Os saldos da sua conta não puderam ser buscados neste momento.", "Your account data could not be fetched at this time.": "Os dados da sua conta não puderam ser buscados neste momento.", "Your assets": "Seus ativos", - "Your assets could not be fetched at this time.": "Your assets could not be fetched at this time.", + "Your assets could not be fetched at this time.": "Seus ativos não puderam ser buscados neste momento.", "Your available XLM balance is not enough to pay for the transaction fee.": "Seu saldo XLM disponível não é suficiente para pagar a taxa de transação.", "Your gateway to the Stellar ecosystem. Browse and connect to decentralized applications built on Stellar.": "Sua porta de entrada para o ecossistema Stellar. Navegue e conecte-se a aplicações descentralizadas construídas na Stellar.", "Your recovery phrase": "Sua frase de recuperação", "Your Recovery Phrase": "Sua Frase de Recuperação", "Your recovery phrase gives you access to your account and is the only way to access it in a new browser.": "Sua frase de recuperação lhe dá acesso à sua conta e é a única maneira de acessá-la em um novo navegador.", "Your recovery phrase gives you full access to your wallets and funds": "Sua frase de recuperação lhe dá acesso total às suas carteiras e fundos", - "Your send data could not be fetched at this time.": "Your send data could not be fetched at this time.", + "Your send data could not be fetched at this time.": "Seus dados de envio não puderam ser buscados neste momento.", "Your Stellar secret key": "Sua Stellar secret key", - "Your swap data could not be fetched at this time.": "Your swap data could not be fetched at this time.", + "Your swap data could not be fetched at this time.": "Seus dados de troca não puderam ser buscados neste momento.", "Your Tokens": "Seus Tokens", "Your wallets could not be fetched at this time.": "Suas carteiras não puderam ser buscadas neste momento." } diff --git a/extension/src/popup/views/SignAuthEntry/index.tsx b/extension/src/popup/views/SignAuthEntry/index.tsx index feeac3d5fc..c2c0651744 100644 --- a/extension/src/popup/views/SignAuthEntry/index.tsx +++ b/extension/src/popup/views/SignAuthEntry/index.tsx @@ -3,7 +3,7 @@ import { Button, Icon } from "@stellar/design-system"; import { Navigate, useLocation } from "react-router-dom"; import { useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; -import { hash, xdr } from "stellar-sdk"; +import { Address, hash, xdr } from "stellar-sdk"; import { PASSPHRASE_TO_NETWORK_NAME } from "@shared/constants/stellar"; import { HardwareSign } from "popup/components/hardwareConnect/HardwareSign"; @@ -40,6 +40,8 @@ import { reRouteOnboarding } from "popup/helpers/route"; import { KeyIdenticon } from "popup/components/identicons/KeyIdenticon"; import { getSiteFavicon } from "popup/helpers/getSiteFavicon"; import { AuthEntries } from "popup/components/AuthEntry"; +import { parseAuthEntryPreimage } from "popup/helpers/soroban"; +import { truncateString } from "helpers/stellar"; import { useMarkQueueActive } from "popup/helpers/useMarkQueueActive"; import "./styles.scss"; @@ -142,9 +144,9 @@ export const SignAuthEntry = () => { // Cryptographically validate the networkId embedded in the XDR against the // wallet's active network. This is stronger than trusting the dApp-supplied // networkPassphrase string (which may be absent or spoofed). - let sorobanAuth: ReturnType; + let sorobanAuth: ReturnType; try { - sorobanAuth = preimage.sorobanAuthorization(); + sorobanAuth = parseAuthEntryPreimage(preimage); const embeddedNetworkId = sorobanAuth.networkId(); const entryNetworkName = Object.entries(PASSPHRASE_TO_NETWORK_NAME).find(([passphrase]) => @@ -161,7 +163,9 @@ export const SignAuthEntry = () => { >

{entryNetworkName - ? `${t("The authorization entry is for")} ${entryNetworkName}.` + ? t("The authorization entry is for {{network}}.", { + network: entryNetworkName, + }) : t( "The authorization entry is for a different network than the one you are connected to.", )} @@ -187,6 +191,34 @@ export const SignAuthEntry = () => { ); } + // The CAP-71 address-bound preimage carries the address the signature is + // bound to. We only support authorizing on behalf of the active account, so + // a bound address that isn't the active account (e.g. delegated auth, or a + // smart-wallet/contract authorizer) is blocked the same way a network + // mismatch is — signing it is not supported for now. + const boundAddress = + sorobanAuth instanceof xdr.HashIdPreimageSorobanAuthorizationWithAddress + ? Address.fromScAddress(sorobanAuth.address()).toString() + : undefined; + + if (boundAddress && boundAddress !== publicKey) { + return ( + rejectAndClose()} + isActive + header={t("Freighter is set to a different account")} + > +

+ {t("This authorization is for {{address}}.", { + address: truncateString(boundAddress), + })} +

+

{t("Signing this authorization is not possible at the moment.")}

+ + ); + } + if (!params.url.startsWith("https") && !isNonSSLEnabled) { return ; } @@ -254,9 +286,24 @@ export const SignAuthEntry = () => { {networkName}
+ {boundAddress && ( + // Address-bound authorization (CAP-71): show the address this + // signature is bound to so the user knows who is authorizing. +
+
+ + {t("Authorized address")} +
+
+ +
+
+ )} - + {/* boundAddress is shown in the metadata block above, so it is + intentionally omitted here to avoid duplication. */} +