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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment jsdom
3
+ import { ChainId, ChainType } from "@coin-voyage/shared/types";
4
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { Swap } from "./swap";
7
+ const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
8
+ vi.mock("@coin-voyage/crypto/hooks", () => ({ useAccount: () => ({ accounts }) }));
9
+ vi.mock("../providers/swapkit-context", () => ({ useSwapKit: () => ({ mode: "light", theme: {} }) }));
10
+ vi.mock("../hooks/use-swap-execution", () => ({
11
+ useSwapExecution: () => ({ stage: { kind: "idle" }, execute, reset: vi.fn() }),
12
+ }));
13
+ vi.mock("../hooks/use-token-data", () => ({
14
+ useTokenData: () => ({
15
+ tokens: [{ currency: source, balance: "2" }, { currency: destination }, { currency: solana }],
16
+ accounts,
17
+ isLoading: false,
18
+ refreshBalances: vi.fn(),
19
+ }),
20
+ }));
21
+ vi.mock("../hooks/use-swap-quote", () => ({
22
+ useSwapQuote: () => ({
23
+ quote: {
24
+ input: { currency: source, amount: { raw: "1000000000000000000", ui: "1" } },
25
+ output: { currency: destination, amount: { raw: "1000000", ui: "1" } },
26
+ price_impact: 0,
27
+ },
28
+ loading: false,
29
+ }),
30
+ }));
31
+ vi.mock("./connected-wallets", () => ({ ConnectedWallets: () => null }));
32
+ vi.mock("./token-picker", () => ({ TokenPicker: () => null }));
33
+ vi.mock("./wallet-connector", () => ({ WalletConnector: () => null }));
34
+ vi.mock("./execution-modal", () => ({ ExecutionModal: () => null }));
35
+ const sender = "0x1111111111111111111111111111111111111111";
36
+ const recipient = "0x2222222222222222222222222222222222222222";
37
+ const accounts = [{ address: sender, chainType: ChainType.EVM }];
38
+ const source = {
39
+ id: "eth",
40
+ chain_id: ChainId.ETH,
41
+ name: "Ether",
42
+ symbol: "ETH",
43
+ decimals: 18,
44
+ };
45
+ const destination = {
46
+ id: "usdc",
47
+ chain_id: ChainId.ETH,
48
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
49
+ name: "USD Coin",
50
+ symbol: "USDC",
51
+ decimals: 6,
52
+ };
53
+ const solana = {
54
+ id: "sol",
55
+ chain_id: ChainId.SOL,
56
+ name: "Solana",
57
+ symbol: "SOL",
58
+ decimals: 9,
59
+ };
60
+ beforeEach(() => execute.mockClear());
61
+ afterEach(cleanup);
62
+ function renderSwap(output = destination) {
63
+ render(_jsx(Swap, { defaultSourceCurrency: source, defaultDestinationCurrency: output }));
64
+ fireEvent.change(screen.getByRole("textbox", { name: "Sell amount" }), { target: { value: "1" } });
65
+ }
66
+ function openRecipientModal() {
67
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
68
+ fireEvent.click(screen.getByRole("menuitem", { name: "Paste Address" }));
69
+ }
70
+ describe("Swap destination recipient", () => {
71
+ it("offers only a pasted address for swaps on the same chain", () => {
72
+ renderSwap();
73
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
74
+ expect(screen.getByRole("menuitem", { name: "Paste Address" })).toBeTruthy();
75
+ expect(screen.queryByRole("menuitem", { name: "Connect Wallet" })).toBeNull();
76
+ expect(screen.getAllByRole("menuitem")).toHaveLength(1);
77
+ });
78
+ it("executes a same-chain swap to the pasted recipient", () => {
79
+ renderSwap();
80
+ openRecipientModal();
81
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: recipient } });
82
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
83
+ expect(screen.queryByRole("dialog", { name: "Paste destination address" })).toBeNull();
84
+ fireEvent.click(screen.getByRole("button", { name: "Swap" }));
85
+ expect(execute).toHaveBeenCalledWith(expect.objectContaining({
86
+ source: expect.objectContaining({ sender_address: sender }),
87
+ destination: expect.objectContaining({ recipient }),
88
+ }), sender);
89
+ });
90
+ it("rejects invalid same-chain destination addresses", () => {
91
+ renderSwap();
92
+ openRecipientModal();
93
+ fireEvent.change(screen.getByRole("textbox", { name: "Wallet address" }), { target: { value: "invalid" } });
94
+ fireEvent.click(screen.getByRole("button", { name: "Use address" }));
95
+ expect(screen.getByText("Enter a valid address for the destination network")).toBeTruthy();
96
+ expect(screen.getByRole("dialog", { name: "Paste destination address" })).toBeTruthy();
97
+ expect(execute).not.toHaveBeenCalled();
98
+ });
99
+ it("keeps both connect and paste options for cross-family swaps", () => {
100
+ renderSwap(solana);
101
+ fireEvent.click(screen.getByRole("button", { name: /^Output wallet:/ }));
102
+ expect(screen.getByRole("menuitem", { name: "Connect Wallet" })).toBeTruthy();
103
+ expect(screen.getByRole("menuitem", { name: "Paste Address" })).toBeTruthy();
104
+ });
105
+ });
@@ -15,7 +15,7 @@ interface CurrencyPanelProps {
15
15
  side: SwapSide;
16
16
  token?: SwapToken;
17
17
  amount: string;
18
- balance?: number;
18
+ balance?: string;
19
19
  quoteUsd?: number | null;
20
20
  quoteLoading?: boolean;
21
21
  disabled?: boolean;
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useAccount } from "@coin-voyage/crypto/hooks";
3
3
  import { getChainTypeByChainId, isValidAddress } from "@coin-voyage/shared/chain";
4
+ import { formatCurrencyAmount } from "@coin-voyage/shared/currency";
4
5
  import { currencyID } from "@coin-voyage/shared/types";
5
6
  import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
6
7
  import { useSwapExecution } from "../hooks/use-swap-execution";
7
8
  import { useSwapQuote } from "../hooks/use-swap-quote";
8
9
  import { useSwapState } from "../hooks/use-swap-state";
9
10
  import { useTokenData } from "../hooks/use-token-data";
10
- import { isValidSwapAmountInput } from "../lib/amount-input";
11
+ import { balanceFraction, isValidSwapAmountInput } from "../lib/amount-input";
11
12
  import { getSwapWalletRequirement, shouldShowOutputWalletSelector, shouldUseSeparateOutputWallet, } from "../lib/output-wallet";
12
13
  import { buildSwapQuoteRequest, isSameSwap, toCurrencyBase } from "../lib/quote";
13
14
  import { useSwapKit } from "../providers/swapkit-context";
@@ -119,7 +120,7 @@ function OutputWalletSelector({ label, open, onOpenChange, onConnect, onPaste, }
119
120
  document.removeEventListener("keydown", dismissOnEscape);
120
121
  };
121
122
  }, [onOpenChange, open]);
122
- return (_jsxs(OutputWalletControl, { ref: controlRef, children: [_jsxs(OutputWalletTrigger, { type: "button", "aria-label": `Output wallet: ${label}`, "aria-expanded": open, onClick: () => onOpenChange(!open), children: [_jsx("span", { children: label }), _jsx(ChevronDownIcon, { size: 16, up: open })] }), open && (_jsxs(OutputWalletMenu, { role: "menu", "aria-label": "Destination wallet options", children: [_jsxs(OutputWalletOption, { type: "button", role: "menuitem", onClick: onConnect, children: [_jsx("span", { children: "Connect Wallet" }), _jsx(WalletIcon, {})] }), _jsxs(OutputWalletOption, { type: "button", role: "menuitem", onClick: onPaste, children: [_jsx("span", { children: "Paste Address" }), _jsx(PasteIcon, {})] })] }))] }));
123
+ return (_jsxs(OutputWalletControl, { ref: controlRef, children: [_jsxs(OutputWalletTrigger, { type: "button", "aria-label": `Output wallet: ${label}`, "aria-expanded": open, onClick: () => onOpenChange(!open), children: [_jsx("span", { children: label }), _jsx(ChevronDownIcon, { size: 16, up: open })] }), open && (_jsxs(OutputWalletMenu, { role: "menu", "aria-label": "Destination wallet options", children: [onConnect && (_jsxs(OutputWalletOption, { type: "button", role: "menuitem", onClick: onConnect, children: [_jsx("span", { children: "Connect Wallet" }), _jsx(WalletIcon, {})] })), _jsxs(OutputWalletOption, { type: "button", role: "menuitem", onClick: onPaste, children: [_jsx("span", { children: "Paste Address" }), _jsx(PasteIcon, {})] })] }))] }));
123
124
  }
124
125
  function formatUsd(value) {
125
126
  if (value === undefined || value === null || !Number.isFinite(value))
@@ -143,11 +144,6 @@ const QUICK_AMOUNTS = [
143
144
  { label: "75%", fraction: 0.75 },
144
145
  { label: "Max", fraction: 1 },
145
146
  ];
146
- function balanceFraction(balance, fraction, decimals) {
147
- if (fraction === 1)
148
- return balance.toString();
149
- return Number((balance * fraction).toFixed(Math.min(Math.max(decimals, 0), 18))).toString();
150
- }
151
147
  export function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken, footerAction, }) {
152
148
  const label = side === "input" ? "SELL" : "BUY";
153
149
  const showQuickAmounts = side === "input" && balance !== undefined && token !== undefined;
@@ -156,13 +152,13 @@ export function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoa
156
152
  : `Use ${amountLabel} of ${token.currency.symbol} balance`, onClick: () => onAmountChange(balanceFraction(balance, fraction, token.currency.decimals)), children: amountLabel }, amountLabel))) })), _jsxs(AmountRow, { children: [quoteLoading ? (_jsx(AmountSkeleton, { role: "status", "aria-label": "Loading quoted amount" })) : (_jsx(AmountInput, { "aria-label": side === "input" ? "Sell amount" : "Buy amount", type: "text", inputMode: "decimal", autoComplete: "off", autoCorrect: "off", spellCheck: false, placeholder: "0", value: amount, disabled: disabled, onChange: (event) => {
157
153
  if (isValidSwapAmountInput(event.target.value))
158
154
  onAmountChange(event.target.value);
159
- } })), _jsxs(TokenButton, { type: "button", onClick: onSelectToken, "aria-label": `Select ${side} token`, children: [token ? _jsx(TokenIcon, { currency: token.currency }) : null, _jsx("span", { children: token?.currency.symbol ?? "Select" }), _jsx(ChevronDownIcon, {})] })] }), _jsxs(CardFooter, { children: [quoteLoading ? (_jsx(QuoteValueSkeleton, { role: "status", "aria-label": "Loading quoted value" })) : (_jsx("span", { children: formatUsd(quoteUsd ?? tokenUsdValue(token, amount)) })), _jsxs(InlineActions, { children: [balance !== undefined && _jsxs("span", { children: ["Balance: ", balance.toLocaleString()] }), footerAction] })] })] }));
155
+ } })), _jsxs(TokenButton, { type: "button", onClick: onSelectToken, "aria-label": `Select ${side} token`, children: [token ? _jsx(TokenIcon, { currency: token.currency }) : null, _jsx("span", { children: token?.currency.symbol ?? "Select" }), _jsx(ChevronDownIcon, {})] })] }), _jsxs(CardFooter, { children: [quoteLoading ? (_jsx(QuoteValueSkeleton, { role: "status", "aria-label": "Loading quoted value" })) : (_jsx("span", { children: formatUsd(quoteUsd ?? tokenUsdValue(token, amount)) })), _jsxs(InlineActions, { children: [balance !== undefined && _jsxs("span", { children: ["Balance: ", Number(balance).toLocaleString()] }), footerAction] })] })] }));
160
156
  }
161
157
  function ManualWalletModal({ open, chainId, initialValue, onClose, onSave, }) {
162
158
  const [value, setValue] = useState(initialValue ?? "");
163
159
  const [error, setError] = useState(null);
164
160
  const outputAddressId = useId();
165
- return (_jsxs(Modal, { open: open, title: "Paste destination address", onClose: onClose, children: [_jsx(FieldLabel, { htmlFor: outputAddressId, children: "Wallet address" }), _jsx(CompactInput, { id: outputAddressId, value: value, autoComplete: "off", onChange: (event) => setValue(event.target.value.trim()) }), error && _jsx(Hint, { "$tone": "danger", children: error }), _jsx(PrimaryButton, { type: "button", onClick: () => {
161
+ return (_jsxs(Modal, { open: open, title: "Paste destination address", onClose: onClose, children: [_jsx(FieldLabel, { htmlFor: outputAddressId, children: "Wallet address" }), _jsx(CompactInput, { id: outputAddressId, value: value, autoComplete: "off", onChange: (event) => setValue(event.target.value.trim()) }), error && _jsx(Hint, { "$tone": "danger", children: error }), _jsx(PrimaryButton, { type: "button", style: { marginTop: 12 }, onClick: () => {
166
162
  if (!chainId || !isValidAddress(value, chainId)) {
167
163
  setError("Enter a valid address for the destination network");
168
164
  return;
@@ -206,7 +202,7 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
206
202
  const manualAddress = manualOutputWallet && manualOutputWallet.chainId === destinationToken?.currency.chain_id
207
203
  ? manualOutputWallet.address
208
204
  : undefined;
209
- const outputAddress = separateOutputWallet ? (manualAddress ?? outputAccount?.address) : inputAccount?.address;
205
+ const outputAddress = manualAddress ?? (separateOutputWallet ? outputAccount?.address : inputAccount?.address);
210
206
  const walletRequirement = getSwapWalletRequirement({
211
207
  inputWalletAddress: inputAccount?.address,
212
208
  inputChainType: sourceChainType,
@@ -215,7 +211,6 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
215
211
  outputChainType: destinationChainType,
216
212
  });
217
213
  const showOutputWalletSelector = shouldShowOutputWalletSelector({
218
- requiresSeparateOutputWallet: separateOutputWallet,
219
214
  outputChainType: destinationChainType,
220
215
  connectionRequirement: walletRequirement,
221
216
  });
@@ -253,10 +248,14 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
253
248
  onComplete,
254
249
  onError,
255
250
  });
256
- const displayedInputAmount = activeSide === "output" && quoteState.quote ? quoteState.quote.input.amount.ui : inputAmount;
257
- const displayedOutputAmount = activeSide === "input" && quoteState.quote ? quoteState.quote.output.amount.ui : outputAmount;
251
+ const displayedInputAmount = activeSide === "output" && quoteState.quote
252
+ ? formatCurrencyAmount(quoteState.quote.input.amount.raw, quoteState.quote.input.currency.decimals)
253
+ : inputAmount;
254
+ const displayedOutputAmount = activeSide === "input" && quoteState.quote
255
+ ? formatCurrencyAmount(quoteState.quote.output.amount.raw, quoteState.quote.output.currency.decimals)
256
+ : outputAmount;
258
257
  const sourceBalance = sourceToken?.balance;
259
- const insufficientBalance = sourceBalance !== undefined && Number(displayedInputAmount) > sourceBalance;
258
+ const insufficientBalance = sourceBalance !== undefined && Number(displayedInputAmount) > Number(sourceBalance);
260
259
  const sameSwap = isSameSwap(sourceToken?.currency, destinationToken?.currency, inputAccount?.address, outputAddress);
261
260
  const canExecute = Boolean(quoteState.quote &&
262
261
  quoteRequest &&
@@ -300,10 +299,12 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
300
299
  swapState.setAmount("input", value);
301
300
  }, onSelectToken: () => setSourcePickerOpen(true) }), _jsx(SwitchButton, { type: "button", "aria-label": "Swap tokens", onClick: reverseTokens, disabled: !sourceToken || !destinationToken, children: _jsx(ArrowDownIcon, { size: 24 }) })] }), _jsx(CurrencyPanel, { side: "output", token: destinationToken, amount: displayedOutputAmount, quoteUsd: quoteState.quote?.output.amount.value_usd, quoteLoading: quoteState.loading && activeSide === "input", disabled: execution.stage.kind !== "idle", onAmountChange: (value) => {
302
301
  swapState.setAmount("output", value);
303
- }, onSelectToken: () => setDestinationPickerOpen(true), footerAction: showOutputWalletSelector ? (_jsx(OutputWalletSelector, { label: outputAddress ? truncateAddress(outputAddress) : "Connect Wallet", open: outputWalletMenuOpen, onOpenChange: setOutputWalletMenuOpen, onConnect: () => {
304
- setOutputWalletMenuOpen(false);
305
- setConnectTarget({ open: true, chainType: destinationChainType });
306
- }, onPaste: () => {
302
+ }, onSelectToken: () => setDestinationPickerOpen(true), footerAction: showOutputWalletSelector ? (_jsx(OutputWalletSelector, { label: outputAddress ? truncateAddress(outputAddress) : "Connect Wallet", open: outputWalletMenuOpen, onOpenChange: setOutputWalletMenuOpen, onConnect: separateOutputWallet
303
+ ? () => {
304
+ setOutputWalletMenuOpen(false);
305
+ setConnectTarget({ open: true, chainType: destinationChainType });
306
+ }
307
+ : undefined, onPaste: () => {
307
308
  setOutputWalletMenuOpen(false);
308
309
  setManualWalletOpen(true);
309
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) => {
@@ -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(): {
@@ -1,5 +1,5 @@
1
1
  import { useAccount } from "@coin-voyage/crypto/hooks";
2
- import { fetchTokenList, tokenToCurrency } from "@coin-voyage/shared/currency";
2
+ import { fetchTokenList, formatCurrencyAmount, tokenToCurrency } from "@coin-voyage/shared/currency";
3
3
  import { currencyID } from "@coin-voyage/shared/types";
4
4
  import { useEffect, useReducer, useState } from "react";
5
5
  import { useSwapKit } from "../providers/swapkit-context";
@@ -63,7 +63,7 @@ export function useTokenData() {
63
63
  walletKey,
64
64
  tokens: data.portfolio.flatMap((result) => result.currencies.map(({ balance, currency }) => ({
65
65
  currency,
66
- balance: Number(balance.ui),
66
+ balance: formatCurrencyAmount(balance.raw, currency.decimals),
67
67
  balanceUsd: balance.value_usd ?? undefined,
68
68
  }))),
69
69
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ // @vitest-environment jsdom
2
+ import { ChainId, ChainType } from "@coin-voyage/shared/types";
3
+ import { cleanup, renderHook, waitFor } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { useTokenData } from "./use-token-data";
6
+ const { api } = vi.hoisted(() => ({ api: { portfolio: vi.fn() } }));
7
+ vi.mock("@coin-voyage/crypto/hooks", () => ({
8
+ useAccount: () => ({ accounts: [{ address: "0x123", chainType: ChainType.EVM }] }),
9
+ }));
10
+ vi.mock("@coin-voyage/shared/currency", async (importOriginal) => ({
11
+ ...(await importOriginal()),
12
+ fetchTokenList: vi.fn().mockResolvedValue({ chains: [] }),
13
+ }));
14
+ vi.mock("../providers/swapkit-context", () => ({
15
+ useSwapKit: () => ({ api }),
16
+ }));
17
+ afterEach(() => {
18
+ cleanup();
19
+ vi.clearAllMocks();
20
+ });
21
+ describe("useTokenData", () => {
22
+ it.each([
23
+ { decimals: 6, raw: "1234567", ui: "1.23", expected: "1.234567" },
24
+ { decimals: 9, raw: "1239999999", ui: "1.24", expected: "1.239999999" },
25
+ { decimals: 18, raw: "1123456789012345678", ui: "1.12", expected: "1.123456789012345678" },
26
+ { decimals: 18, raw: "1", ui: "0.00", expected: "0.000000000000000001" },
27
+ ])("preserves the raw balance at $decimals decimals ($raw)", async ({ decimals, raw, ui, expected }) => {
28
+ const currency = { id: "eth", chain_id: ChainId.ETH, name: "Ether", symbol: "ETH", decimals };
29
+ api.portfolio.mockResolvedValue({
30
+ data: { portfolio: [{ currencies: [{ currency, balance: { raw, ui, value_usd: 12.34 } }] }] },
31
+ });
32
+ const { result } = renderHook(() => useTokenData());
33
+ await waitFor(() => expect(result.current.tokens).toHaveLength(1));
34
+ expect(result.current.tokens[0]).toEqual({ currency, balance: expected, balanceUsd: 12.34 });
35
+ });
36
+ });
@@ -1,3 +1,4 @@
1
+ export declare function balanceFraction(balance: string, fraction: number, decimals: number): string;
1
2
  export declare function isValidSwapAmountInput(value: string): boolean;
2
3
  export declare function toSwapAmountInputValue(amount?: string | null): string;
3
4
  export declare function shouldValidateSwapBalance(amount: string): boolean;
@@ -1,4 +1,11 @@
1
+ import { formatCurrencyAmount, parseCurrencyAmount } from "@coin-voyage/shared/currency";
1
2
  const DECIMAL_INPUT = /^(?:\d+(?:\.\d*)?|\.\d+)?$/;
3
+ export function balanceFraction(balance, fraction, decimals) {
4
+ if (fraction === 1)
5
+ return balance;
6
+ const raw = BigInt(parseCurrencyAmount(balance, decimals));
7
+ return formatCurrencyAmount((raw * BigInt(fraction * 100)) / 100n, decimals);
8
+ }
2
9
  export function isValidSwapAmountInput(value) {
3
10
  return DECIMAL_INPUT.test(value);
4
11
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { balanceFraction } from "./amount-input";
3
+ describe("balanceFraction", () => {
4
+ it.each(["1.239999999999999999", "0.000000000000000001", "9007199254740993.123456789012345678", "0"])("preserves the full MAX balance %s", (balance) => {
5
+ expect(balanceFraction(balance, 1, 18)).toBe(balance);
6
+ });
7
+ it.each([
8
+ [0.25, "0.309999999999999999"],
9
+ [0.5, "0.619999999999999999"],
10
+ [0.75, "0.929999999999999999"],
11
+ ])("floors a %s fraction to spendable base units", (fraction, expected) => {
12
+ expect(balanceFraction("1.239999999999999999", fraction, 18)).toBe(expected);
13
+ });
14
+ it("does not round a fraction up for indivisible tokens", () => {
15
+ expect(balanceFraction("3", 0.5, 0)).toBe("1");
16
+ });
17
+ });
@@ -15,10 +15,9 @@ interface GetSwapWalletRequirementProps {
15
15
  }
16
16
  export declare function shouldUseSeparateOutputWallet(sourceChainType: ChainType | undefined, destinationChainType: ChainType | undefined): boolean;
17
17
  interface ShouldShowOutputWalletSelectorProps {
18
- requiresSeparateOutputWallet: boolean;
19
18
  outputChainType?: ChainType;
20
19
  connectionRequirement: SwapWalletRequirement;
21
20
  }
22
- export declare function shouldShowOutputWalletSelector({ requiresSeparateOutputWallet, outputChainType, connectionRequirement, }: ShouldShowOutputWalletSelectorProps): boolean;
21
+ export declare function shouldShowOutputWalletSelector({ outputChainType, connectionRequirement, }: ShouldShowOutputWalletSelectorProps): boolean;
23
22
  export declare function getSwapWalletRequirement({ inputWalletAddress, inputChainType, shouldRestrictInputChainType, outputWalletAddress, outputChainType, }: GetSwapWalletRequirementProps): SwapWalletRequirement;
24
23
  export {};
@@ -2,8 +2,8 @@ import { getChainTypeName } from "@coin-voyage/shared/chain";
2
2
  export function shouldUseSeparateOutputWallet(sourceChainType, destinationChainType) {
3
3
  return Boolean(sourceChainType && destinationChainType && sourceChainType !== destinationChainType);
4
4
  }
5
- export function shouldShowOutputWalletSelector({ requiresSeparateOutputWallet, outputChainType, connectionRequirement, }) {
6
- return Boolean(outputChainType && requiresSeparateOutputWallet && connectionRequirement.target !== "input");
5
+ export function shouldShowOutputWalletSelector({ outputChainType, connectionRequirement, }) {
6
+ return Boolean(outputChainType && connectionRequirement.target !== "input");
7
7
  }
8
8
  export function getSwapWalletRequirement({ inputWalletAddress, inputChainType, shouldRestrictInputChainType = true, outputWalletAddress, outputChainType, }) {
9
9
  if (!inputWalletAddress) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { ChainType } from "@coin-voyage/shared/types";
2
+ import { describe, expect, it } from "vitest";
3
+ import { shouldShowOutputWalletSelector } from "./output-wallet";
4
+ describe("shouldShowOutputWalletSelector", () => {
5
+ it("allows changing the recipient when the input wallet is connected", () => {
6
+ expect(shouldShowOutputWalletSelector({
7
+ outputChainType: ChainType.EVM,
8
+ connectionRequirement: { target: "none" },
9
+ })).toBe(true);
10
+ });
11
+ it("allows choosing an address when an output wallet is required", () => {
12
+ expect(shouldShowOutputWalletSelector({
13
+ outputChainType: ChainType.SOL,
14
+ connectionRequirement: { target: "output", label: "Connect Wallet" },
15
+ })).toBe(true);
16
+ });
17
+ it("hides the selector until the input wallet is connected", () => {
18
+ expect(shouldShowOutputWalletSelector({
19
+ outputChainType: ChainType.EVM,
20
+ connectionRequirement: { target: "input", label: "Connect Wallet" },
21
+ })).toBe(false);
22
+ });
23
+ it("hides the selector without a destination chain", () => {
24
+ expect(shouldShowOutputWalletSelector({ connectionRequirement: { target: "none" } })).toBe(false);
25
+ });
26
+ });
@@ -1,4 +1,4 @@
1
- import type { ApiClient, APIEnvironment } from "@coin-voyage/shared/api";
1
+ import type { ApiClient, APIEnvironment } from "@coin-voyage/shared/api/browser";
2
2
  import type { SwapKitMode, SwapKitTheme } from "../types";
3
3
  export interface SwapKitContextValue {
4
4
  api: ApiClient;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useInWagmiContext } from "@coin-voyage/crypto/evm";
3
3
  import { useConnectCallback } from "@coin-voyage/crypto/hooks";
4
- import { ApiClient } from "@coin-voyage/shared/api";
4
+ import { ApiClient } from "@coin-voyage/shared/api/browser";
5
5
  import { generateSessionId } from "@coin-voyage/shared/utils";
6
6
  import { useMemo, useState } from "react";
7
7
  import { SwapKitContext } from "./swapkit-context";
@@ -42,7 +42,7 @@ export const SwapKitRoot = styled.div `
42
42
  display: block;
43
43
  width: 100%;
44
44
  color: var(--sk-text);
45
- background: var(--sk-background);
45
+ background: ${({ $customTheme }) => $customTheme.background ?? "transparent"};
46
46
  font-family: var(--sk-font-family);
47
47
  font-size: 16px;
48
48
  text-align: left;
@@ -1,7 +1,7 @@
1
1
  export const lightTheme = {
2
2
  fontFamily: `Roboto, Arial, Helvetica, sans-serif`,
3
3
  background: "#f4f5f7",
4
- surface: "#ffffff",
4
+ surface: "#f4f5f7",
5
5
  surfaceElevated: "#f7f8fa",
6
6
  border: "#e4e7ec",
7
7
  text: "#15181e",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coin-voyage/swapkit",
3
3
  "description": "A reusable cross-chain swap component for CoinVoyage.",
4
- "version": "0.3.0",
4
+ "version": "0.4.0-beta.0",
5
5
  "private": false,
6
6
  "sideEffects": false,
7
7
  "author": "Lars <lars@coinvoyage.io>",
@@ -22,8 +22,8 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "dependencies": {
25
- "@coin-voyage/crypto": "3.5.0",
26
- "@coin-voyage/shared": "3.5.0"
25
+ "@coin-voyage/crypto": "3.6.0-beta.0",
26
+ "@coin-voyage/shared": "3.6.0-beta.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@testing-library/react": "^16.3.0",
@@ -56,14 +56,14 @@
56
56
  "scripts": {
57
57
  "watch": "tsc -w -p ./tsconfig.json",
58
58
  "build": "tsc --build --force",
59
- "test": "vitest run --config ./vitest.config.ts",
60
- "test:coverage": "vitest run --coverage --maxWorkers=1 --no-file-parallelism --config ./vitest.config.ts",
61
59
  "clean": "rm -rf dist tsconfig.tsbuildinfo coverage",
60
+ "release:build": "pnpm clean && pnpm build",
62
61
  "pre:release": "pnpm version prerelease --preid=beta",
63
62
  "pre:publish": "pnpm publish --access public --tag beta --no-git-checks",
64
63
  "pre:steps": "pnpm pre:release && pnpm pre:publish",
65
- "release:build": "pnpm clean && pnpm build",
66
64
  "release": "pnpm publish --access public --no-git-checks",
67
- "type-check": "tsc --noEmit"
65
+ "type-check": "tsc --noEmit",
66
+ "test": "vitest run --config ./vitest.config.ts",
67
+ "test:coverage": "vitest run --coverage --maxWorkers=1 --no-file-parallelism --config ./vitest.config.ts"
68
68
  }
69
69
  }