From 000969ef2a73a1e9f30c6920949026b2158bd1ff Mon Sep 17 00:00:00 2001 From: "Adebanjo Abraham.I" Date: Sat, 29 Aug 2026 12:15:53 +0100 Subject: [PATCH] feat(transaction-panel): send assetIssuer and show a balance hint TransactionPanel's asset selector already let users pick a non-native asset from their balances, but TransactionParams had no assetIssuer field and the payload never included one, so a payment in USDC (or any other issued asset) would submit with only the asset code and no way for the API to disambiguate which issuer's USDC to pay with. - lib/client.ts: add an optional assetIssuer field to TransactionParams. - TransactionPanel.tsx: pass the selected balance's assetIssuer through to transaction.submit (undefined for native XLM), and show the selected asset's balance as a hint under the Amount input via the existing Input hint prop. Closes #565 Closes #566 --- src/components/TransactionPanel.test.tsx | 89 ++++++++++++++++++++++++ src/components/TransactionPanel.tsx | 6 ++ src/lib/client.ts | 2 + 3 files changed, 97 insertions(+) diff --git a/src/components/TransactionPanel.test.tsx b/src/components/TransactionPanel.test.tsx index d109556..bdc0885 100644 --- a/src/components/TransactionPanel.test.tsx +++ b/src/components/TransactionPanel.test.tsx @@ -361,6 +361,95 @@ describe("TransactionPanel", () => { expect(select).toBeDisabled(); expect(select).toHaveValue("XLM"); }); + + it("includes the asset's issuer in the submitted payload for a non-native asset (#565)", async () => { + const mockSubmit = vi + .fn() + .mockResolvedValue({ data: { hash: "h1", ledger: 1 }, error: null }); + mockGetClient(mockSubmit); + vi.mocked(useSorokit).mockReturnValue({ + address: "GABC", + isConnected: true, + balances, + // getClient() here returns the object mockGetClient() just wired up + // above; TransactionPanel reads `client` from context, not from a + // direct getClient() call, so the mocked client has to be threaded + // through explicitly for the submission to actually run. + client: getClient(), + } as unknown as ReturnType); + + render(); + + fireEvent.change(screen.getByLabelText("Asset"), { + target: { value: "USDC" }, + }); + const validDest = + "GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + fireEvent.change(screen.getByLabelText("Destination Address"), { + target: { value: validDest }, + }); + fireEvent.change(screen.getByLabelText("Amount (USDC)"), { + target: { value: "10" }, + }); + + await reviewAndConfirm(); + + await screen.findByText("Transaction submitted"); + expect(mockSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + asset: "USDC", + assetIssuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + }), + ); + }); + + it("omits assetIssuer for the native XLM asset (#565)", async () => { + const mockSubmit = vi + .fn() + .mockResolvedValue({ data: { hash: "h1", ledger: 1 }, error: null }); + mockGetClient(mockSubmit); + vi.mocked(useSorokit).mockReturnValue({ + address: "GABC", + isConnected: true, + balances, + client: getClient(), + } as unknown as ReturnType); + + render(); + + const validDest = + "GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + fireEvent.change(screen.getByLabelText("Destination Address"), { + target: { value: validDest }, + }); + fireEvent.change(screen.getByLabelText("Amount (XLM)"), { + target: { value: "10" }, + }); + + await reviewAndConfirm(); + + await screen.findByText("Transaction submitted"); + expect(mockSubmit).toHaveBeenCalledWith( + expect.objectContaining({ asset: "XLM", assetIssuer: undefined }), + ); + }); + + it("shows the selected asset's balance as a hint near the amount input (#565)", () => { + vi.mocked(useSorokit).mockReturnValue({ + address: "GABC", + isConnected: true, + balances, + } as unknown as ReturnType); + + render(); + + expect(screen.getByText("Balance: 100.0000000 XLM")).toBeInTheDocument(); + + fireEvent.change(screen.getByLabelText("Asset"), { + target: { value: "USDC" }, + }); + expect(screen.getByText("Balance: 50.0000000 USDC")).toBeInTheDocument(); + }); }); describe("memo ID validation", () => { diff --git a/src/components/TransactionPanel.tsx b/src/components/TransactionPanel.tsx index 9dfc9f6..82527e6 100644 --- a/src/components/TransactionPanel.tsx +++ b/src/components/TransactionPanel.tsx @@ -150,6 +150,7 @@ export function TransactionPanel({ destination: dest.trim(), amount: amount.trim(), asset: selectedAsset, + assetIssuer: selectedAssetBalance?.assetIssuer, memoType, memo: memoType !== "none" && memo.trim() !== "" ? memo.trim() : undefined, @@ -366,6 +367,11 @@ export function TransactionPanel({ setAmount(e.target.value); setAmountDirty(true); }} + hint={ + selectedAssetBalance + ? `Balance: ${selectedAssetBalance.balance} ${selectedAsset}` + : undefined + } error={ amountDirty ? amount.trim() === "" diff --git a/src/lib/client.ts b/src/lib/client.ts index c2d675d..da85cb1 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -11,6 +11,8 @@ export interface TransactionParams { destination: string; amount: string; asset: string; + /** Issuer account for a non-native asset. Omitted (or undefined) for XLM. */ + assetIssuer?: string; memoType: "none" | "text" | "id"; memo?: string; }