From bf9a8bbaf76f5360194823fc06077a9d4229f80a Mon Sep 17 00:00:00 2001 From: flarcos Date: Sat, 12 Sep 2026 17:12:16 +0100 Subject: [PATCH 01/10] Add Soran names to payments and history --- @shared/api/internal.ts | 21 + @shared/api/types/message-request.ts | 13 + @shared/api/types/soran.ts | 25 ++ @shared/api/types/types.ts | 1 + @shared/constants/services.ts | 2 + extension/e2e-tests/accountHistory.test.ts | 4 +- .../__tests__/soranPaymentNames.test.ts | 139 ++++++ .../handlers/soranPaymentNames.ts | 89 ++++ .../messageListener/popupMessageListener.ts | 12 + extension/src/constants/localStorageTypes.ts | 2 + .../useSubmitTxData.telemetry.test.tsx | 102 ++++- .../hooks/useSubmitTxData.tsx | 70 ++++ .../__tests__/SoranHistoryDisplay.test.tsx | 116 ++++++ .../accountHistory/HistoryItem/index.tsx | 23 + .../accountHistory/HistoryItem/styles.scss | 9 + .../TransactionDetail/index.tsx | 58 ++- .../TransactionDetail/styles.scss | 33 ++ .../__tests__/HardwareSign.test.tsx | 43 +- .../hardwareConnect/HardwareSign/index.tsx | 27 ++ .../SendAmount/hooks/useSimulateTxData.tsx | 46 +- .../components/send/SendAmount/index.tsx | 16 +- .../__tests__/useSendToData.soran.test.tsx | 174 ++++++++ .../send/SendTo/hooks/useSendToData.tsx | 165 +++++--- .../popup/components/send/SendTo/index.tsx | 58 +-- .../hooks/useSimulateTxData.ts | 31 ++ .../fixtures/soran-destinations.json | 262 ++++++++++++ .../src/popup/helpers/__tests__/soran.test.ts | 189 +++++++++ .../helpers/__tests__/soranAdversary.test.tsx | 394 ++++++++++++++++++ .../helpers/__tests__/soranHistory.test.ts | 66 +++ .../helpers/__tests__/soranPrimary.test.ts | 217 ++++++++++ .../__tests__/soranTransaction.test.ts | 274 ++++++++++++ extension/src/popup/helpers/soran.ts | 261 ++++++++++++ extension/src/popup/helpers/soranHistory.ts | 38 ++ extension/src/popup/helpers/soranPrimary.ts | 217 ++++++++++ .../src/popup/helpers/soranTransaction.ts | 148 +++++++ .../__tests__/useSoranHistoryName.test.tsx | 100 +++++ .../src/popup/hooks/useSoranHistoryName.ts | 91 ++++ .../src/popup/locales/en/translation.json | 15 + .../src/popup/locales/pt/translation.json | 15 + .../__tests__/useGetHistoryData.test.tsx | 19 + .../hooks/useGetHistoryData.tsx | 5 +- .../popup/views/__tests__/Account.test.tsx | 2 +- 42 files changed, 3475 insertions(+), 117 deletions(-) create mode 100644 @shared/api/types/soran.ts create mode 100644 extension/src/background/messageListener/handlers/__tests__/soranPaymentNames.test.ts create mode 100644 extension/src/background/messageListener/handlers/soranPaymentNames.ts create mode 100644 extension/src/popup/components/__tests__/SoranHistoryDisplay.test.tsx create mode 100644 extension/src/popup/components/send/SendTo/hooks/__tests__/useSendToData.soran.test.tsx create mode 100644 extension/src/popup/helpers/__tests__/fixtures/soran-destinations.json create mode 100644 extension/src/popup/helpers/__tests__/soran.test.ts create mode 100644 extension/src/popup/helpers/__tests__/soranAdversary.test.tsx create mode 100644 extension/src/popup/helpers/__tests__/soranHistory.test.ts create mode 100644 extension/src/popup/helpers/__tests__/soranPrimary.test.ts create mode 100644 extension/src/popup/helpers/__tests__/soranTransaction.test.ts create mode 100644 extension/src/popup/helpers/soran.ts create mode 100644 extension/src/popup/helpers/soranHistory.ts create mode 100644 extension/src/popup/helpers/soranPrimary.ts create mode 100644 extension/src/popup/helpers/soranTransaction.ts create mode 100644 extension/src/popup/hooks/__tests__/useSoranHistoryName.test.tsx create mode 100644 extension/src/popup/hooks/useSoranHistoryName.ts diff --git a/@shared/api/internal.ts b/@shared/api/internal.ts index d84f8b89ef..60ad7b6a9b 100644 --- a/@shared/api/internal.ts +++ b/@shared/api/internal.ts @@ -1,3 +1,4 @@ +import { SoranPaymentName, SoranPaymentReference } from "./types/soran"; import { captureException } from "@sentry/browser"; import { Address, @@ -2875,3 +2876,23 @@ export const cacheSwapTopTokens = async ( throw new Error(error); } }; + +export const saveSoranPaymentName = ( + activePublicKey: string, + payment: SoranPaymentName, +): Promise<{ saved: boolean }> => + sendMessageToBackground({ + activePublicKey, + payment, + type: SERVICE_TYPES.SAVE_SORAN_PAYMENT_NAME, + }); + +export const getSoranPaymentName = ( + activePublicKey: string, + payment: SoranPaymentReference, +): Promise<{ name: string | null }> => + sendMessageToBackground({ + activePublicKey, + payment, + type: SERVICE_TYPES.GET_SORAN_PAYMENT_NAME, + }); diff --git a/@shared/api/types/message-request.ts b/@shared/api/types/message-request.ts index 53db55f049..4d321e5a84 100644 --- a/@shared/api/types/message-request.ts +++ b/@shared/api/types/message-request.ts @@ -1,3 +1,4 @@ +import { SoranPaymentName, SoranPaymentReference } from "./soran"; import { Transaction } from "stellar-sdk"; import browser from "webextension-polyfill"; @@ -261,6 +262,16 @@ export interface SignFreighterSorobanTransactionMessage extends BaseMessage { transactionXDR: string; } +export interface SaveSoranPaymentNameMessage extends BaseMessage { + type: SERVICE_TYPES.SAVE_SORAN_PAYMENT_NAME; + payment: SoranPaymentName; +} + +export interface GetSoranPaymentNameMessage extends BaseMessage { + type: SERVICE_TYPES.GET_SORAN_PAYMENT_NAME; + payment: SoranPaymentReference; +} + export interface AddRecentAddressMessage extends BaseMessage { type: SERVICE_TYPES.ADD_RECENT_ADDRESS; address: string; @@ -543,6 +554,8 @@ export type ServiceMessageRequest = | RejectTransactionMessage | SignFreighterTransactionMessage | SignFreighterSorobanTransactionMessage + | SaveSoranPaymentNameMessage + | GetSoranPaymentNameMessage | AddRecentAddressMessage | LoadRecentAddressesMessage | LoadLastAccountUsedMessage diff --git a/@shared/api/types/soran.ts b/@shared/api/types/soran.ts new file mode 100644 index 0000000000..34b23c7a52 --- /dev/null +++ b/@shared/api/types/soran.ts @@ -0,0 +1,25 @@ +/** Local annotation of the exact route used by a successful Freighter send. */ +export interface SoranPaymentReference { + networkPassphrase: string; + transactionHash: string; + destination: string; + memo: string; + memoType: string; +} + +export interface SoranPaymentName extends SoranPaymentReference { + name: string; +} + +export const soranPaymentKey = ( + publicKey: string, + payment: SoranPaymentReference, +) => + JSON.stringify([ + publicKey, + payment.networkPassphrase, + payment.transactionHash, + payment.destination, + payment.memoType || "none", + payment.memo || "", + ]); diff --git a/@shared/api/types/types.ts b/@shared/api/types/types.ts index 9abb6aad50..17e17baa59 100644 --- a/@shared/api/types/types.ts +++ b/@shared/api/types/types.ts @@ -379,6 +379,7 @@ export type HorizonOperation = Horizon.ServerApi.OperationRecord & { [key: string]: any; }; }; + from_muxed?: string; to_muxed?: string; to?: string; from?: string; diff --git a/@shared/constants/services.ts b/@shared/constants/services.ts index 9c967543a3..76bdfad7e6 100644 --- a/@shared/constants/services.ts +++ b/@shared/constants/services.ts @@ -1,4 +1,6 @@ export enum SERVICE_TYPES { + SAVE_SORAN_PAYMENT_NAME = "SAVE_SORAN_PAYMENT_NAME", + GET_SORAN_PAYMENT_NAME = "GET_SORAN_PAYMENT_NAME", CREATE_ACCOUNT = "CREATE_ACCOUNT", FUND_ACCOUNT = "FUND_ACCOUNT", ADD_ACCOUNT = "ADD_ACCOUNT", diff --git a/extension/e2e-tests/accountHistory.test.ts b/extension/e2e-tests/accountHistory.test.ts index e77865a36e..b37243fee9 100644 --- a/extension/e2e-tests/accountHistory.test.ts +++ b/extension/e2e-tests/accountHistory.test.ts @@ -436,7 +436,7 @@ test("History row displays muxed address extracted from XDR for payment", async await page.getByTestId("history-item").nth(0).click(); // Verify muxed address is displayed (from to_muxed field in API response) - const dstAmount = page.getByTestId("KeyIdenticonKey"); + const dstAmount = page.getByTestId("AssetDiff__to-from-address"); await expect(dstAmount).toBeVisible({ timeout: 10000 }); expect(await dstAmount.textContent()).toContain(TEST_M_ADDRESS.slice(0, 4)); @@ -618,7 +618,7 @@ test("History row displays regular G address when no muxed address in XDR", asyn await page.getByTestId("history-item").first().click(); // Verify G address is displayed - const dstAmount = page.getByTestId("KeyIdenticonKey"); + const dstAmount = page.getByTestId("AssetDiff__to-from-address"); await expect(dstAmount).toBeVisible({ timeout: 10000 }); expect(await dstAmount.textContent()).toContain(G_ADDRESS.slice(0, 4)); diff --git a/extension/src/background/messageListener/handlers/__tests__/soranPaymentNames.test.ts b/extension/src/background/messageListener/handlers/__tests__/soranPaymentNames.test.ts new file mode 100644 index 0000000000..4ee0f9ef1c --- /dev/null +++ b/extension/src/background/messageListener/handlers/__tests__/soranPaymentNames.test.ts @@ -0,0 +1,139 @@ +import { Networks, Keypair } from "stellar-sdk"; +import { SERVICE_TYPES } from "@shared/constants/services"; +import { SORAN_PAYMENT_NAMES } from "constants/localStorageTypes"; +import { dataStorageAccess } from "background/helpers/dataStorageAccess"; +import { + saveSoranPaymentName, + getSoranPaymentName, +} from "../soranPaymentNames"; + +const account = Keypair.random().publicKey(); +const destination = Keypair.random().publicKey(); +const payment = { + networkPassphrase: Networks.TESTNET, + transactionHash: "a".repeat(64), + destination, + memo: "hello", + memoType: "text", + name: "alice.nova", +}; +const saveRequest = { + activePublicKey: account, + payment, + type: SERVICE_TYPES.SAVE_SORAN_PAYMENT_NAME as const, +}; +const readRequest = { + activePublicKey: account, + payment, + type: SERVICE_TYPES.GET_SORAN_PAYMENT_NAME as const, +}; +let data: Record; +let localStore: ReturnType; +beforeEach(() => { + data = {}; + localStore = { + getItem: jest.fn(async (key) => data[key]), + setItem: jest.fn(async (key, value) => { + data[key] = value; + }), + remove: jest.fn(), + clear: jest.fn(), + }; +}); +it("saves the original name immutably and keeps accounts and complete routes separate", async () => { + expect( + await saveSoranPaymentName({ request: saveRequest, localStore }), + ).toEqual({ saved: true }); + await saveSoranPaymentName({ + request: { ...saveRequest, payment: { ...payment, name: "changed.nova" } }, + localStore, + }); + expect( + await getSoranPaymentName({ request: readRequest, localStore }), + ).toEqual({ name: "alice.nova" }); + for (const change of [ + { memo: "other" }, + { memoType: "id" }, + { transactionHash: "b".repeat(64) }, + { destination: account }, + { networkPassphrase: Networks.PUBLIC }, + ]) { + expect( + await getSoranPaymentName({ + request: { ...readRequest, payment: { ...payment, ...change } }, + localStore, + }), + ).toEqual({ name: null }); + } + expect( + await getSoranPaymentName({ + request: { ...readRequest, activePublicKey: destination }, + localStore, + }), + ).toEqual({ name: null }); +}); +it("normalizes the no-memo representation", async () => { + await saveSoranPaymentName({ + request: { + ...saveRequest, + payment: { ...payment, memo: "", memoType: "" }, + }, + localStore, + }); + expect( + await getSoranPaymentName({ + request: { + ...readRequest, + payment: { ...payment, memo: "", memoType: "none" }, + }, + localStore, + }), + ).toEqual({ name: "alice.nova" }); +}); +it.each([ + { name: "ALICE.NOVA" }, + { name: "