@coin-voyage/swapkit 0.2.0 → 0.4.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/components/connected-wallets.d.ts +1 -1
  2. package/dist/components/connected-wallets.js +37 -14
  3. package/dist/components/connected-wallets.test.d.ts +1 -0
  4. package/dist/components/connected-wallets.test.js +104 -0
  5. package/dist/components/styles.d.ts +2 -0
  6. package/dist/components/styles.js +27 -0
  7. package/dist/components/swap-recipient.test.d.ts +1 -0
  8. package/dist/components/swap-recipient.test.js +105 -0
  9. package/dist/components/swap.d.ts +1 -1
  10. package/dist/components/swap.js +20 -19
  11. package/dist/components/swap.test.d.ts +1 -0
  12. package/dist/components/swap.test.js +76 -0
  13. package/dist/components/token-picker.d.ts +3 -1
  14. package/dist/components/token-picker.js +20 -10
  15. package/dist/components/token-picker.test.d.ts +1 -0
  16. package/dist/components/token-picker.test.js +43 -0
  17. package/dist/components/wallet-connector.d.ts +4 -2
  18. package/dist/components/wallet-connector.js +89 -20
  19. package/dist/components/wallet-connector.test.d.ts +1 -0
  20. package/dist/components/wallet-connector.test.js +143 -0
  21. package/dist/hooks/use-order-status.d.ts +1 -1
  22. package/dist/hooks/use-token-data.d.ts +1 -1
  23. package/dist/hooks/use-token-data.js +2 -2
  24. package/dist/hooks/use-token-data.test.d.ts +1 -0
  25. package/dist/hooks/use-token-data.test.js +36 -0
  26. package/dist/lib/amount-input.d.ts +1 -0
  27. package/dist/lib/amount-input.js +7 -0
  28. package/dist/lib/amount-input.test.d.ts +1 -0
  29. package/dist/lib/amount-input.test.js +17 -0
  30. package/dist/lib/output-wallet.d.ts +1 -2
  31. package/dist/lib/output-wallet.js +2 -2
  32. package/dist/lib/output-wallet.test.d.ts +1 -0
  33. package/dist/lib/output-wallet.test.js +26 -0
  34. package/dist/providers/swapkit-context.d.ts +1 -1
  35. package/dist/providers/swapkit-provider.d.ts +1 -1
  36. package/dist/providers/swapkit-provider.js +4 -2
  37. package/dist/providers/swapkit-provider.test.d.ts +1 -0
  38. package/dist/providers/swapkit-provider.test.js +20 -0
  39. package/dist/styles/root.js +1 -1
  40. package/dist/styles/theme.js +1 -1
  41. package/dist/types.d.ts +2 -1
  42. package/package.json +8 -8
@@ -1 +1 @@
1
- export declare function ConnectedWallets(): import("react/jsx-runtime").JSX.Element | null;
1
+ export declare function ConnectedWallets(): import("react/jsx-runtime").JSX.Element;
@@ -1,10 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useAccount, useAccountDisconnect, useInstalledWallets } from "@coin-voyage/crypto/hooks";
3
3
  import { getChainTypeName } from "@coin-voyage/shared/chain";
4
+ import { ChainType } from "@coin-voyage/shared/types";
4
5
  import { useEffect, useRef, useState } from "react";
5
6
  import { CloseIcon, UnplugIcon } from "./icons";
6
- import { ConnectedWalletPanel, ConnectedWalletRow, DisconnectAllButton, IconButton, TriggerWalletIcons, ToolbarGroup, WalletDetails, WalletDisconnectButton, WalletIdentity, WalletLabel, WalletPanelHeader, } from "./styles";
7
+ import { ConnectedWalletPanel, ConnectedWalletRow, DisconnectAllButton, IconButton, TriggerWalletIcons, ToolbarGroup, WalletActions, WalletDetails, WalletDisconnectButton, WalletIdentity, WalletLabel, WalletPanelHeader, WalletSwitchButton, } from "./styles";
7
8
  import { WalletIconDisplay } from "./wallet-icon";
9
+ import { WalletConnector } from "./wallet-connector";
8
10
  function truncateAddress(address) {
9
11
  return address.length <= 14 ? address : `${address.slice(0, 6)}…${address.slice(-5)}`;
10
12
  }
@@ -17,6 +19,15 @@ function connectorId(connector) {
17
19
  return connector.name;
18
20
  return undefined;
19
21
  }
22
+ function connectorName(connector) {
23
+ if (!connector || typeof connector !== "object")
24
+ return undefined;
25
+ if ("displayName" in connector && typeof connector.displayName === "string")
26
+ return connector.displayName;
27
+ if ("name" in connector && typeof connector.name === "string")
28
+ return connector.name;
29
+ return undefined;
30
+ }
20
31
  function accountAddress(account) {
21
32
  return account.address ?? account.addresses?.[0];
22
33
  }
@@ -25,11 +36,20 @@ export function ConnectedWallets() {
25
36
  const wallets = useInstalledWallets();
26
37
  const disconnectAccount = useAccountDisconnect();
27
38
  const [open, setOpen] = useState(false);
39
+ const [switchAccount, setSwitchAccount] = useState(null);
28
40
  const groupRef = useRef(null);
29
41
  const connectedAccounts = accounts.filter((account) => account.isConnected);
30
42
  const walletForAccount = (account) => {
31
43
  const accountConnectorId = connectorId(account.connector);
32
- return wallets.find((wallet) => wallet.connectors.some((candidate) => candidate.chainType === account.chainType && connectorId(candidate.connector) === accountConnectorId));
44
+ const accountConnectorName = connectorName(account.connector)?.toLowerCase();
45
+ return wallets.find((wallet) => wallet.connectors.some((candidate) => {
46
+ if (candidate.chainType !== account.chainType)
47
+ return false;
48
+ const candidateConnectorId = connectorId(candidate.connector);
49
+ const candidateConnectorName = connectorName(candidate.connector)?.toLowerCase();
50
+ return ((accountConnectorId !== undefined && candidateConnectorId === accountConnectorId) ||
51
+ (accountConnectorName !== undefined && candidateConnectorName === accountConnectorName));
52
+ }));
33
53
  };
34
54
  useEffect(() => {
35
55
  if (!open)
@@ -49,18 +69,21 @@ export function ConnectedWallets() {
49
69
  document.removeEventListener("keydown", dismissFromKeyboard);
50
70
  };
51
71
  }, [open]);
52
- if (connectedAccounts.length === 0)
53
- return null;
54
72
  const triggerLabel = connectedAccounts.length === 1
55
73
  ? truncateAddress(accountAddress(connectedAccounts[0]) ?? "Wallet")
56
74
  : `${connectedAccounts.length} connections`;
57
- return (_jsxs(ToolbarGroup, { ref: groupRef, children: [_jsxs(IconButton, { type: "button", "aria-label": "Connected wallets", "aria-expanded": open, onClick: () => setOpen((current) => !current), children: [_jsx(TriggerWalletIcons, { children: connectedAccounts.slice(0, 3).map((account, index) => {
58
- const wallet = walletForAccount(account);
59
- return (_jsx(WalletIconDisplay, { icon: wallet?.icon, name: wallet?.name ?? getChainTypeName(account.chainType), size: 20 }, `${account.chainType}:${accountAddress(account) ?? index}`));
60
- }) }), _jsx(WalletLabel, { children: triggerLabel })] }), open && (_jsxs(ConnectedWalletPanel, { role: "menu", "aria-label": "Connected wallets menu", children: [_jsx(WalletPanelHeader, { children: "Connected wallets" }), connectedAccounts.map((account, index) => {
61
- const wallet = walletForAccount(account);
62
- const chainName = getChainTypeName(account.chainType);
63
- const address = accountAddress(account);
64
- return (_jsxs(ConnectedWalletRow, { children: [_jsxs(WalletIdentity, { children: [_jsx(WalletIconDisplay, { icon: wallet?.icon, name: wallet?.name ?? chainName, size: 20 }), _jsxs(WalletDetails, { children: [_jsx("strong", { children: chainName }), _jsx("small", { children: address ? truncateAddress(address) : "Connected" })] })] }), _jsx(WalletDisconnectButton, { type: "button", "aria-label": `Disconnect ${chainName} wallet`, onClick: () => void disconnectAccount(account), children: _jsx(CloseIcon, { size: 16 }) })] }, `${account.chainType}:${address ?? index}`));
65
- }), _jsxs(DisconnectAllButton, { type: "button", onClick: () => void disconnectAccount(), children: [_jsx(UnplugIcon, {}), "Disconnect all"] })] }))] }));
75
+ return (_jsxs(_Fragment, { children: [connectedAccounts.length > 0 && (_jsxs(ToolbarGroup, { ref: groupRef, children: [_jsxs(IconButton, { type: "button", "aria-label": "Connected wallets", "aria-expanded": open, onClick: () => setOpen((current) => !current), children: [_jsx(TriggerWalletIcons, { children: connectedAccounts.slice(0, 3).map((account, index) => {
76
+ const wallet = walletForAccount(account);
77
+ return (_jsx(WalletIconDisplay, { icon: wallet?.icon, name: wallet?.name ?? getChainTypeName(account.chainType), size: 20 }, `${account.chainType}:${accountAddress(account) ?? index}`));
78
+ }) }), _jsx(WalletLabel, { children: triggerLabel })] }), open && (_jsxs(ConnectedWalletPanel, { role: "group", "aria-label": "Connected wallets", children: [_jsx(WalletPanelHeader, { children: "Connected wallets" }), connectedAccounts.map((account, index) => {
79
+ const wallet = walletForAccount(account);
80
+ const chainName = getChainTypeName(account.chainType);
81
+ const address = accountAddress(account);
82
+ return (_jsxs(ConnectedWalletRow, { children: [_jsxs(WalletIdentity, { children: [_jsx(WalletIconDisplay, { icon: wallet?.icon, name: wallet?.name ?? chainName, size: 20 }), _jsxs(WalletDetails, { children: [_jsx("strong", { children: chainName }), _jsx("small", { children: address ? truncateAddress(address) : "Connected" })] })] }), _jsxs(WalletActions, { children: [_jsx(WalletSwitchButton, { type: "button", "aria-label": `Switch ${chainName} wallet`, onClick: () => {
83
+ setOpen(false);
84
+ setSwitchAccount(account);
85
+ }, children: "Switch" }), _jsx(WalletDisconnectButton, { type: "button", "aria-label": `Disconnect ${chainName} wallet`, onClick: () => void disconnectAccount(account), children: _jsx(CloseIcon, { size: 16 }) })] })] }, `${account.chainType}:${address ?? index}`));
86
+ }), _jsxs(DisconnectAllButton, { type: "button", onClick: () => void disconnectAccount(), children: [_jsx(UnplugIcon, {}), "Disconnect all"] })] }))] })), _jsx(WalletConnector, { open: switchAccount !== null, chainType: switchAccount?.chainType, intent: "switch", beforeConnect: switchAccount && switchAccount.chainType !== ChainType.SOL
87
+ ? () => disconnectAccount(switchAccount)
88
+ : undefined, onClose: () => setSwitchAccount(null) })] }));
66
89
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment jsdom
3
+ import { ChainId, ChainType } from "@coin-voyage/shared/types";
4
+ import { cleanup, render, screen } from "@testing-library/react";
5
+ import userEvent from "@testing-library/user-event";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { ConnectedWallets } from "./connected-wallets";
8
+ const disconnectAccount = vi.fn();
9
+ let accountsConnected = true;
10
+ let accountChainType = ChainType.SOL;
11
+ let accountAddress = "current-address";
12
+ let accountConnector = { id: "Test wallet", name: "Test wallet" };
13
+ let installedWalletOverride;
14
+ vi.mock("@coin-voyage/crypto/hooks", () => ({
15
+ useAccount: () => ({
16
+ accounts: accountsConnected
17
+ ? [
18
+ {
19
+ address: accountAddress,
20
+ chainId: ChainId.SOL,
21
+ chainType: accountChainType,
22
+ connector: accountConnector,
23
+ isConnected: true,
24
+ isConnecting: false,
25
+ isDisconnected: false,
26
+ isReconnecting: false,
27
+ status: "connected",
28
+ },
29
+ ]
30
+ : [],
31
+ }),
32
+ useAccountDisconnect: () => disconnectAccount,
33
+ useInstalledWallets: () => [
34
+ installedWalletOverride ?? {
35
+ id: "test-wallet",
36
+ name: "Test wallet",
37
+ connectors: [{ chainType: accountChainType, connector: { id: "Test wallet", name: "Test wallet" } }],
38
+ },
39
+ ],
40
+ }));
41
+ vi.mock("./wallet-connector", () => ({
42
+ WalletConnector: ({ open, chainType, intent }) => open ? _jsx("div", { "data-testid": "wallet-connector", children: `${intent}:${chainType}` }) : null,
43
+ }));
44
+ afterEach(() => {
45
+ cleanup();
46
+ disconnectAccount.mockClear();
47
+ accountsConnected = true;
48
+ accountChainType = ChainType.SOL;
49
+ accountAddress = "current-address";
50
+ accountConnector = { id: "Test wallet", name: "Test wallet" };
51
+ installedWalletOverride = undefined;
52
+ });
53
+ describe("ConnectedWallets", () => {
54
+ it("opens the wallet switcher for the selected account chain", async () => {
55
+ const user = userEvent.setup();
56
+ render(_jsx(ConnectedWallets, {}));
57
+ await user.click(screen.getByRole("button", { name: "Connected wallets" }));
58
+ await user.click(screen.getByRole("button", { name: "Switch Solana wallet" }));
59
+ expect(screen.queryByRole("group", { name: "Connected wallets" })).toBeNull();
60
+ expect(screen.getByTestId("wallet-connector").textContent).toBe(`switch:${ChainType.SOL}`);
61
+ expect(disconnectAccount).not.toHaveBeenCalled();
62
+ });
63
+ it("keeps the switcher open while the previous account disconnects", async () => {
64
+ const user = userEvent.setup();
65
+ const { rerender } = render(_jsx(ConnectedWallets, {}));
66
+ await user.click(screen.getByRole("button", { name: "Connected wallets" }));
67
+ await user.click(screen.getByRole("button", { name: "Switch Solana wallet" }));
68
+ accountsConnected = false;
69
+ rerender(_jsx(ConnectedWallets, {}));
70
+ expect(screen.getByTestId("wallet-connector").textContent).toBe(`switch:${ChainType.SOL}`);
71
+ });
72
+ it("updates the connected-wallet label when Phantom changes address", () => {
73
+ accountAddress = "old-address-123456789";
74
+ const { rerender } = render(_jsx(ConnectedWallets, {}));
75
+ expect(screen.getByRole("button", { name: "Connected wallets" }).textContent).toContain("old-ad…56789");
76
+ accountAddress = "new-address-987654321";
77
+ rerender(_jsx(ConnectedWallets, {}));
78
+ expect(screen.getByRole("button", { name: "Connected wallets" }).textContent).toContain("new-ad…54321");
79
+ });
80
+ it("uses the Phantom icon for its Ethereum connection in a multichain wallet", () => {
81
+ accountChainType = ChainType.EVM;
82
+ accountConnector = { id: "injected", name: "Phantom" };
83
+ installedWalletOverride = {
84
+ id: "app.phantom",
85
+ name: "Phantom",
86
+ icon: "phantom.svg",
87
+ connectors: [
88
+ { chainType: ChainType.EVM, connector: { id: "app.phantom", name: "Phantom" } },
89
+ { chainType: ChainType.SOL, connector: { id: "Phantom", name: "Phantom" } },
90
+ ],
91
+ };
92
+ render(_jsx(ConnectedWallets, {}));
93
+ expect(screen.getByAltText("Phantom wallet")).not.toBeNull();
94
+ });
95
+ it("only disconnects an account from the explicit disconnect action", async () => {
96
+ accountChainType = ChainType.EVM;
97
+ const user = userEvent.setup();
98
+ render(_jsx(ConnectedWallets, {}));
99
+ await user.click(screen.getByRole("button", { name: "Connected wallets" }));
100
+ await user.click(screen.getByRole("button", { name: "Disconnect Ethereum wallet" }));
101
+ expect(disconnectAccount).toHaveBeenCalledTimes(1);
102
+ expect(disconnectAccount.mock.calls[0]?.[0]?.chainType).toBe(ChainType.EVM);
103
+ });
104
+ });
@@ -95,6 +95,8 @@ export declare const WalletPanelHeader: import("styled-components").StyledCompon
95
95
  export declare const ConnectedWalletRow: import("styled-components").StyledComponent<"div", any, {}, never>;
96
96
  export declare const WalletIdentity: import("styled-components").StyledComponent<"div", any, {}, never>;
97
97
  export declare const WalletDetails: import("styled-components").StyledComponent<"div", any, {}, never>;
98
+ export declare const WalletActions: import("styled-components").StyledComponent<"div", any, {}, never>;
99
+ export declare const WalletSwitchButton: import("styled-components").StyledComponent<"button", any, {}, never>;
98
100
  export declare const WalletDisconnectButton: import("styled-components").StyledComponent<"button", any, {}, never>;
99
101
  export declare const DisconnectAllButton: import("styled-components").StyledComponent<"button", any, {}, never>;
100
102
  export declare const TriggerWalletIcons: import("styled-components").StyledComponent<"span", any, {}, never>;
@@ -955,6 +955,11 @@ export const WalletOption = styled.button `
955
955
  &:hover {
956
956
  background: var(--sk-surface);
957
957
  }
958
+
959
+ &:disabled {
960
+ cursor: wait;
961
+ opacity: 0.6;
962
+ }
958
963
  `;
959
964
  export const WalletIconFrame = styled.span `
960
965
  display: inline-flex;
@@ -1068,6 +1073,28 @@ export const WalletDetails = styled.div `
1068
1073
  font-size: 12px;
1069
1074
  }
1070
1075
  `;
1076
+ export const WalletActions = styled.div `
1077
+ display: flex;
1078
+ flex: 0 0 auto;
1079
+ align-items: center;
1080
+ gap: 4px;
1081
+ `;
1082
+ export const WalletSwitchButton = styled.button `
1083
+ height: 28px;
1084
+ padding: 0 8px;
1085
+ border: 0;
1086
+ border-radius: 6px;
1087
+ color: var(--sk-text-muted);
1088
+ background: transparent;
1089
+ cursor: pointer;
1090
+ font-size: 12px;
1091
+ font-weight: 500;
1092
+
1093
+ &:hover {
1094
+ color: var(--sk-accent);
1095
+ background: color-mix(in srgb, var(--sk-accent) 12%, transparent);
1096
+ }
1097
+ `;
1071
1098
  export const WalletDisconnectButton = styled.button `
1072
1099
  display: inline-flex;
1073
1100
  width: 28px;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment jsdom
3
+ import { ChainId, ChainType } from "@coin-voyage/shared/types";
4
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { Swap } from "./swap";
7
+ const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
8
+ vi.mock("@coin-voyage/crypto/hooks", () => ({ useAccount: () => ({ accounts }) }));
9
+ vi.mock("../providers/swapkit-context", () => ({ useSwapKit: () => ({ mode: "light", theme: {} }) }));
10
+ vi.mock("../hooks/use-swap-execution", () => ({
11
+ useSwapExecution: () => ({ stage: { kind: "idle" }, execute, reset: vi.fn() }),
12
+ }));
13
+ vi.mock("../hooks/use-token-data", () => ({
14
+ useTokenData: () => ({
15
+ tokens: [{ currency: source, balance: "2" }, { currency: destination }, { currency: solana }],
16
+ accounts,
17
+ isLoading: false,
18
+ refreshBalances: vi.fn(),
19
+ }),
20
+ }));
21
+ vi.mock("../hooks/use-swap-quote", () => ({
22
+ useSwapQuote: () => ({
23
+ quote: {
24
+ input: { currency: source, amount: { raw: "1000000000000000000", ui: "1" } },
25
+ output: { currency: destination, amount: { raw: "1000000", ui: "1" } },
26
+ price_impact: 0,
27
+ },
28
+ loading: false,
29
+ }),
30
+ }));
31
+ vi.mock("./connected-wallets", () => ({ ConnectedWallets: () => null }));
32
+ vi.mock("./token-picker", () => ({ TokenPicker: () => null }));
33
+ vi.mock("./wallet-connector", () => ({ WalletConnector: () => null }));
34
+ vi.mock("./execution-modal", () => ({ ExecutionModal: () => null }));
35
+ const sender = "0x1111111111111111111111111111111111111111";
36
+ const recipient = "0x2222222222222222222222222222222222222222";
37
+ const accounts = [{ address: sender, chainType: ChainType.EVM }];
38
+ const source = {
39
+ id: "eth",
40
+ chain_id: ChainId.ETH,
41
+ name: "Ether",
42
+ symbol: "ETH",
43
+ decimals: 18,
44
+ };
45
+ const destination = {
46
+ id: "usdc",
47
+ chain_id: ChainId.ETH,
48
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
49
+ name: "USD Coin",
50
+ symbol: "USDC",
51
+ decimals: 6,
52
+ };
53
+ const solana = {
54
+ id: "sol",
55
+ chain_id: ChainId.SOL,
56
+ name: "Solana",
57
+ symbol: "SOL",
58
+ decimals: 9,
59
+ };
60
+ beforeEach(() => execute.mockClear());
61
+ afterEach(cleanup);
62
+ function renderSwap(output = destination) {
63
+ render(_jsx(Swap, { defaultSourceCurrency: source, defaultDestinationCurrency: output }));
64
+ fireEvent.change(screen.getByRole("textbox", { name: "Sell amount" }), { target: { value: "1" } });
65
+ }
66
+ function openRecipientModal() {
67
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
68
+ fireEvent.click(screen.getByRole("menuitem", { name: "Paste Address" }));
69
+ }
70
+ describe("Swap destination recipient", () => {
71
+ it("offers only a pasted address for swaps on the same chain", () => {
72
+ renderSwap();
73
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
74
+ expect(screen.getByRole("menuitem", { name: "Paste Address" })).toBeTruthy();
75
+ expect(screen.queryByRole("menuitem", { name: "Connect Wallet" })).toBeNull();
76
+ expect(screen.getAllByRole("menuitem")).toHaveLength(1);
77
+ });
78
+ it("executes a same-chain swap to the pasted recipient", () => {
79
+ renderSwap();
80
+ openRecipientModal();
81
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: recipient } });
82
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
83
+ expect(screen.queryByRole("dialog", { name: "Paste destination address" })).toBeNull();
84
+ fireEvent.click(screen.getByRole("button", { name: "Swap" }));
85
+ expect(execute).toHaveBeenCalledWith(expect.objectContaining({
86
+ source: expect.objectContaining({ sender_address: sender }),
87
+ destination: expect.objectContaining({ recipient }),
88
+ }), sender);
89
+ });
90
+ it("rejects invalid same-chain destination addresses", () => {
91
+ renderSwap();
92
+ openRecipientModal();
93
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: "invalid" } });
94
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
95
+ expect(screen.getByText("Enter a valid address for the destination network")).toBeTruthy();
96
+ expect(screen.getByRole("dialog", { name: "Paste destination address" })).toBeTruthy();
97
+ expect(execute).not.toHaveBeenCalled();
98
+ });
99
+ it("keeps both connect and paste options for cross-family swaps", () => {
100
+ renderSwap(solana);
101
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
102
+ expect(screen.getByRole("menuitem", { name: "Connect Wallet" })).toBeTruthy();
103
+ expect(screen.getByRole("menuitem", { name: "Paste Address" })).toBeTruthy();
104
+ });
105
+ });
@@ -15,7 +15,7 @@ interface CurrencyPanelProps {
15
15
  side: SwapSide;
16
16
  token?: SwapToken;
17
17
  amount: string;
18
- balance?: number;
18
+ balance?: string;
19
19
  quoteUsd?: number | null;
20
20
  quoteLoading?: boolean;
21
21
  disabled?: boolean;
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useAccount } from "@coin-voyage/crypto/hooks";
3
3
  import { getChainTypeByChainId, isValidAddress } from "@coin-voyage/shared/chain";
4
+ import { formatCurrencyAmount } from "@coin-voyage/shared/currency";
4
5
  import { currencyID } from "@coin-voyage/shared/types";
5
6
  import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
6
7
  import { useSwapExecution } from "../hooks/use-swap-execution";
7
8
  import { useSwapQuote } from "../hooks/use-swap-quote";
8
9
  import { useSwapState } from "../hooks/use-swap-state";
9
10
  import { useTokenData } from "../hooks/use-token-data";
10
- import { isValidSwapAmountInput } from "../lib/amount-input";
11
+ import { balanceFraction, isValidSwapAmountInput } from "../lib/amount-input";
11
12
  import { getSwapWalletRequirement, shouldShowOutputWalletSelector, shouldUseSeparateOutputWallet, } from "../lib/output-wallet";
12
13
  import { buildSwapQuoteRequest, isSameSwap, toCurrencyBase } from "../lib/quote";
13
14
  import { useSwapKit } from "../providers/swapkit-context";
@@ -119,7 +120,7 @@ function OutputWalletSelector({ label, open, onOpenChange, onConnect, onPaste, }
119
120
  document.removeEventListener("keydown", dismissOnEscape);
120
121
  };
121
122
  }, [onOpenChange, open]);
122
- return (_jsxs(OutputWalletControl, { ref: controlRef, children: [_jsxs(OutputWalletTrigger, { type: "button", "aria-label": `Output wallet: ${label}`, "aria-expanded": open, onClick: () => onOpenChange(!open), children: [_jsx("span", { children: label }), _jsx(ChevronDownIcon, { size: 16, up: open })] }), open && (_jsxs(OutputWalletMenu, { role: "menu", "aria-label": "Destination wallet options", children: [_jsxs(OutputWalletOption, { type: "button", role: "menuitem", onClick: onConnect, children: [_jsx("span", { children: "Connect Wallet" }), _jsx(WalletIcon, {})] }), _jsxs(OutputWalletOption, { type: "button", role: "menuitem", onClick: onPaste, children: [_jsx("span", { children: "Paste Address" }), _jsx(PasteIcon, {})] })] }))] }));
123
+ return (_jsxs(OutputWalletControl, { ref: controlRef, children: [_jsxs(OutputWalletTrigger, { type: "button", "aria-label": `Output wallet: ${label}`, "aria-expanded": open, onClick: () => onOpenChange(!open), children: [_jsx("span", { children: label }), _jsx(ChevronDownIcon, { size: 16, up: open })] }), open && (_jsxs(OutputWalletMenu, { role: "menu", "aria-label": "Destination wallet options", children: [onConnect && (_jsxs(OutputWalletOption, { type: "button", role: "menuitem", onClick: onConnect, children: [_jsx("span", { children: "Connect Wallet" }), _jsx(WalletIcon, {})] })), _jsxs(OutputWalletOption, { type: "button", role: "menuitem", onClick: onPaste, children: [_jsx("span", { children: "Paste Address" }), _jsx(PasteIcon, {})] })] }))] }));
123
124
  }
124
125
  function formatUsd(value) {
125
126
  if (value === undefined || value === null || !Number.isFinite(value))
@@ -143,11 +144,6 @@ const QUICK_AMOUNTS = [
143
144
  { label: "75%", fraction: 0.75 },
144
145
  { label: "Max", fraction: 1 },
145
146
  ];
146
- function balanceFraction(balance, fraction, decimals) {
147
- if (fraction === 1)
148
- return balance.toString();
149
- return Number((balance * fraction).toFixed(Math.min(Math.max(decimals, 0), 18))).toString();
150
- }
151
147
  export function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken, footerAction, }) {
152
148
  const label = side === "input" ? "SELL" : "BUY";
153
149
  const showQuickAmounts = side === "input" && balance !== undefined && token !== undefined;
@@ -156,13 +152,13 @@ export function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoa
156
152
  : `Use ${amountLabel} of ${token.currency.symbol} balance`, onClick: () => onAmountChange(balanceFraction(balance, fraction, token.currency.decimals)), children: amountLabel }, amountLabel))) })), _jsxs(AmountRow, { children: [quoteLoading ? (_jsx(AmountSkeleton, { role: "status", "aria-label": "Loading quoted amount" })) : (_jsx(AmountInput, { "aria-label": side === "input" ? "Sell amount" : "Buy amount", type: "text", inputMode: "decimal", autoComplete: "off", autoCorrect: "off", spellCheck: false, placeholder: "0", value: amount, disabled: disabled, onChange: (event) => {
157
153
  if (isValidSwapAmountInput(event.target.value))
158
154
  onAmountChange(event.target.value);
159
- } })), _jsxs(TokenButton, { type: "button", onClick: onSelectToken, "aria-label": `Select ${side} token`, children: [token ? _jsx(TokenIcon, { currency: token.currency }) : null, _jsx("span", { children: token?.currency.symbol ?? "Select" }), _jsx(ChevronDownIcon, {})] })] }), _jsxs(CardFooter, { children: [quoteLoading ? (_jsx(QuoteValueSkeleton, { role: "status", "aria-label": "Loading quoted value" })) : (_jsx("span", { children: formatUsd(quoteUsd ?? tokenUsdValue(token, amount)) })), _jsxs(InlineActions, { children: [balance !== undefined && _jsxs("span", { children: ["Balance: ", balance.toLocaleString()] }), footerAction] })] })] }));
155
+ } })), _jsxs(TokenButton, { type: "button", onClick: onSelectToken, "aria-label": `Select ${side} token`, children: [token ? _jsx(TokenIcon, { currency: token.currency }) : null, _jsx("span", { children: token?.currency.symbol ?? "Select" }), _jsx(ChevronDownIcon, {})] })] }), _jsxs(CardFooter, { children: [quoteLoading ? (_jsx(QuoteValueSkeleton, { role: "status", "aria-label": "Loading quoted value" })) : (_jsx("span", { children: formatUsd(quoteUsd ?? tokenUsdValue(token, amount)) })), _jsxs(InlineActions, { children: [balance !== undefined && _jsxs("span", { children: ["Balance: ", Number(balance).toLocaleString()] }), footerAction] })] })] }));
160
156
  }
161
157
  function ManualWalletModal({ open, chainId, initialValue, onClose, onSave, }) {
162
158
  const [value, setValue] = useState(initialValue ?? "");
163
159
  const [error, setError] = useState(null);
164
160
  const outputAddressId = useId();
165
- return (_jsxs(Modal, { open: open, title: "Paste destination address", onClose: onClose, children: [_jsx(FieldLabel, { htmlFor: outputAddressId, children: "Wallet address" }), _jsx(CompactInput, { id: outputAddressId, value: value, autoComplete: "off", onChange: (event) => setValue(event.target.value.trim()) }), error && _jsx(Hint, { "$tone": "danger", children: error }), _jsx(PrimaryButton, { type: "button", onClick: () => {
161
+ return (_jsxs(Modal, { open: open, title: "Paste destination address", onClose: onClose, children: [_jsx(FieldLabel, { htmlFor: outputAddressId, children: "Wallet address" }), _jsx(CompactInput, { id: outputAddressId, value: value, autoComplete: "off", onChange: (event) => setValue(event.target.value.trim()) }), error && _jsx(Hint, { "$tone": "danger", children: error }), _jsx(PrimaryButton, { type: "button", style: { marginTop: 12 }, onClick: () => {
166
162
  if (!chainId || !isValidAddress(value, chainId)) {
167
163
  setError("Enter a valid address for the destination network");
168
164
  return;
@@ -206,7 +202,7 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
206
202
  const manualAddress = manualOutputWallet && manualOutputWallet.chainId === destinationToken?.currency.chain_id
207
203
  ? manualOutputWallet.address
208
204
  : undefined;
209
- const outputAddress = separateOutputWallet ? (manualAddress ?? outputAccount?.address) : inputAccount?.address;
205
+ const outputAddress = manualAddress ?? (separateOutputWallet ? outputAccount?.address : inputAccount?.address);
210
206
  const walletRequirement = getSwapWalletRequirement({
211
207
  inputWalletAddress: inputAccount?.address,
212
208
  inputChainType: sourceChainType,
@@ -215,7 +211,6 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
215
211
  outputChainType: destinationChainType,
216
212
  });
217
213
  const showOutputWalletSelector = shouldShowOutputWalletSelector({
218
- requiresSeparateOutputWallet: separateOutputWallet,
219
214
  outputChainType: destinationChainType,
220
215
  connectionRequirement: walletRequirement,
221
216
  });
@@ -253,10 +248,14 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
253
248
  onComplete,
254
249
  onError,
255
250
  });
256
- const displayedInputAmount = activeSide === "output" && quoteState.quote ? quoteState.quote.input.amount.ui : inputAmount;
257
- const displayedOutputAmount = activeSide === "input" && quoteState.quote ? quoteState.quote.output.amount.ui : outputAmount;
251
+ const displayedInputAmount = activeSide === "output" && quoteState.quote
252
+ ? formatCurrencyAmount(quoteState.quote.input.amount.raw, quoteState.quote.input.currency.decimals)
253
+ : inputAmount;
254
+ const displayedOutputAmount = activeSide === "input" && quoteState.quote
255
+ ? formatCurrencyAmount(quoteState.quote.output.amount.raw, quoteState.quote.output.currency.decimals)
256
+ : outputAmount;
258
257
  const sourceBalance = sourceToken?.balance;
259
- const insufficientBalance = sourceBalance !== undefined && Number(displayedInputAmount) > sourceBalance;
258
+ const insufficientBalance = sourceBalance !== undefined && Number(displayedInputAmount) > Number(sourceBalance);
260
259
  const sameSwap = isSameSwap(sourceToken?.currency, destinationToken?.currency, inputAccount?.address, outputAddress);
261
260
  const canExecute = Boolean(quoteState.quote &&
262
261
  quoteRequest &&
@@ -300,13 +299,15 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
300
299
  swapState.setAmount("input", value);
301
300
  }, onSelectToken: () => setSourcePickerOpen(true) }), _jsx(SwitchButton, { type: "button", "aria-label": "Swap tokens", onClick: reverseTokens, disabled: !sourceToken || !destinationToken, children: _jsx(ArrowDownIcon, { size: 24 }) })] }), _jsx(CurrencyPanel, { side: "output", token: destinationToken, amount: displayedOutputAmount, quoteUsd: quoteState.quote?.output.amount.value_usd, quoteLoading: quoteState.loading && activeSide === "input", disabled: execution.stage.kind !== "idle", onAmountChange: (value) => {
302
301
  swapState.setAmount("output", value);
303
- }, onSelectToken: () => setDestinationPickerOpen(true), footerAction: showOutputWalletSelector ? (_jsx(OutputWalletSelector, { label: outputAddress ? truncateAddress(outputAddress) : "Connect Wallet", open: outputWalletMenuOpen, onOpenChange: setOutputWalletMenuOpen, onConnect: () => {
304
- setOutputWalletMenuOpen(false);
305
- setConnectTarget({ open: true, chainType: destinationChainType });
306
- }, onPaste: () => {
302
+ }, onSelectToken: () => setDestinationPickerOpen(true), footerAction: showOutputWalletSelector ? (_jsx(OutputWalletSelector, { label: outputAddress ? truncateAddress(outputAddress) : "Connect Wallet", open: outputWalletMenuOpen, onOpenChange: setOutputWalletMenuOpen, onConnect: separateOutputWallet
303
+ ? () => {
304
+ setOutputWalletMenuOpen(false);
305
+ setConnectTarget({ open: true, chainType: destinationChainType });
306
+ }
307
+ : undefined, onPaste: () => {
307
308
  setOutputWalletMenuOpen(false);
308
309
  setManualWalletOpen(true);
309
- } })) : undefined }), quoteState.error && _jsx(ErrorBox, { children: quoteState.error }), tokenError && _jsx(ErrorBox, { children: tokenError }), insufficientBalance && _jsxs(ErrorBox, { children: ["Insufficient ", sourceToken?.currency.symbol, " balance"] }), sameSwap && _jsx(ErrorBox, { children: "Select a different token or destination wallet" }), (quoteState.loading || quoteState.quote) && (_jsxs(BreakdownCard, { children: [_jsxs(BreakdownToggle, { type: "button", "aria-expanded": breakdownOpen, onClick: () => setBreakdownOpen((open) => !open), children: [_jsx("span", { children: quoteState.loading ? "Finding the best route…" : quoteSummary }), _jsxs(BreakdownMeta, { children: [quoteState.quote && (_jsxs(NetworkCost, { children: [_jsx(FuelIcon, {}), _jsx("span", { children: formatUsd(quoteState.quote.input.breakdown?.gas?.value_usd) })] })), _jsx(ChevronDownIcon, { size: 16, up: breakdownOpen })] })] }), breakdownOpen && (_jsxs(BreakdownBody, { children: [quoteState.quote?.output.amount_out_minimum && (_jsxs(DetailRow, { children: [_jsx("span", { children: "Receive at least" }), _jsxs("strong", { children: [quoteState.quote.output.amount_out_minimum.ui, " ", quoteState.quote.output.currency.symbol] })] })), _jsxs(DetailRow, { children: [_jsx("span", { children: "Price impact" }), _jsx("strong", { children: quoteState.quote ? formatPriceImpact(quoteState.quote.price_impact) : "—" })] }), _jsxs(DetailRow, { children: [_jsx("span", { children: "Network cost" }), _jsx("strong", { children: formatUsd(quoteState.quote?.input.breakdown?.gas?.value_usd) })] }), _jsxs(DetailRow, { children: [_jsx("span", { children: "Total fees" }), quoteState.quote?.input.breakdown?.fees?.total_fee.value_usd === 0 ? (_jsx(FreeFeeBadge, { children: "Free" })) : (_jsx("strong", { children: formatUsd(quoteState.quote?.input.breakdown?.fees?.total_fee.value_usd) }))] })] }))] })), walletRequirement.target !== "none" ? (_jsx(WalletRequirementButton, { type: "button", onClick: () => setConnectTarget({ open: true, chainType: walletRequirement.chainType }), children: walletRequirement.label })) : (_jsxs(PrimaryButton, { type: "button", disabled: !canExecute || quoteState.loading, onClick: () => void executeSwap(), children: [quoteState.loading ? _jsx(Spinner, {}) : null, isLoading ? "Loading tokens…" : quoteState.loading ? "Fetching quote…" : "Swap"] })), _jsx(TokenPicker, { open: sourcePickerOpen, title: "Select Token", tokens: tokens, selectedId: resolvedSourceId, onClose: () => setSourcePickerOpen(false), onSelect: (token) => selectToken("input", token) }), _jsx(TokenPicker, { open: destinationPickerOpen, title: "Select Token", tokens: tokens, selectedId: resolvedDestinationId, onClose: () => setDestinationPickerOpen(false), onSelect: (token) => selectToken("output", token) }), _jsx(WalletConnector, { open: connectTarget.open, chainType: connectTarget.chainType, onClose: () => setConnectTarget({ open: false }) }), manualWalletOpen && (_jsx(ManualWalletModal, { open: true, chainId: destinationToken?.currency.chain_id, initialValue: manualAddress, onClose: () => setManualWalletOpen(false), onSave: (address) => {
310
+ } })) : undefined }), quoteState.error && _jsx(ErrorBox, { children: quoteState.error }), tokenError && _jsx(ErrorBox, { children: tokenError }), insufficientBalance && _jsxs(ErrorBox, { children: ["Insufficient ", sourceToken?.currency.symbol, " balance"] }), sameSwap && _jsx(ErrorBox, { children: "Select a different token or destination wallet" }), (quoteState.loading || quoteState.quote) && (_jsxs(BreakdownCard, { children: [_jsxs(BreakdownToggle, { type: "button", "aria-expanded": breakdownOpen, onClick: () => setBreakdownOpen((open) => !open), children: [_jsx("span", { children: quoteState.loading ? "Finding the best route…" : quoteSummary }), _jsxs(BreakdownMeta, { children: [quoteState.quote && (_jsxs(NetworkCost, { children: [_jsx(FuelIcon, {}), _jsx("span", { children: formatUsd(quoteState.quote.input.breakdown?.gas?.value_usd) })] })), _jsx(ChevronDownIcon, { size: 16, up: breakdownOpen })] })] }), breakdownOpen && (_jsxs(BreakdownBody, { children: [quoteState.quote?.output.amount_out_minimum && (_jsxs(DetailRow, { children: [_jsx("span", { children: "Receive at least" }), _jsxs("strong", { children: [quoteState.quote.output.amount_out_minimum.ui, " ", quoteState.quote.output.currency.symbol] })] })), _jsxs(DetailRow, { children: [_jsx("span", { children: "Price impact" }), _jsx("strong", { children: quoteState.quote ? formatPriceImpact(quoteState.quote.price_impact) : "—" })] }), _jsxs(DetailRow, { children: [_jsx("span", { children: "Network cost" }), _jsx("strong", { children: formatUsd(quoteState.quote?.input.breakdown?.gas?.value_usd) })] }), _jsxs(DetailRow, { children: [_jsx("span", { children: "Total fees" }), quoteState.quote?.input.breakdown?.fees?.total_fee.value_usd === 0 ? (_jsx(FreeFeeBadge, { children: "Free" })) : (_jsx("strong", { children: formatUsd(quoteState.quote?.input.breakdown?.fees?.total_fee.value_usd) }))] })] }))] })), walletRequirement.target !== "none" ? (_jsx(WalletRequirementButton, { type: "button", onClick: () => setConnectTarget({ open: true, chainType: walletRequirement.chainType }), children: walletRequirement.label })) : (_jsxs(PrimaryButton, { type: "button", disabled: !canExecute || quoteState.loading, onClick: () => void executeSwap(), children: [quoteState.loading ? _jsx(Spinner, {}) : null, isLoading ? "Loading tokens…" : quoteState.loading ? "Fetching quote…" : "Swap"] })), _jsx(TokenPicker, { open: sourcePickerOpen, title: "Select Token", tokens: tokens, selectedId: resolvedSourceId, onClose: () => setSourcePickerOpen(false), onSelect: (token) => selectToken("input", token) }), _jsx(TokenPicker, { open: destinationPickerOpen, title: "Select Token", tokens: tokens, selectedId: resolvedDestinationId, preferredChainId: sourceToken?.currency.chain_id, onClose: () => setDestinationPickerOpen(false), onSelect: (token) => selectToken("output", token) }), _jsx(WalletConnector, { open: connectTarget.open, chainType: connectTarget.chainType, onClose: () => setConnectTarget({ open: false }) }), manualWalletOpen && (_jsx(ManualWalletModal, { open: true, chainId: destinationToken?.currency.chain_id, initialValue: manualAddress, onClose: () => setManualWalletOpen(false), onSave: (address) => {
310
311
  if (destinationToken)
311
312
  swapState.setManualOutputWallet({ chainId: destinationToken.currency.chain_id, address });
312
313
  } })), _jsx(ExecutionModal, { stage: execution.stage, onClose: () => {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment jsdom
3
+ import { ChainId } from "@coin-voyage/shared/types";
4
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { Swap } from "./swap";
7
+ vi.mock("@coin-voyage/crypto/hooks", () => ({ useAccount: () => ({ accounts: [] }) }));
8
+ vi.mock("../providers/swapkit-context", () => ({ useSwapKit: () => ({ mode: "light", theme: {} }) }));
9
+ vi.mock("../hooks/use-swap-execution", () => ({
10
+ useSwapExecution: () => ({ stage: { kind: "idle" }, execute: vi.fn(), reset: vi.fn() }),
11
+ }));
12
+ vi.mock("../hooks/use-token-data", () => ({
13
+ useTokenData: () => ({
14
+ tokens: [{ currency: source, balance: "2.345678901234567891" }, { currency: destination }],
15
+ accounts: [],
16
+ isLoading: false,
17
+ refreshBalances: vi.fn(),
18
+ }),
19
+ }));
20
+ vi.mock("../hooks/use-swap-quote", () => ({
21
+ useSwapQuote: () => ({
22
+ quote: {
23
+ input: { currency: source, amount: { raw: "1234567890123456789", ui: "1.23" } },
24
+ output: { currency: destination, amount: { raw: "9876543", ui: "9.88" } },
25
+ price_impact: 0,
26
+ },
27
+ loading: false,
28
+ }),
29
+ }));
30
+ vi.mock("./connected-wallets", () => ({ ConnectedWallets: () => null }));
31
+ vi.mock("./token-picker", () => ({ TokenPicker: () => null }));
32
+ vi.mock("./wallet-connector", () => ({ WalletConnector: () => null }));
33
+ vi.mock("./execution-modal", () => ({ ExecutionModal: () => null }));
34
+ const source = {
35
+ id: "eth",
36
+ chain_id: ChainId.ETH,
37
+ name: "Ether",
38
+ symbol: "ETH",
39
+ decimals: 18,
40
+ };
41
+ const destination = {
42
+ id: "usdc",
43
+ chain_id: ChainId.ETH,
44
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
45
+ name: "USD Coin",
46
+ symbol: "USDC",
47
+ decimals: 6,
48
+ };
49
+ afterEach(cleanup);
50
+ function renderSwap() {
51
+ render(_jsx(Swap, { defaultSourceCurrency: source, defaultDestinationCurrency: destination }));
52
+ return {
53
+ sell: screen.getByRole("textbox", { name: "Sell amount" }),
54
+ buy: screen.getByRole("textbox", { name: "Buy amount" }),
55
+ };
56
+ }
57
+ describe("Swap amount precision", () => {
58
+ it("shows the exact quoted buy amount while preserving the typed sell amount", () => {
59
+ const { sell, buy } = renderSwap();
60
+ fireEvent.change(sell, { target: { value: "1.234567890123456780" } });
61
+ expect(sell.value).toBe("1.234567890123456780");
62
+ expect(buy.value).toBe("9.876543");
63
+ });
64
+ it("shows the exact quoted sell amount while preserving the typed buy amount", () => {
65
+ const { sell, buy } = renderSwap();
66
+ fireEvent.change(buy, { target: { value: "9.876540" } });
67
+ expect(buy.value).toBe("9.876540");
68
+ expect(sell.value).toBe("1.234567890123456789");
69
+ });
70
+ it("fills the full balance with MAX after editing the buy amount", () => {
71
+ const { sell, buy } = renderSwap();
72
+ fireEvent.change(buy, { target: { value: "9.876543" } });
73
+ fireEvent.click(screen.getByRole("button", { name: "Use maximum ETH balance" }));
74
+ expect(sell.value).toBe("2.345678901234567891");
75
+ });
76
+ });
@@ -1,11 +1,13 @@
1
+ import { ChainId } from "@coin-voyage/shared/types";
1
2
  import type { SwapToken } from "../hooks/use-token-data";
2
3
  interface TokenPickerProps {
3
4
  open: boolean;
4
5
  title: string;
5
6
  tokens: SwapToken[];
6
7
  selectedId?: string;
8
+ preferredChainId?: ChainId;
7
9
  onClose: () => void;
8
10
  onSelect: (token: SwapToken) => void;
9
11
  }
10
- export declare function TokenPicker({ open, title, tokens, selectedId, onClose, onSelect }: TokenPickerProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function TokenPicker({ open, title, tokens, selectedId, preferredChainId, onClose, onSelect, }: TokenPickerProps): import("react/jsx-runtime").JSX.Element;
11
13
  export {};