@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.
- package/LICENSE +8 -8
- package/README.md +68 -68
- package/dist/components/execution-modal.d.ts +1 -1
- package/dist/components/execution-modal.js +6 -2
- package/dist/components/styles.d.ts +1 -1
- package/dist/components/styles.js +1282 -1281
- package/dist/components/swap-recipient.test.d.ts +1 -0
- package/dist/components/swap-recipient.test.js +136 -0
- package/dist/components/swap.d.ts +3 -3
- package/dist/components/swap.js +35 -23
- package/dist/components/swap.test.d.ts +1 -0
- package/dist/components/swap.test.js +76 -0
- package/dist/components/token-picker.js +1 -1
- package/dist/components/wallet-connector.d.ts +1 -1
- package/dist/components/wallet-connector.js +24 -4
- package/dist/components/wallet-connector.test.js +55 -1
- package/dist/hooks/use-order-status.d.ts +1 -1
- package/dist/hooks/use-token-data.d.ts +3 -2
- package/dist/hooks/use-token-data.js +41 -12
- package/dist/hooks/use-token-data.test.d.ts +1 -0
- package/dist/hooks/use-token-data.test.js +81 -0
- package/dist/lib/amount-input.d.ts +1 -0
- package/dist/lib/amount-input.js +7 -0
- package/dist/lib/amount-input.test.d.ts +1 -0
- package/dist/lib/amount-input.test.js +17 -0
- package/dist/lib/execution.d.ts +2 -0
- package/dist/lib/execution.js +6 -0
- package/dist/lib/execution.test.d.ts +1 -0
- package/dist/lib/execution.test.js +24 -0
- package/dist/lib/output-wallet.d.ts +1 -2
- package/dist/lib/output-wallet.js +2 -2
- package/dist/lib/output-wallet.test.d.ts +1 -0
- package/dist/lib/output-wallet.test.js +26 -0
- package/dist/providers/swapkit-context.d.ts +1 -1
- package/dist/providers/swapkit-provider.js +1 -1
- package/dist/styles/root.js +59 -59
- package/dist/styles/theme.js +1 -1
- 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?:
|
|
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
|
-
|
|
24
|
+
headerAction?: ReactNode;
|
|
25
25
|
}
|
|
26
|
-
export declare function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken,
|
|
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 {};
|
package/dist/components/swap.js
CHANGED
|
@@ -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,
|
|
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
|
|
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)) })),
|
|
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 =
|
|
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
|
|
257
|
-
|
|
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
|
-
|
|
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),
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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"} ${
|
|
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.
|
|
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,7 +1,7 @@
|
|
|
1
1
|
import { type Currency } from "@coin-voyage/shared/types";
|
|
2
2
|
export interface SwapToken {
|
|
3
3
|
currency: Currency;
|
|
4
|
-
balance?:
|
|
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
|
-
|
|
12
|
+
balancesLoaded: boolean;
|
|
13
|
+
refreshBalances: () => void;
|
|
13
14
|
};
|