@coin-voyage/swapkit 0.4.0-beta.0 → 0.5.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 (47) hide show
  1. package/LICENSE +8 -8
  2. package/README.md +68 -68
  3. package/dist/components/currency-panel.d.ts +17 -0
  4. package/dist/components/currency-panel.js +22 -0
  5. package/dist/components/execution-modal.d.ts +1 -1
  6. package/dist/components/execution-modal.js +33 -6
  7. package/dist/components/execution-modal.test.d.ts +1 -0
  8. package/dist/components/execution-modal.test.js +92 -0
  9. package/dist/components/manual-wallet-modal.d.ts +8 -0
  10. package/dist/components/manual-wallet-modal.js +19 -0
  11. package/dist/components/output-wallet-selector.d.ts +7 -0
  12. package/dist/components/output-wallet-selector.js +10 -0
  13. package/dist/components/quote-breakdown.d.ts +5 -0
  14. package/dist/components/quote-breakdown.js +13 -0
  15. package/dist/components/styles.d.ts +2 -1
  16. package/dist/components/styles.js +1294 -1281
  17. package/dist/components/swap-recipient.test.js +61 -2
  18. package/dist/components/swap-settings.d.ts +9 -0
  19. package/dist/components/swap-settings.js +24 -0
  20. package/dist/components/swap.d.ts +2 -28
  21. package/dist/components/swap.js +27 -301
  22. package/dist/components/token-picker.test.js +11 -0
  23. package/dist/hooks/use-dismiss.d.ts +3 -0
  24. package/dist/hooks/use-dismiss.js +26 -0
  25. package/dist/hooks/use-swap-notifications.d.ts +10 -0
  26. package/dist/hooks/use-swap-notifications.js +53 -0
  27. package/dist/hooks/use-swap-quote.js +49 -51
  28. package/dist/hooks/use-swap.d.ts +47 -0
  29. package/dist/hooks/use-swap.js +189 -0
  30. package/dist/hooks/use-token-data.d.ts +2 -1
  31. package/dist/hooks/use-token-data.js +42 -13
  32. package/dist/hooks/use-token-data.test.js +78 -3
  33. package/dist/lib/currency-selection.js +6 -4
  34. package/dist/lib/currency-selection.test.d.ts +1 -0
  35. package/dist/lib/currency-selection.test.js +16 -0
  36. package/dist/lib/execution.d.ts +2 -0
  37. package/dist/lib/execution.js +6 -0
  38. package/dist/lib/execution.test.d.ts +1 -0
  39. package/dist/lib/execution.test.js +24 -0
  40. package/dist/lib/format.d.ts +5 -0
  41. package/dist/lib/format.js +20 -0
  42. package/dist/lib/quote.d.ts +10 -1
  43. package/dist/lib/quote.js +12 -0
  44. package/dist/lib/slippage.d.ts +11 -0
  45. package/dist/lib/slippage.js +17 -0
  46. package/dist/styles/root.js +59 -59
  47. package/package.json +4 -4
@@ -12,9 +12,16 @@ vi.mock("../hooks/use-swap-execution", () => ({
12
12
  }));
13
13
  vi.mock("../hooks/use-token-data", () => ({
14
14
  useTokenData: () => ({
15
- tokens: [{ currency: source, balance: "2" }, { currency: destination }, { currency: solana }],
15
+ tokens: [
16
+ { currency: source, balance: "2" },
17
+ { currency: destination, balance: "7.5" },
18
+ { currency: unheld },
19
+ { currency: solana },
20
+ { currency: zcash },
21
+ ],
16
22
  accounts,
17
23
  isLoading: false,
24
+ balancesLoaded: true,
18
25
  refreshBalances: vi.fn(),
19
26
  }),
20
27
  }));
@@ -34,7 +41,12 @@ vi.mock("./wallet-connector", () => ({ WalletConnector: () => null }));
34
41
  vi.mock("./execution-modal", () => ({ ExecutionModal: () => null }));
35
42
  const sender = "0x1111111111111111111111111111111111111111";
36
43
  const recipient = "0x2222222222222222222222222222222222222222";
37
- const accounts = [{ address: sender, chainType: ChainType.EVM }];
44
+ const bitcoinWallet = "bc1q048uhadg63tlua07htxaufxgqe5yz798vlwqxw";
45
+ const zcashRecipient = "t1QidWX3ojKEXAYJSr7S8pUDUQ2T81dgYEc";
46
+ const accounts = [
47
+ { address: sender, chainType: ChainType.EVM },
48
+ { address: bitcoinWallet, chainType: ChainType.UTXO },
49
+ ];
38
50
  const source = {
39
51
  id: "eth",
40
52
  chain_id: ChainId.ETH,
@@ -50,6 +62,14 @@ const destination = {
50
62
  symbol: "USDC",
51
63
  decimals: 6,
52
64
  };
65
+ const unheld = {
66
+ id: "dai",
67
+ chain_id: ChainId.ETH,
68
+ address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
69
+ name: "Dai",
70
+ symbol: "DAI",
71
+ decimals: 18,
72
+ };
53
73
  const solana = {
54
74
  id: "sol",
55
75
  chain_id: ChainId.SOL,
@@ -57,6 +77,13 @@ const solana = {
57
77
  symbol: "SOL",
58
78
  decimals: 9,
59
79
  };
80
+ const zcash = {
81
+ id: "zec",
82
+ chain_id: ChainId.ZEC,
83
+ name: "Zcash",
84
+ symbol: "ZEC",
85
+ decimals: 8,
86
+ };
60
87
  beforeEach(() => execute.mockClear());
61
88
  afterEach(cleanup);
62
89
  function renderSwap(output = destination) {
@@ -87,6 +114,23 @@ describe("Swap destination recipient", () => {
87
114
  destination: expect.objectContaining({ recipient }),
88
115
  }), sender);
89
116
  });
117
+ it("shows the buy token balance until a recipient is pasted", () => {
118
+ renderSwap();
119
+ expect(screen.getByText("Balance: 7.5")).toBeTruthy();
120
+ openRecipientModal();
121
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: recipient } });
122
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
123
+ expect(screen.queryByText("Balance: 7.5")).toBeNull();
124
+ expect(screen.getByText("Balance: 2")).toBeTruthy();
125
+ });
126
+ it("shows a zero buy balance for a token the connected wallet does not hold", () => {
127
+ renderSwap(unheld);
128
+ expect(screen.getByText("Balance: 0")).toBeTruthy();
129
+ });
130
+ it("shows no buy balance when no wallet is connected for the destination chain", () => {
131
+ renderSwap(solana);
132
+ expect(screen.queryByText("Balance: 0")).toBeNull();
133
+ });
90
134
  it("rejects invalid same-chain destination addresses", () => {
91
135
  renderSwap();
92
136
  openRecipientModal();
@@ -102,4 +146,19 @@ describe("Swap destination recipient", () => {
102
146
  expect(screen.getByRole("menuitem", { name: "Connect Wallet" })).toBeTruthy();
103
147
  expect(screen.getByRole("menuitem", { name: "Paste Address" })).toBeTruthy();
104
148
  });
149
+ it("requires a pasted address for Zcash instead of the connected Bitcoin wallet", () => {
150
+ renderSwap(zcash);
151
+ expect(screen.queryByRole("button", { name: "Swap" })).toBeNull();
152
+ expect(screen.getByRole("button", { name: "Swap tokens" }).hasAttribute("disabled")).toBe(true);
153
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
154
+ expect(screen.queryByRole("menuitem", { name: "Connect Wallet" })).toBeNull();
155
+ fireEvent.click(screen.getByRole("button", { name: "Paste Zcash Address" }));
156
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: bitcoinWallet } });
157
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
158
+ expect(screen.getByText("Enter a valid address for the destination network")).toBeTruthy();
159
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: zcashRecipient } });
160
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
161
+ fireEvent.click(screen.getByRole("button", { name: "Swap" }));
162
+ expect(execute).toHaveBeenCalledWith(expect.objectContaining({ destination: expect.objectContaining({ recipient: zcashRecipient }) }), sender);
163
+ });
105
164
  });
@@ -0,0 +1,9 @@
1
+ import type { SlippageMode } from "../hooks/use-swap-state";
2
+ import type { ResolvedSlippage } from "../lib/slippage";
3
+ export declare function SwapSettings({ slippageMode, slippagePercent, warning, onSlippageModeChange, onSlippagePercentChange, }: {
4
+ slippageMode: SlippageMode;
5
+ slippagePercent: string;
6
+ warning?: ResolvedSlippage["warning"];
7
+ onSlippageModeChange: (mode: SlippageMode) => void;
8
+ onSlippagePercentChange: (value: string) => void;
9
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useRef, useState } from "react";
3
+ import { useDismiss } from "../hooks/use-dismiss";
4
+ import { isValidSwapAmountInput } from "../lib/amount-input";
5
+ import { InfoIcon, SettingsIcon } from "./icons";
6
+ import { Hint, IconButton, InfoButton, InfoWrap, SettingsLabel, SettingsPanel, SettingsRow, SettingsTooltip, SlippageControl, SlippageInput, SlippageInputWrap, SlippageModeButton, ToolbarGroup, } from "./styles";
7
+ const SLIPPAGE_TOOLTIP = "Your transaction will revert if the price changes more than the slippage percentage. For crosschain swaps involving multiple steps, this slippage will apply for each swap.";
8
+ function SlippageInfoTooltip({ id }) {
9
+ const [hovered, setHovered] = useState(false);
10
+ const [focused, setFocused] = useState(false);
11
+ const open = hovered || focused;
12
+ return (_jsxs(InfoWrap, { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [_jsx(InfoButton, { type: "button", "aria-label": "About max slippage", "aria-describedby": open ? id : undefined, onFocus: () => setFocused(true), onBlur: () => setFocused(false), children: _jsx(InfoIcon, {}) }), open && (_jsx(SettingsTooltip, { id: id, role: "tooltip", children: SLIPPAGE_TOOLTIP }))] }));
13
+ }
14
+ export function SwapSettings({ slippageMode, slippagePercent, warning, onSlippageModeChange, onSlippagePercentChange, }) {
15
+ const [open, setOpen] = useState(false);
16
+ const groupRef = useRef(null);
17
+ const slippageInputId = useId();
18
+ const slippageTooltipId = useId();
19
+ useDismiss(groupRef, open, () => setOpen(false));
20
+ return (_jsxs(ToolbarGroup, { ref: groupRef, "$pushRight": true, children: [_jsxs(IconButton, { type: "button", "aria-label": "Swap settings", "aria-expanded": open, onClick: () => setOpen((current) => !current), children: [_jsx(SettingsIcon, {}), _jsx("span", { children: slippageMode === "auto" ? "Auto" : `${slippagePercent || "0"} %` })] }), open && (_jsxs(SettingsPanel, { role: "dialog", "aria-label": "Swap settings", children: [_jsxs(SettingsRow, { children: [_jsxs(SettingsLabel, { children: ["Max slippage", _jsx(SlippageInfoTooltip, { id: slippageTooltipId })] }), _jsxs(SlippageControl, { children: [_jsx(SlippageModeButton, { type: "button", "aria-label": "Use automatic slippage", "aria-pressed": slippageMode === "auto", "$active": slippageMode === "auto", onClick: () => onSlippageModeChange("auto"), children: "Auto" }), _jsxs(SlippageInputWrap, { "$active": slippageMode === "custom", children: [_jsx(SlippageInput, { id: slippageInputId, "aria-label": "Slippage tolerance", inputMode: "decimal", value: slippagePercent, onClick: () => onSlippageModeChange("custom"), onFocus: () => onSlippageModeChange("custom"), onChange: (event) => {
21
+ if (isValidSwapAmountInput(event.target.value))
22
+ onSlippagePercentChange(event.target.value);
23
+ } }), _jsx("span", { children: "%" })] })] })] }), warning && _jsx(Hint, { "$tone": warning.tone, children: warning.message })] }))] }));
24
+ }
@@ -1,28 +1,2 @@
1
- import { type ReactNode } from "react";
2
- import { useSwapExecution } from "../hooks/use-swap-execution";
3
- import { type SwapToken } from "../hooks/use-token-data";
4
- import { type SwapSide } from "../lib/quote";
5
- import type { SwapCompleteResult, SwapError, SwapProps } from "../types";
6
- export declare function useSwapNotifications({ stage, quoteError, tokenError, onBalancesChanged, onComplete, onError, }: {
7
- stage: ReturnType<typeof useSwapExecution>["stage"];
8
- quoteError?: string | null;
9
- tokenError?: string | null;
10
- onBalancesChanged: () => void;
11
- onComplete?: (result: SwapCompleteResult) => void;
12
- onError?: (error: SwapError) => void;
13
- }): void;
14
- interface CurrencyPanelProps {
15
- side: SwapSide;
16
- token?: SwapToken;
17
- amount: string;
18
- balance?: string;
19
- quoteUsd?: number | null;
20
- quoteLoading?: boolean;
21
- disabled?: boolean;
22
- onAmountChange: (value: string) => void;
23
- onSelectToken: () => void;
24
- footerAction?: ReactNode;
25
- }
26
- export declare function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken, footerAction, }: CurrencyPanelProps): import("react/jsx-runtime").JSX.Element;
27
- export declare function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaultSlippageBps, onComplete, onError, }?: SwapProps): import("react/jsx-runtime").JSX.Element;
28
- export {};
1
+ import type { SwapProps } from "../types";
2
+ export declare function Swap(props?: SwapProps): import("react/jsx-runtime").JSX.Element;
@@ -1,319 +1,45 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useAccount } from "@coin-voyage/crypto/hooks";
3
- import { getChainTypeByChainId, isValidAddress } from "@coin-voyage/shared/chain";
4
- import { formatCurrencyAmount } from "@coin-voyage/shared/currency";
5
- import { currencyID } from "@coin-voyage/shared/types";
6
- import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
7
- import { useSwapExecution } from "../hooks/use-swap-execution";
8
- import { useSwapQuote } from "../hooks/use-swap-quote";
9
- import { useSwapState } from "../hooks/use-swap-state";
10
- import { useTokenData } from "../hooks/use-token-data";
11
- import { balanceFraction, isValidSwapAmountInput } from "../lib/amount-input";
12
- import { getSwapWalletRequirement, shouldShowOutputWalletSelector, shouldUseSeparateOutputWallet, } from "../lib/output-wallet";
13
- import { buildSwapQuoteRequest, isSameSwap, toCurrencyBase } from "../lib/quote";
2
+ import { useState } from "react";
3
+ import { useSwap } from "../hooks/use-swap";
14
4
  import { useSwapKit } from "../providers/swapkit-context";
15
5
  import { SwapKitRoot } from "../styles/root";
16
- import { ExecutionModal } from "./execution-modal";
17
6
  import { ConnectedWallets } from "./connected-wallets";
18
- import { ArrowDownIcon, ChevronDownIcon, FuelIcon, InfoIcon, PasteIcon, SettingsIcon, WalletIcon } from "./icons";
19
- import { Modal } from "./modal";
20
- 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";
21
- import { TokenIcon } from "./token-icon";
7
+ import { CurrencyPanel } from "./currency-panel";
8
+ import { ExecutionModal } from "./execution-modal";
9
+ import { ArrowDownIcon } from "./icons";
10
+ import { ManualWalletModal } from "./manual-wallet-modal";
11
+ import { OutputWalletSelector } from "./output-wallet-selector";
12
+ import { QuoteBreakdown } from "./quote-breakdown";
13
+ import { CardWrap, ErrorBox, FormStack, PrimaryButton, Spinner, SwitchButton, Toolbar, WalletRequirementButton, } from "./styles";
14
+ import { SwapSettings } from "./swap-settings";
22
15
  import { TokenPicker } from "./token-picker";
23
16
  import { WalletConnector } from "./wallet-connector";
24
- import { truncateAddress } from "@coin-voyage/shared/utils";
25
- function DismissibleToolbarGroup({ open, onDismiss, pushRight = false, children, }) {
26
- const groupRef = useRef(null);
27
- const onDismissRef = useRef(onDismiss);
28
- useEffect(() => {
29
- onDismissRef.current = onDismiss;
30
- }, [onDismiss]);
31
- useEffect(() => {
32
- if (!open)
33
- return;
34
- const onPointerDown = (event) => {
35
- if (!groupRef.current?.contains(event.target))
36
- onDismissRef.current();
37
- };
38
- const onKeyDown = (event) => {
39
- if (event.key === "Escape")
40
- onDismissRef.current();
41
- };
42
- document.addEventListener("pointerdown", onPointerDown);
43
- document.addEventListener("keydown", onKeyDown);
44
- return () => {
45
- document.removeEventListener("pointerdown", onPointerDown);
46
- document.removeEventListener("keydown", onKeyDown);
47
- };
48
- }, [open]);
49
- return (_jsx(ToolbarGroup, { ref: groupRef, "$pushRight": pushRight, children: children }));
50
- }
51
- const SLIPPAGE_TOOLTIP = "Your transaction will revert if the price changes more than the slippage percentage. For crosschain swaps involving multiple steps, this slippage will apply for each swap.";
52
- function SlippageInfoTooltip({ id }) {
53
- const [hovered, setHovered] = useState(false);
54
- const [focused, setFocused] = useState(false);
55
- const open = hovered || focused;
56
- return (_jsxs(InfoWrap, { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [_jsx(InfoButton, { type: "button", "aria-label": "About max slippage", "aria-describedby": open ? id : undefined, onFocus: () => setFocused(true), onBlur: () => setFocused(false), children: _jsx(InfoIcon, {}) }), open && (_jsx(SettingsTooltip, { id: id, role: "tooltip", children: SLIPPAGE_TOOLTIP }))] }));
57
- }
58
- export function useSwapNotifications({ stage, quoteError, tokenError, onBalancesChanged, onComplete, onError, }) {
59
- const onCompleteRef = useRef(onComplete);
60
- const onErrorRef = useRef(onError);
61
- const onBalancesChangedRef = useRef(onBalancesChanged);
62
- const lastNotification = useRef(undefined);
63
- useEffect(() => {
64
- onCompleteRef.current = onComplete;
65
- onErrorRef.current = onError;
66
- onBalancesChangedRef.current = onBalancesChanged;
67
- }, [onBalancesChanged, onComplete, onError]);
68
- useEffect(() => {
69
- if (stage.kind === "idle") {
70
- lastNotification.current = undefined;
71
- return;
72
- }
73
- if (stage.kind === "done") {
74
- const transactionHash = stage.destinationTxHash ?? stage.sourceTxHash;
75
- if (!transactionHash)
76
- return;
77
- const key = `complete:${stage.orderId}:${transactionHash}`;
78
- if (lastNotification.current !== key) {
79
- lastNotification.current = key;
80
- onBalancesChangedRef.current();
81
- onCompleteRef.current?.({ orderId: stage.orderId, transactionHash });
82
- }
83
- }
84
- else if (stage.kind === "error") {
85
- const key = `error:${stage.message}`;
86
- if (lastNotification.current !== key) {
87
- lastNotification.current = key;
88
- onErrorRef.current?.({ message: stage.message });
89
- }
90
- }
91
- }, [stage]);
92
- useEffect(() => {
93
- const message = quoteError ?? tokenError;
94
- if (!message)
95
- return;
96
- const key = `error:${message}`;
97
- if (lastNotification.current !== key) {
98
- lastNotification.current = key;
99
- onErrorRef.current?.({ message });
100
- }
101
- }, [quoteError, tokenError]);
102
- }
103
- function OutputWalletSelector({ label, open, onOpenChange, onConnect, onPaste, }) {
104
- const controlRef = useRef(null);
105
- useEffect(() => {
106
- if (!open)
107
- return;
108
- const dismiss = (event) => {
109
- if (!controlRef.current?.contains(event.target))
110
- onOpenChange(false);
111
- };
112
- const dismissOnEscape = (event) => {
113
- if (event.key === "Escape")
114
- onOpenChange(false);
115
- };
116
- document.addEventListener("pointerdown", dismiss);
117
- document.addEventListener("keydown", dismissOnEscape);
118
- return () => {
119
- document.removeEventListener("pointerdown", dismiss);
120
- document.removeEventListener("keydown", dismissOnEscape);
121
- };
122
- }, [onOpenChange, open]);
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, {})] })] }))] }));
124
- }
125
- function formatUsd(value) {
126
- if (value === undefined || value === null || !Number.isFinite(value))
127
- return "$0.00";
128
- return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 2 }).format(value);
129
- }
130
- function formatPriceImpact(value) {
131
- if (value > 0 && value < 0.01)
132
- return "<0.01%";
133
- return `${value.toFixed(2)}%`;
134
- }
135
- function tokenUsdValue(token, amount) {
136
- const numericAmount = Number(amount);
137
- if (!token?.currency.price_usd || !Number.isFinite(numericAmount))
138
- return undefined;
139
- return token.currency.price_usd * numericAmount;
140
- }
141
- const QUICK_AMOUNTS = [
142
- { label: "25%", fraction: 0.25 },
143
- { label: "50%", fraction: 0.5 },
144
- { label: "75%", fraction: 0.75 },
145
- { label: "Max", fraction: 1 },
146
- ];
147
- export function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken, footerAction, }) {
148
- const label = side === "input" ? "SELL" : "BUY";
149
- const showQuickAmounts = side === "input" && balance !== undefined && token !== undefined;
150
- 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
151
- ? `Use maximum ${token.currency.symbol} balance`
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) => {
153
- if (isValidSwapAmountInput(event.target.value))
154
- onAmountChange(event.target.value);
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] })] })] }));
156
- }
157
- function ManualWalletModal({ open, chainId, initialValue, onClose, onSave, }) {
158
- const [value, setValue] = useState(initialValue ?? "");
159
- const [error, setError] = useState(null);
160
- const outputAddressId = useId();
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: () => {
162
- if (!chainId || !isValidAddress(value, chainId)) {
163
- setError("Enter a valid address for the destination network");
164
- return;
165
- }
166
- setError(null);
167
- onSave(value);
168
- onClose();
169
- }, children: "Use address" })] }));
170
- }
171
- export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaultSlippageBps, onComplete, onError, } = {}) {
17
+ export function Swap(props = {}) {
172
18
  const { mode, theme } = useSwapKit();
173
- const slippageInputId = useId();
174
- const slippageTooltipId = useId();
175
- const { tokens, accounts, isLoading, error: tokenError, refreshBalances } = useTokenData();
176
- const { accounts: connectedAccounts } = useAccount();
19
+ const swap = useSwap(props);
20
+ const { sourceToken, destinationToken, walletRequirement, quote, quoteLoading } = swap;
177
21
  const [sourcePickerOpen, setSourcePickerOpen] = useState(false);
178
22
  const [destinationPickerOpen, setDestinationPickerOpen] = useState(false);
179
- const [settingsOpen, setSettingsOpen] = useState(false);
180
- const [connectTarget, setConnectTarget] = useState({
181
- open: false,
182
- });
23
+ const [connectTarget, setConnectTarget] = useState({ open: false });
183
24
  const [manualWalletOpen, setManualWalletOpen] = useState(false);
184
25
  const [outputWalletMenuOpen, setOutputWalletMenuOpen] = useState(false);
185
- const [breakdownOpen, setBreakdownOpen] = useState(false);
186
- const connectedChainTypes = new Set(accounts.map((account) => account.chainType));
187
- const swapState = useSwapState({
188
- currencies: tokens.map((token) => token.currency),
189
- connectedChainTypes,
190
- defaultSourceCurrency,
191
- defaultDestinationCurrency,
192
- defaultSlippageBps,
193
- });
194
- const { sourceId: resolvedSourceId, destinationId: resolvedDestinationId, inputAmount, outputAmount, activeSide, slippageMode, slippagePercent, manualOutputWallet, } = swapState.state;
195
- const sourceToken = tokens.find((token) => currencyID(token.currency) === resolvedSourceId);
196
- const destinationToken = tokens.find((token) => currencyID(token.currency) === resolvedDestinationId);
197
- const sourceChainType = sourceToken ? getChainTypeByChainId(sourceToken.currency.chain_id) : undefined;
198
- const destinationChainType = destinationToken ? getChainTypeByChainId(destinationToken.currency.chain_id) : undefined;
199
- const inputAccount = connectedAccounts.find((account) => account.chainType === sourceChainType);
200
- const outputAccount = connectedAccounts.find((account) => account.chainType === destinationChainType);
201
- const separateOutputWallet = shouldUseSeparateOutputWallet(sourceChainType, destinationChainType);
202
- const manualAddress = manualOutputWallet && manualOutputWallet.chainId === destinationToken?.currency.chain_id
203
- ? manualOutputWallet.address
204
- : undefined;
205
- const outputAddress = manualAddress ?? (separateOutputWallet ? outputAccount?.address : inputAccount?.address);
206
- const walletRequirement = getSwapWalletRequirement({
207
- inputWalletAddress: inputAccount?.address,
208
- inputChainType: sourceChainType,
209
- shouldRestrictInputChainType: Boolean(sourceToken),
210
- outputWalletAddress: outputAddress,
211
- outputChainType: destinationChainType,
212
- });
213
- const showOutputWalletSelector = shouldShowOutputWalletSelector({
214
- outputChainType: destinationChainType,
215
- connectionRequirement: walletRequirement,
216
- });
217
- const slippage = Number(slippagePercent);
218
- const invalidSlippage = slippageMode === "custom" &&
219
- (slippagePercent.length === 0 || !Number.isFinite(slippage) || slippage < 0 || slippage > 100);
220
- const slippageBps = slippageMode === "custom" && !invalidSlippage ? Math.round(slippage * 100) : undefined;
221
- const activeAmount = activeSide === "input" ? inputAmount : outputAmount;
222
- const quoteRequest = useMemo(() => invalidSlippage ||
223
- isSameSwap(sourceToken?.currency, destinationToken?.currency, inputAccount?.address, outputAddress)
224
- ? null
225
- : buildSwapQuoteRequest({
226
- amount: activeAmount,
227
- activeSide,
228
- source: sourceToken?.currency,
229
- destination: destinationToken?.currency,
230
- slippageBps,
231
- }), [
232
- activeAmount,
233
- activeSide,
234
- destinationToken?.currency,
235
- inputAccount?.address,
236
- invalidSlippage,
237
- outputAddress,
238
- slippageBps,
239
- sourceToken?.currency,
240
- ]);
241
- const execution = useSwapExecution(sourceChainType, inputAccount?.address);
242
- const quoteState = useSwapQuote(quoteRequest, execution.stage.kind === "idle");
243
- useSwapNotifications({
244
- stage: execution.stage,
245
- quoteError: quoteState.error,
246
- tokenError,
247
- onBalancesChanged: refreshBalances,
248
- onComplete,
249
- onError,
250
- });
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;
257
- const sourceBalance = sourceToken?.balance;
258
- const insufficientBalance = sourceBalance !== undefined && Number(displayedInputAmount) > Number(sourceBalance);
259
- const sameSwap = isSameSwap(sourceToken?.currency, destinationToken?.currency, inputAccount?.address, outputAddress);
260
- const canExecute = Boolean(quoteState.quote &&
261
- quoteRequest &&
262
- inputAccount?.address &&
263
- outputAddress &&
264
- walletRequirement.target === "none" &&
265
- !insufficientBalance &&
266
- !sameSwap &&
267
- !invalidSlippage);
268
- const reverseTokens = useCallback(() => {
269
- swapState.reverse(displayedInputAmount, displayedOutputAmount);
270
- setOutputWalletMenuOpen(false);
271
- }, [displayedInputAmount, displayedOutputAmount, swapState]);
272
- const executeSwap = useCallback(async () => {
273
- if (!quoteRequest || !sourceToken || !destinationToken || !inputAccount?.address || !outputAddress)
274
- return;
275
- const request = {
276
- trade_type: quoteRequest.trade_type,
277
- amount: quoteRequest.amount,
278
- source: {
279
- currency: toCurrencyBase(sourceToken.currency),
280
- sender_address: inputAccount.address,
281
- ...(slippageBps === undefined ? {} : { slippage_bps: slippageBps }),
282
- },
283
- destination: { currency: toCurrencyBase(destinationToken.currency), recipient: outputAddress },
284
- };
285
- await execution.execute(request, inputAccount.address);
286
- }, [destinationToken, execution, inputAccount, outputAddress, quoteRequest, slippageBps, sourceToken]);
287
- const selectToken = (side, token) => {
288
- swapState.selectCurrency(side, currencyID(token.currency));
289
- };
290
- const quoteSummary = quoteState.quote
291
- ? `1 ${quoteState.quote.input.currency.symbol} ≈ ${(Number(quoteState.quote.output.amount.ui) / Math.max(Number(quoteState.quote.input.amount.ui), Number.EPSILON)).toLocaleString(undefined, { maximumSignificantDigits: 6 })} ${quoteState.quote.output.currency.symbol}`
292
- : "Swap details";
293
- return (_jsx(SwapKitRoot, { "$mode": mode, "$customTheme": theme, "data-swapkit-root": "", children: _jsxs(FormStack, { children: [_jsxs(Toolbar, { children: [_jsx(ConnectedWallets, {}), _jsxs(DismissibleToolbarGroup, { open: settingsOpen, onDismiss: () => setSettingsOpen(false), pushRight: true, children: [_jsxs(IconButton, { type: "button", "aria-label": "Swap settings", "aria-expanded": settingsOpen, onClick: () => setSettingsOpen((open) => !open), children: [_jsx(SettingsIcon, {}), _jsx("span", { children: slippageMode === "auto" ? "Auto" : `${slippagePercent || "0"} %` })] }), settingsOpen && (_jsxs(SettingsPanel, { role: "dialog", "aria-label": "Swap settings", children: [_jsxs(SettingsRow, { children: [_jsxs(SettingsLabel, { children: ["Max slippage", _jsx(SlippageInfoTooltip, { id: slippageTooltipId })] }), _jsxs(SlippageControl, { children: [_jsx(SlippageModeButton, { type: "button", "aria-label": "Use automatic slippage", "aria-pressed": slippageMode === "auto", "$active": slippageMode === "auto", onClick: () => swapState.setSlippageMode("auto"), children: "Auto" }), _jsxs(SlippageInputWrap, { "$active": slippageMode === "custom", children: [_jsx(SlippageInput, { id: slippageInputId, "aria-label": "Slippage tolerance", inputMode: "decimal", value: slippagePercent, onClick: () => swapState.setSlippageMode("custom"), onFocus: () => swapState.setSlippageMode("custom"), onChange: (event) => {
294
- if (isValidSwapAmountInput(event.target.value)) {
295
- swapState.setSlippagePercent(event.target.value);
296
- }
297
- } }), _jsx("span", { children: "%" })] })] })] }), slippageMode === "custom" &&
298
- (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) => {
299
- swapState.setAmount("input", value);
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) => {
301
- swapState.setAmount("output", value);
302
- }, onSelectToken: () => setDestinationPickerOpen(true), footerAction: showOutputWalletSelector ? (_jsx(OutputWalletSelector, { label: outputAddress ? truncateAddress(outputAddress) : "Connect Wallet", open: outputWalletMenuOpen, onOpenChange: setOutputWalletMenuOpen, onConnect: separateOutputWallet
26
+ return (_jsx(SwapKitRoot, { "$mode": mode, "$customTheme": theme, "data-swapkit-root": "", children: _jsxs(FormStack, { children: [_jsxs(Toolbar, { children: [_jsx(ConnectedWallets, {}), _jsx(SwapSettings, { slippageMode: swap.slippageMode, slippagePercent: swap.slippagePercent, warning: swap.slippage.warning, onSlippageModeChange: swap.setSlippageMode, onSlippagePercentChange: swap.setSlippagePercent })] }), _jsxs(CardWrap, { children: [_jsx(CurrencyPanel, { side: "input", token: sourceToken, amount: swap.displayedInputAmount, balance: swap.sourceBalance, quoteUsd: quote?.input.amount.value_usd, quoteLoading: quoteLoading && swap.activeSide === "output", disabled: !swap.idle, onAmountChange: (value) => swap.setAmount("input", value), onSelectToken: () => setSourcePickerOpen(true) }), _jsx(SwitchButton, { type: "button", "aria-label": "Swap tokens", onClick: () => {
27
+ swap.reverse();
28
+ setOutputWalletMenuOpen(false);
29
+ }, disabled: !sourceToken || !destinationToken || !swap.destinationHasWallet, children: _jsx(ArrowDownIcon, { size: 24 }) })] }), _jsx(CurrencyPanel, { side: "output", token: destinationToken, amount: swap.displayedOutputAmount, balance: swap.destinationBalance, quoteUsd: quote?.output.amount.value_usd, quoteLoading: quoteLoading && swap.activeSide === "input", disabled: !swap.idle, onAmountChange: (value) => swap.setAmount("output", value), onSelectToken: () => setDestinationPickerOpen(true), headerAction: swap.showOutputWalletSelector ? (_jsx(OutputWalletSelector, { label: swap.outputWalletLabel, open: outputWalletMenuOpen, onOpenChange: setOutputWalletMenuOpen, onConnect: swap.separateOutputWallet && swap.destinationHasWallet
303
30
  ? () => {
304
31
  setOutputWalletMenuOpen(false);
305
- setConnectTarget({ open: true, chainType: destinationChainType });
32
+ setConnectTarget({ open: true, chainType: swap.destinationChainType });
306
33
  }
307
34
  : undefined, onPaste: () => {
308
35
  setOutputWalletMenuOpen(false);
309
36
  setManualWalletOpen(true);
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) => {
311
- if (destinationToken)
312
- swapState.setManualOutputWallet({ chainId: destinationToken.currency.chain_id, address });
313
- } })), _jsx(ExecutionModal, { stage: execution.stage, onClose: () => {
314
- const terminal = ["done", "refunded"].includes(execution.stage.kind);
315
- execution.reset();
316
- if (terminal)
317
- swapState.reset({ preserveCurrencies: execution.stage.kind === "done" });
318
- } })] }) }));
37
+ } })) : undefined }), swap.quoteError && _jsx(ErrorBox, { children: swap.quoteError }), swap.tokenError && _jsx(ErrorBox, { children: swap.tokenError }), swap.insufficientBalance && _jsxs(ErrorBox, { children: ["Insufficient ", sourceToken?.currency.symbol, " balance"] }), swap.sameSwap && _jsx(ErrorBox, { children: "Select a different token or destination wallet" }), _jsx(QuoteBreakdown, { quote: quote, loading: quoteLoading }), walletRequirement.target !== "none" ? (_jsx(WalletRequirementButton, { type: "button", onClick: () => {
38
+ if (swap.requiresPastedAddress)
39
+ setManualWalletOpen(true);
40
+ else
41
+ setConnectTarget({ open: true, chainType: walletRequirement.chainType });
42
+ }, children: swap.requiresPastedAddress
43
+ ? `Paste ${swap.destinationChainName ?? "Destination"} Address`
44
+ : walletRequirement.label })) : (_jsxs(PrimaryButton, { type: "button", disabled: !swap.canExecute || quoteLoading, onClick: () => void swap.execute(), children: [quoteLoading ? _jsx(Spinner, {}) : null, swap.tokensLoading ? "Loading tokens…" : quoteLoading ? "Fetching quote…" : "Swap"] })), _jsx(TokenPicker, { open: sourcePickerOpen, title: "Select Token", tokens: swap.sourceTokens, selectedId: swap.sourceId, onClose: () => setSourcePickerOpen(false), onSelect: (token) => swap.selectToken("input", token) }), _jsx(TokenPicker, { open: destinationPickerOpen, title: "Select Token", tokens: swap.tokens, selectedId: swap.destinationId, preferredChainId: sourceToken?.currency.chain_id, onClose: () => setDestinationPickerOpen(false), onSelect: (token) => swap.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: swap.manualAddress, onClose: () => setManualWalletOpen(false), onSave: swap.saveManualAddress })), _jsx(ExecutionModal, { stage: swap.stage, onClose: swap.closeExecution })] }) }));
319
45
  }
@@ -25,6 +25,17 @@ const tokens = [
25
25
  token({ id: "usdc-base", chain_id: ChainId.BASE, name: "Base USD Coin", symbol: "USDC", decimals: 6 }),
26
26
  ];
27
27
  describe("TokenPicker", () => {
28
+ it("lists Zcash as a BUY network and selects its native token", () => {
29
+ const zec = token({ id: "zec", chain_id: ChainId.ZEC, name: "Zcash", symbol: "ZEC", decimals: 8 });
30
+ const onSelect = vi.fn();
31
+ render(_jsx(TokenPicker, { open: true, title: "Select BUY token", tokens: [...tokens, zec], preferredChainId: ChainId.SOL, onClose: vi.fn(), onSelect: onSelect }));
32
+ fireEvent.click(screen.getByRole("button", { name: "Solana" }));
33
+ fireEvent.click(screen.getByRole("option", { name: "Zcash" }));
34
+ expect(screen.getByRole("button", { name: "Zcash" })).toBeTruthy();
35
+ expect(screen.queryByRole("option", { name: "SOL Solana" })).toBeNull();
36
+ fireEvent.click(screen.getByRole("option", { name: "ZEC Zcash" }));
37
+ expect(onSelect).toHaveBeenCalledWith(zec);
38
+ });
28
39
  it("prefilters BUY tokens to the selected SELL chain", () => {
29
40
  render(_jsx(TokenPicker, { open: true, title: "Select BUY token", tokens: tokens, preferredChainId: ChainId.SOL, onClose: vi.fn(), onSelect: vi.fn() }));
30
41
  expect(screen.getByRole("option", { name: "SOL Solana" })).toBeTruthy();
@@ -0,0 +1,3 @@
1
+ import { type RefObject } from "react";
2
+ /** Calls `onDismiss` on Escape or a pointer press outside `ref` while `open`. */
3
+ export declare function useDismiss(ref: RefObject<HTMLElement | null>, open: boolean, onDismiss: () => void): void;
@@ -0,0 +1,26 @@
1
+ import { useEffect, useRef } from "react";
2
+ /** Calls `onDismiss` on Escape or a pointer press outside `ref` while `open`. */
3
+ export function useDismiss(ref, open, onDismiss) {
4
+ const onDismissRef = useRef(onDismiss);
5
+ useEffect(() => {
6
+ onDismissRef.current = onDismiss;
7
+ }, [onDismiss]);
8
+ useEffect(() => {
9
+ if (!open)
10
+ return;
11
+ const onPointerDown = (event) => {
12
+ if (!ref.current?.contains(event.target))
13
+ onDismissRef.current();
14
+ };
15
+ const onKeyDown = (event) => {
16
+ if (event.key === "Escape")
17
+ onDismissRef.current();
18
+ };
19
+ document.addEventListener("pointerdown", onPointerDown);
20
+ document.addEventListener("keydown", onKeyDown);
21
+ return () => {
22
+ document.removeEventListener("pointerdown", onPointerDown);
23
+ document.removeEventListener("keydown", onKeyDown);
24
+ };
25
+ }, [open, ref]);
26
+ }
@@ -0,0 +1,10 @@
1
+ import type { SwapStage } from "../lib/execution";
2
+ import type { SwapCompleteResult, SwapError } from "../types";
3
+ export declare function useSwapNotifications({ stage, quoteError, tokenError, onBalancesChanged, onComplete, onError, }: {
4
+ stage: SwapStage;
5
+ quoteError?: string | null;
6
+ tokenError?: string | null;
7
+ onBalancesChanged: () => void;
8
+ onComplete?: (result: SwapCompleteResult) => void;
9
+ onError?: (error: SwapError) => void;
10
+ }): void;