@coin-voyage/swapkit 0.3.0 → 0.4.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 (38) hide show
  1. package/LICENSE +8 -8
  2. package/README.md +68 -68
  3. package/dist/components/execution-modal.d.ts +1 -1
  4. package/dist/components/execution-modal.js +6 -2
  5. package/dist/components/styles.d.ts +1 -1
  6. package/dist/components/styles.js +1282 -1281
  7. package/dist/components/swap-recipient.test.d.ts +1 -0
  8. package/dist/components/swap-recipient.test.js +136 -0
  9. package/dist/components/swap.d.ts +3 -3
  10. package/dist/components/swap.js +35 -23
  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.js +1 -1
  14. package/dist/components/wallet-connector.d.ts +1 -1
  15. package/dist/components/wallet-connector.js +24 -4
  16. package/dist/components/wallet-connector.test.js +55 -1
  17. package/dist/hooks/use-order-status.d.ts +1 -1
  18. package/dist/hooks/use-token-data.d.ts +3 -2
  19. package/dist/hooks/use-token-data.js +41 -12
  20. package/dist/hooks/use-token-data.test.d.ts +1 -0
  21. package/dist/hooks/use-token-data.test.js +81 -0
  22. package/dist/lib/amount-input.d.ts +1 -0
  23. package/dist/lib/amount-input.js +7 -0
  24. package/dist/lib/amount-input.test.d.ts +1 -0
  25. package/dist/lib/amount-input.test.js +17 -0
  26. package/dist/lib/execution.d.ts +2 -0
  27. package/dist/lib/execution.js +6 -0
  28. package/dist/lib/execution.test.d.ts +1 -0
  29. package/dist/lib/execution.test.js +24 -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.js +1 -1
  36. package/dist/styles/root.js +59 -59
  37. package/dist/styles/theme.js +1 -1
  38. package/package.json +8 -8
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,136 @@
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: [
16
+ { currency: source, balance: "2" },
17
+ { currency: destination, balance: "7.5" },
18
+ { currency: unheld },
19
+ { currency: solana },
20
+ ],
21
+ accounts,
22
+ isLoading: false,
23
+ balancesLoaded: true,
24
+ refreshBalances: vi.fn(),
25
+ }),
26
+ }));
27
+ vi.mock("../hooks/use-swap-quote", () => ({
28
+ useSwapQuote: () => ({
29
+ quote: {
30
+ input: { currency: source, amount: { raw: "1000000000000000000", ui: "1" } },
31
+ output: { currency: destination, amount: { raw: "1000000", ui: "1" } },
32
+ price_impact: 0,
33
+ },
34
+ loading: false,
35
+ }),
36
+ }));
37
+ vi.mock("./connected-wallets", () => ({ ConnectedWallets: () => null }));
38
+ vi.mock("./token-picker", () => ({ TokenPicker: () => null }));
39
+ vi.mock("./wallet-connector", () => ({ WalletConnector: () => null }));
40
+ vi.mock("./execution-modal", () => ({ ExecutionModal: () => null }));
41
+ const sender = "0x1111111111111111111111111111111111111111";
42
+ const recipient = "0x2222222222222222222222222222222222222222";
43
+ const accounts = [{ address: sender, chainType: ChainType.EVM }];
44
+ const source = {
45
+ id: "eth",
46
+ chain_id: ChainId.ETH,
47
+ name: "Ether",
48
+ symbol: "ETH",
49
+ decimals: 18,
50
+ };
51
+ const destination = {
52
+ id: "usdc",
53
+ chain_id: ChainId.ETH,
54
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
55
+ name: "USD Coin",
56
+ symbol: "USDC",
57
+ decimals: 6,
58
+ };
59
+ const unheld = {
60
+ id: "dai",
61
+ chain_id: ChainId.ETH,
62
+ address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
63
+ name: "Dai",
64
+ symbol: "DAI",
65
+ decimals: 18,
66
+ };
67
+ const solana = {
68
+ id: "sol",
69
+ chain_id: ChainId.SOL,
70
+ name: "Solana",
71
+ symbol: "SOL",
72
+ decimals: 9,
73
+ };
74
+ beforeEach(() => execute.mockClear());
75
+ afterEach(cleanup);
76
+ function renderSwap(output = destination) {
77
+ render(_jsx(Swap, { defaultSourceCurrency: source, defaultDestinationCurrency: output }));
78
+ fireEvent.change(screen.getByRole("textbox", { name: "Sell amount" }), { target: { value: "1" } });
79
+ }
80
+ function openRecipientModal() {
81
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
82
+ fireEvent.click(screen.getByRole("menuitem", { name: "Paste Address" }));
83
+ }
84
+ describe("Swap destination recipient", () => {
85
+ it("offers only a pasted address for swaps on the same chain", () => {
86
+ renderSwap();
87
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
88
+ expect(screen.getByRole("menuitem", { name: "Paste Address" })).toBeTruthy();
89
+ expect(screen.queryByRole("menuitem", { name: "Connect Wallet" })).toBeNull();
90
+ expect(screen.getAllByRole("menuitem")).toHaveLength(1);
91
+ });
92
+ it("executes a same-chain swap to the pasted recipient", () => {
93
+ renderSwap();
94
+ openRecipientModal();
95
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: recipient } });
96
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
97
+ expect(screen.queryByRole("dialog", { name: "Paste destination address" })).toBeNull();
98
+ fireEvent.click(screen.getByRole("button", { name: "Swap" }));
99
+ expect(execute).toHaveBeenCalledWith(expect.objectContaining({
100
+ source: expect.objectContaining({ sender_address: sender }),
101
+ destination: expect.objectContaining({ recipient }),
102
+ }), sender);
103
+ });
104
+ it("shows the buy token balance until a recipient is pasted", () => {
105
+ renderSwap();
106
+ expect(screen.getByText("Balance: 7.5")).toBeTruthy();
107
+ openRecipientModal();
108
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: recipient } });
109
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
110
+ expect(screen.queryByText("Balance: 7.5")).toBeNull();
111
+ expect(screen.getByText("Balance: 2")).toBeTruthy();
112
+ });
113
+ it("shows a zero buy balance for a token the connected wallet does not hold", () => {
114
+ renderSwap(unheld);
115
+ expect(screen.getByText("Balance: 0")).toBeTruthy();
116
+ });
117
+ it("shows no buy balance when no wallet is connected for the destination chain", () => {
118
+ renderSwap(solana);
119
+ expect(screen.queryByText("Balance: 0")).toBeNull();
120
+ });
121
+ it("rejects invalid same-chain destination addresses", () => {
122
+ renderSwap();
123
+ openRecipientModal();
124
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: "invalid" } });
125
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
126
+ expect(screen.getByText("Enter a valid address for the destination network")).toBeTruthy();
127
+ expect(screen.getByRole("dialog", { name: "Paste destination address" })).toBeTruthy();
128
+ expect(execute).not.toHaveBeenCalled();
129
+ });
130
+ it("keeps both connect and paste options for cross-family swaps", () => {
131
+ renderSwap(solana);
132
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
133
+ expect(screen.getByRole("menuitem", { name: "Connect Wallet" })).toBeTruthy();
134
+ expect(screen.getByRole("menuitem", { name: "Paste Address" })).toBeTruthy();
135
+ });
136
+ });
@@ -15,14 +15,14 @@ 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;
22
22
  onAmountChange: (value: string) => void;
23
23
  onSelectToken: () => void;
24
- footerAction?: ReactNode;
24
+ headerAction?: ReactNode;
25
25
  }
26
- export declare function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken, footerAction, }: CurrencyPanelProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken, headerAction, }: CurrencyPanelProps): import("react/jsx-runtime").JSX.Element;
27
27
  export declare function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaultSlippageBps, onComplete, onError, }?: SwapProps): import("react/jsx-runtime").JSX.Element;
28
28
  export {};
@@ -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";
@@ -16,7 +17,7 @@ import { ExecutionModal } from "./execution-modal";
16
17
  import { ConnectedWallets } from "./connected-wallets";
17
18
  import { ArrowDownIcon, ChevronDownIcon, FuelIcon, InfoIcon, PasteIcon, SettingsIcon, WalletIcon } from "./icons";
18
19
  import { Modal } from "./modal";
19
- import { AmountInput, AmountRow, AmountSkeleton, BreakdownBody, BreakdownCard, BreakdownMeta, BreakdownToggle, CardFooter, CardTitle, CardWrap, CompactInput, CurrencyCard, DetailRow, ErrorBox, FieldLabel, FormStack, FreeFeeBadge, Hint, IconButton, InlineActions, InfoButton, InfoWrap, NetworkCost, OutputWalletControl, OutputWalletMenu, OutputWalletOption, OutputWalletTrigger, PrimaryButton, QuoteValueSkeleton, QuickAmountActions, QuickAmountButton, SettingsLabel, SettingsPanel, SettingsRow, SettingsTooltip, SlippageControl, SlippageInput, SlippageInputWrap, SlippageModeButton, Spinner, SwitchButton, TokenButton, Toolbar, ToolbarGroup, WalletRequirementButton, } from "./styles";
20
+ import { AmountInput, AmountRow, AmountSkeleton, BreakdownBody, BreakdownCard, BreakdownMeta, BreakdownToggle, CardFooter, CardHeaderAction, CardTitle, CardWrap, CompactInput, CurrencyCard, DetailRow, ErrorBox, FieldLabel, FormStack, FreeFeeBadge, Hint, IconButton, InfoButton, InfoWrap, NetworkCost, OutputWalletControl, OutputWalletMenu, OutputWalletOption, OutputWalletTrigger, PrimaryButton, QuoteValueSkeleton, QuickAmountActions, QuickAmountButton, SettingsLabel, SettingsPanel, SettingsRow, SettingsTooltip, SlippageControl, SlippageInput, SlippageInputWrap, SlippageModeButton, Spinner, SwitchButton, TokenButton, Toolbar, ToolbarGroup, WalletRequirementButton, } from "./styles";
20
21
  import { TokenIcon } from "./token-icon";
21
22
  import { TokenPicker } from "./token-picker";
22
23
  import { WalletConnector } from "./wallet-connector";
@@ -80,6 +81,13 @@ export function useSwapNotifications({ stage, quoteError, tokenError, onBalances
80
81
  onCompleteRef.current?.({ orderId: stage.orderId, transactionHash });
81
82
  }
82
83
  }
84
+ else if (stage.kind === "refunded") {
85
+ const key = `refunded:${stage.orderId}`;
86
+ if (lastNotification.current !== key) {
87
+ lastNotification.current = key;
88
+ onBalancesChangedRef.current();
89
+ }
90
+ }
83
91
  else if (stage.kind === "error") {
84
92
  const key = `error:${stage.message}`;
85
93
  if (lastNotification.current !== key) {
@@ -119,7 +127,7 @@ function OutputWalletSelector({ label, open, onOpenChange, onConnect, onPaste, }
119
127
  document.removeEventListener("keydown", dismissOnEscape);
120
128
  };
121
129
  }, [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, {})] })] }))] }));
130
+ 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
131
  }
124
132
  function formatUsd(value) {
125
133
  if (value === undefined || value === null || !Number.isFinite(value))
@@ -143,26 +151,21 @@ const QUICK_AMOUNTS = [
143
151
  { label: "75%", fraction: 0.75 },
144
152
  { label: "Max", fraction: 1 },
145
153
  ];
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
- export function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken, footerAction, }) {
154
+ export function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken, headerAction, }) {
152
155
  const label = side === "input" ? "SELL" : "BUY";
153
156
  const showQuickAmounts = side === "input" && balance !== undefined && token !== undefined;
154
- return (_jsxs(CurrencyCard, { children: [_jsx(CardTitle, { children: label }), showQuickAmounts && (_jsx(QuickAmountActions, { "data-quick-amounts": true, children: QUICK_AMOUNTS.map(({ label: amountLabel, fraction }) => (_jsx(QuickAmountButton, { type: "button", disabled: disabled, "aria-label": fraction === 1
157
+ return (_jsxs(CurrencyCard, { children: [_jsx(CardTitle, { children: label }), headerAction && _jsx(CardHeaderAction, { children: headerAction }), showQuickAmounts && (_jsx(QuickAmountActions, { "data-quick-amounts": true, children: QUICK_AMOUNTS.map(({ label: amountLabel, fraction }) => (_jsx(QuickAmountButton, { type: "button", disabled: disabled, "aria-label": fraction === 1
155
158
  ? `Use maximum ${token.currency.symbol} balance`
156
159
  : `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
160
  if (isValidSwapAmountInput(event.target.value))
158
161
  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] })] })] }));
162
+ } })), _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)) })), balance !== undefined && _jsxs("span", { children: ["Balance: ", Number(balance).toLocaleString()] })] })] }));
160
163
  }
161
164
  function ManualWalletModal({ open, chainId, initialValue, onClose, onSave, }) {
162
165
  const [value, setValue] = useState(initialValue ?? "");
163
166
  const [error, setError] = useState(null);
164
167
  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: () => {
168
+ 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
169
  if (!chainId || !isValidAddress(value, chainId)) {
167
170
  setError("Enter a valid address for the destination network");
168
171
  return;
@@ -176,7 +179,7 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
176
179
  const { mode, theme } = useSwapKit();
177
180
  const slippageInputId = useId();
178
181
  const slippageTooltipId = useId();
179
- const { tokens, accounts, isLoading, error: tokenError, refreshBalances } = useTokenData();
182
+ const { tokens, accounts, isLoading, error: tokenError, balancesLoaded, refreshBalances } = useTokenData();
180
183
  const { accounts: connectedAccounts } = useAccount();
181
184
  const [sourcePickerOpen, setSourcePickerOpen] = useState(false);
182
185
  const [destinationPickerOpen, setDestinationPickerOpen] = useState(false);
@@ -206,7 +209,7 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
206
209
  const manualAddress = manualOutputWallet && manualOutputWallet.chainId === destinationToken?.currency.chain_id
207
210
  ? manualOutputWallet.address
208
211
  : undefined;
209
- const outputAddress = separateOutputWallet ? (manualAddress ?? outputAccount?.address) : inputAccount?.address;
212
+ const outputAddress = manualAddress ?? (separateOutputWallet ? outputAccount?.address : inputAccount?.address);
210
213
  const walletRequirement = getSwapWalletRequirement({
211
214
  inputWalletAddress: inputAccount?.address,
212
215
  inputChainType: sourceChainType,
@@ -215,7 +218,6 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
215
218
  outputChainType: destinationChainType,
216
219
  });
217
220
  const showOutputWalletSelector = shouldShowOutputWalletSelector({
218
- requiresSeparateOutputWallet: separateOutputWallet,
219
221
  outputChainType: destinationChainType,
220
222
  connectionRequirement: walletRequirement,
221
223
  });
@@ -253,10 +255,18 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
253
255
  onComplete,
254
256
  onError,
255
257
  });
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;
258
+ const displayedInputAmount = activeSide === "output" && quoteState.quote
259
+ ? formatCurrencyAmount(quoteState.quote.input.amount.raw, quoteState.quote.input.currency.decimals)
260
+ : inputAmount;
261
+ const displayedOutputAmount = activeSide === "input" && quoteState.quote
262
+ ? formatCurrencyAmount(quoteState.quote.output.amount.raw, quoteState.quote.output.currency.decimals)
263
+ : outputAmount;
258
264
  const sourceBalance = sourceToken?.balance;
259
- const insufficientBalance = sourceBalance !== undefined && Number(displayedInputAmount) > sourceBalance;
265
+ // A pasted recipient is not one of the connected wallets, so its balance is unknown.
266
+ const destinationBalance = manualAddress
267
+ ? undefined
268
+ : (destinationToken?.balance ?? (balancesLoaded && outputAccount?.address ? "0" : undefined));
269
+ const insufficientBalance = sourceBalance !== undefined && Number(displayedInputAmount) > Number(sourceBalance);
260
270
  const sameSwap = isSameSwap(sourceToken?.currency, destinationToken?.currency, inputAccount?.address, outputAddress);
261
271
  const canExecute = Boolean(quoteState.quote &&
262
272
  quoteRequest &&
@@ -298,12 +308,14 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
298
308
  } }), _jsx("span", { children: "%" })] })] })] }), slippageMode === "custom" &&
299
309
  (slippage >= 40 ? (_jsx(Hint, { "$tone": "danger", children: "This slippage tolerance may cause a severely unfavorable trade." })) : slippage >= 6 ? (_jsx(Hint, { "$tone": "warning", children: "High slippage can result in a worse execution price." })) : null)] }))] })] }), _jsxs(CardWrap, { children: [_jsx(CurrencyPanel, { side: "input", token: sourceToken, amount: displayedInputAmount, balance: sourceBalance, quoteUsd: quoteState.quote?.input.amount.value_usd, quoteLoading: quoteState.loading && activeSide === "output", disabled: execution.stage.kind !== "idle", onAmountChange: (value) => {
300
310
  swapState.setAmount("input", value);
301
- }, 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) => {
311
+ }, 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, balance: destinationBalance, quoteUsd: quoteState.quote?.output.amount.value_usd, quoteLoading: quoteState.loading && activeSide === "input", disabled: execution.stage.kind !== "idle", onAmountChange: (value) => {
302
312
  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: () => {
313
+ }, onSelectToken: () => setDestinationPickerOpen(true), headerAction: showOutputWalletSelector ? (_jsx(OutputWalletSelector, { label: outputAddress ? truncateAddress(outputAddress) : "Connect Wallet", open: outputWalletMenuOpen, onOpenChange: setOutputWalletMenuOpen, onConnect: separateOutputWallet
314
+ ? () => {
315
+ setOutputWalletMenuOpen(false);
316
+ setConnectTarget({ open: true, chainType: destinationChainType });
317
+ }
318
+ : undefined, onPaste: () => {
307
319
  setOutputWalletMenuOpen(false);
308
320
  setManualWalletOpen(true);
309
321
  } })) : 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) => {
@@ -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
+ });
@@ -74,6 +74,6 @@ export function TokenPicker({ open, title, tokens, selectedId, preferredChainId,
74
74
  return (_jsxs(TokenOption, { type: "button", role: "option", "aria-label": `${token.currency.symbol} ${token.currency.name}`, "aria-selected": id === selectedId, onClick: () => {
75
75
  onSelect(token);
76
76
  closePicker();
77
- }, children: [_jsx(TokenIcon, { currency: token.currency, size: 40 }), _jsxs(TokenMeta, { children: [_jsx("strong", { children: token.currency.name }), _jsx("small", { children: tokenDescription(token) })] }), _jsxs(TokenBalance, { children: [_jsx("strong", { children: formatUsd(token.balanceUsd) }), _jsx("small", { children: token.balance === undefined ? "" : token.balance.toLocaleString() })] })] }, id));
77
+ }, children: [_jsx(TokenIcon, { currency: token.currency, size: 40 }), _jsxs(TokenMeta, { children: [_jsx("strong", { children: token.currency.name }), _jsx("small", { children: tokenDescription(token) })] }), _jsxs(TokenBalance, { children: [_jsx("strong", { children: formatUsd(token.balanceUsd) }), _jsx("small", { children: token.balance === undefined ? "" : Number(token.balance).toLocaleString() })] })] }, id));
78
78
  }), filtered.length === 0 && _jsx(EmptyState, { children: "No matching tokens" })] })] }) }));
79
79
  }
@@ -1,4 +1,4 @@
1
- import type { ChainType } from "@coin-voyage/shared/types";
1
+ import { ChainType } from "@coin-voyage/shared/types";
2
2
  interface WalletConnectorProps {
3
3
  open: boolean;
4
4
  chainType?: ChainType;
@@ -1,11 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createWalletConnectModalConnector } from "@coin-voyage/crypto/connectors";
2
3
  import { useInstalledWallets, useUniversalConnect } from "@coin-voyage/crypto/hooks";
3
- import { getErrorMessage, isWalletConnectionCancelledError } from "@coin-voyage/crypto/utils";
4
+ import { getConnectorId, getErrorMessage, isWalletConnectionCancelledError } from "@coin-voyage/crypto/utils";
4
5
  import { getChainTypeName } from "@coin-voyage/shared/chain";
6
+ import { ChainType } from "@coin-voyage/shared/types";
5
7
  import { useMemo, useRef, useState } from "react";
6
8
  import { EmptyState, Hint, MultichainBadge, WalletList, WalletOption, WalletOptionMeta, WalletSwitchButton, } from "./styles";
7
9
  import { Modal } from "./modal";
8
10
  import { WalletIconDisplay } from "./wallet-icon";
11
+ const getWalletDisplayName = (wallet) => wallet.id.toLowerCase() === "walletconnect" ? "Mobile Wallets" : wallet.name;
12
+ const addWalletConnectModalStyle = () => {
13
+ const style = document.createElement("style");
14
+ style.textContent = "w3m-modal, wcm-modal { --wcm-z-index: 2147483647; --w3m-z-index: 2147483647; }";
15
+ document.head.appendChild(style);
16
+ return style;
17
+ };
9
18
  export function WalletConnector(props) {
10
19
  if (!props.open)
11
20
  return null;
@@ -42,14 +51,23 @@ function OpenWalletConnector({ chainType, intent = "connect", beforeConnect, onC
42
51
  pendingRef.current = true;
43
52
  setPending(true);
44
53
  setError(null);
54
+ let walletConnectModalStyle;
45
55
  try {
46
56
  await beforeConnect?.();
47
- await connect({ walletConnector: connector });
57
+ const connectorId = getConnectorId(connector.connector);
58
+ const isWalletConnect = connectorId?.toLowerCase().includes("walletconnect");
59
+ if (isWalletConnect)
60
+ walletConnectModalStyle = addWalletConnectModalStyle();
61
+ const walletConnector = connector.chainType === ChainType.EVM && isWalletConnect
62
+ ? { ...connector, connector: await createWalletConnectModalConnector(connector.connector) }
63
+ : connector;
64
+ await connect({ walletConnector });
48
65
  }
49
66
  catch (error) {
50
67
  showError(error);
51
68
  }
52
69
  finally {
70
+ walletConnectModalStyle?.remove();
53
71
  const shouldUpdatePending = pendingRef.current;
54
72
  pendingRef.current = false;
55
73
  if (shouldUpdatePending)
@@ -69,14 +87,16 @@ function OpenWalletConnector({ chainType, intent = "connect", beforeConnect, onC
69
87
  return (_jsxs(Modal, { open: true, title: modalTitle, onClose: close, closeDisabled: pending, children: [selectedWallet && (_jsx(WalletSwitchButton, { type: "button", disabled: pending, onClick: () => setSelectedWallet(null), children: "Back" })), _jsx(WalletList, { "aria-busy": pending, children: selectedWallet
70
88
  ? selectedConnectors.map((connector) => {
71
89
  const chainName = getChainTypeName(connector.chainType);
72
- return (_jsxs(WalletOption, { type: "button", disabled: pending, "aria-label": `Connect ${selectedWallet.name} on ${chainName}`, onClick: () => void handleConnect(connector), children: [_jsx(WalletIconDisplay, { icon: selectedWallet.icon, name: chainName }), _jsxs(WalletOptionMeta, { children: [_jsx("span", { children: _jsx("strong", { children: chainName }) }), _jsxs("small", { children: [chainName, " network"] })] })] }, connector.chainType));
90
+ const walletName = getWalletDisplayName(selectedWallet);
91
+ return (_jsxs(WalletOption, { type: "button", disabled: pending, "aria-label": `Connect ${walletName} on ${chainName}`, onClick: () => void handleConnect(connector), children: [_jsx(WalletIconDisplay, { icon: selectedWallet.icon, name: chainName }), _jsxs(WalletOptionMeta, { children: [_jsx("span", { children: _jsx("strong", { children: chainName }) }), _jsxs("small", { children: [chainName, " network"] })] })] }, connector.chainType));
73
92
  })
74
93
  : wallets.map((wallet) => {
75
94
  const connector = wallet.connectors.find((candidate) => !chainType || candidate.chainType === chainType);
76
95
  if (!connector)
77
96
  return null;
97
+ const walletName = getWalletDisplayName(wallet);
78
98
  const chainTypes = [...new Set(wallet.connectors.map(({ chainType }) => chainType))];
79
99
  const capabilityLabel = chainTypes.map(getChainTypeName).join(" · ");
80
- return (_jsxs(WalletOption, { type: "button", disabled: pending, "aria-label": `${intent === "switch" ? "Switch to" : "Connect"} ${wallet.name} wallet`, onClick: () => chooseWallet(wallet), children: [_jsx(WalletIconDisplay, { icon: wallet.icon, name: wallet.name }), _jsxs(WalletOptionMeta, { children: [_jsxs("span", { children: [_jsx("strong", { children: wallet.name }), chainTypes.length > 1 && _jsx(MultichainBadge, { children: "Multichain" })] }), _jsx("small", { children: chainTypes.length > 1 ? capabilityLabel : `${capabilityLabel} wallet` })] })] }, wallet.id));
100
+ return (_jsxs(WalletOption, { type: "button", disabled: pending, "aria-label": `${intent === "switch" ? "Switch to" : "Connect"} ${walletName} wallet`, onClick: () => chooseWallet(wallet), children: [_jsx(WalletIconDisplay, { icon: wallet.icon, name: walletName }), _jsxs(WalletOptionMeta, { children: [_jsxs("span", { children: [_jsx("strong", { children: walletName }), chainTypes.length > 1 && _jsx(MultichainBadge, { children: "Multichain" })] }), _jsx("small", { children: chainTypes.length > 1 ? capabilityLabel : `${capabilityLabel} wallet` })] })] }, wallet.id));
81
101
  }) }), !selectedWallet && wallets.length === 0 && _jsx(EmptyState, { children: "No compatible installed wallets found" }), error && (_jsx(Hint, { "$tone": "danger", role: "alert", children: error }))] }));
82
102
  }
@@ -5,7 +5,9 @@ import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
5
5
  import userEvent from "@testing-library/user-event";
6
6
  import { afterEach, describe, expect, it, vi } from "vitest";
7
7
  import { WalletConnector } from "./wallet-connector";
8
+ import { Overlay } from "./styles";
8
9
  const connectWallet = vi.fn();
10
+ const createWalletConnectModalConnector = vi.fn();
9
11
  let connectErrorHandler;
10
12
  let installedWallets = [
11
13
  {
@@ -21,12 +23,16 @@ vi.mock("@coin-voyage/crypto/hooks", () => ({
21
23
  return { connect: connectWallet };
22
24
  },
23
25
  }));
26
+ vi.mock("@coin-voyage/crypto/connectors", () => ({
27
+ createWalletConnectModalConnector: (...args) => createWalletConnectModalConnector(...args),
28
+ }));
24
29
  vi.mock("./modal", () => ({
25
30
  Modal: ({ open, title, closeDisabled, children, }) => open ? (_jsx("div", { role: "dialog", "aria-label": title, "data-close-disabled": closeDisabled || undefined, children: children })) : null,
26
31
  }));
27
32
  afterEach(() => {
28
33
  cleanup();
29
- connectWallet.mockClear();
34
+ connectWallet.mockReset();
35
+ createWalletConnectModalConnector.mockReset();
30
36
  connectErrorHandler = undefined;
31
37
  installedWallets = [
32
38
  {
@@ -37,6 +43,10 @@ afterEach(() => {
37
43
  ];
38
44
  });
39
45
  describe("WalletConnector", () => {
46
+ it("keeps the wallet selector below the WalletConnect modal layer", () => {
47
+ render(_jsx(Overlay, { "data-testid": "wallet-selector-overlay" }));
48
+ expect(Number(getComputedStyle(screen.getByTestId("wallet-selector-overlay")).zIndex)).toBeLessThan(2147483647);
49
+ });
40
50
  it("uses switch language when replacing a connected wallet", () => {
41
51
  render(_jsx(WalletConnector, { open: true, chainType: ChainType.SOL, intent: "switch", onClose: vi.fn() }));
42
52
  expect(screen.getByRole("dialog", { name: "Switch wallet" })).not.toBeNull();
@@ -74,6 +84,50 @@ describe("WalletConnector", () => {
74
84
  await user.click(screen.getByRole("button", { name: "Connect Phantom on Solana" }));
75
85
  expect(connectWallet).toHaveBeenCalledWith({ walletConnector: solanaConnector });
76
86
  });
87
+ it("opens the WalletConnect modal before connecting an EVM wallet", async () => {
88
+ const user = userEvent.setup();
89
+ const headlessConnector = { id: "walletConnect", name: "WalletConnect" };
90
+ const modalConnector = { id: "walletConnect", name: "WalletConnect Modal" };
91
+ createWalletConnectModalConnector.mockResolvedValue(modalConnector);
92
+ installedWallets = [
93
+ {
94
+ id: "walletconnect",
95
+ name: "WalletConnect",
96
+ connectors: [{ chainType: ChainType.EVM, connector: headlessConnector }],
97
+ },
98
+ ];
99
+ render(_jsx(WalletConnector, { open: true, onClose: vi.fn() }));
100
+ expect(screen.getByText("Mobile Wallets")).not.toBeNull();
101
+ await user.click(screen.getByRole("button", { name: "Connect Mobile Wallets wallet" }));
102
+ expect(createWalletConnectModalConnector).toHaveBeenCalledWith(headlessConnector);
103
+ expect(connectWallet).toHaveBeenCalledWith({
104
+ walletConnector: { chainType: ChainType.EVM, connector: modalConnector },
105
+ });
106
+ expect([...document.head.querySelectorAll("style")].some((style) => style.textContent?.includes("--w3m-z-index: 2147483647"))).toBe(false);
107
+ });
108
+ it("keeps the Solana WalletConnect modal above the wallet selector and cleans up after failure", async () => {
109
+ const user = userEvent.setup();
110
+ const solanaConnector = { id: "WalletConnect", name: "WalletConnect" };
111
+ let failConnect;
112
+ connectWallet.mockImplementation(() => new Promise((_resolve, reject) => {
113
+ failConnect = () => reject(new Error("WalletConnect failed"));
114
+ }));
115
+ installedWallets = [
116
+ {
117
+ id: "walletconnect",
118
+ name: "WalletConnect",
119
+ connectors: [{ chainType: ChainType.SOL, connector: solanaConnector }],
120
+ },
121
+ ];
122
+ render(_jsx(WalletConnector, { open: true, chainType: ChainType.SOL, onClose: vi.fn() }));
123
+ await user.click(screen.getByRole("button", { name: "Connect Mobile Wallets wallet" }));
124
+ expect(connectWallet).toHaveBeenCalledWith({
125
+ walletConnector: { chainType: ChainType.SOL, connector: solanaConnector },
126
+ });
127
+ expect([...document.head.querySelectorAll("style")].some((style) => style.textContent?.includes("--w3m-z-index: 2147483647"))).toBe(true);
128
+ failConnect?.();
129
+ await waitFor(() => expect([...document.head.querySelectorAll("style")].some((style) => style.textContent?.includes("--w3m-z-index: 2147483647"))).toBe(false));
130
+ });
77
131
  it("does not show viem's resource-unavailable error when the wallet prompt is closed", () => {
78
132
  render(_jsx(WalletConnector, { open: true, onClose: vi.fn() }));
79
133
  act(() => {
@@ -1,4 +1,4 @@
1
- import type { ApiClient } from "@coin-voyage/shared/api";
1
+ import type { ApiClient } from "@coin-voyage/shared/api/browser";
2
2
  import type { WebhookEventPayload } from "@coin-voyage/shared/types";
3
3
  interface UseOrderStatusProps {
4
4
  api: ApiClient;
@@ -1,7 +1,7 @@
1
1
  import { type Currency } from "@coin-voyage/shared/types";
2
2
  export interface SwapToken {
3
3
  currency: Currency;
4
- balance?: number;
4
+ balance?: string;
5
5
  balanceUsd?: number;
6
6
  }
7
7
  export declare function useTokenData(): {
@@ -9,5 +9,6 @@ export declare function useTokenData(): {
9
9
  accounts: import("@coin-voyage/crypto/hooks").Account[];
10
10
  isLoading: boolean;
11
11
  error: string | null;
12
- refreshBalances: import("react").ActionDispatch<[]>;
12
+ balancesLoaded: boolean;
13
+ refreshBalances: () => void;
13
14
  };