@coin-voyage/swapkit 0.1.0-beta.2 → 0.2.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/dist/components/execution-modal.js +8 -2
- package/dist/components/styles.d.ts +2 -1
- package/dist/components/styles.js +59 -16
- package/dist/components/swap.d.ts +27 -1
- package/dist/components/swap.js +40 -17
- package/dist/hooks/use-swap-execution.d.ts +1 -1
- package/dist/hooks/use-swap-execution.js +69 -35
- package/dist/hooks/use-swap-state.d.ts +4 -1
- package/dist/hooks/use-swap-state.js +3 -3
- package/dist/hooks/use-token-data.d.ts +1 -0
- package/dist/hooks/use-token-data.js +4 -3
- package/dist/lib/currency-selection.js +3 -4
- package/dist/lib/execution.d.ts +12 -5
- package/dist/lib/quote.d.ts +1 -1
- package/dist/lib/quote.js +7 -7
- package/package.json +2 -2
|
@@ -36,16 +36,22 @@ function outcomeState(stage) {
|
|
|
36
36
|
if (!("swapData" in stage) || !stage.swapData)
|
|
37
37
|
return { tone: "muted", label: "Awaiting" };
|
|
38
38
|
if (stage.kind === "done") {
|
|
39
|
+
const transactionHash = stage.destinationTxHash ?? stage.sourceTxHash;
|
|
40
|
+
const chainId = stage.destinationTxHash
|
|
41
|
+
? stage.swapData.data.output.currency.chain_id
|
|
42
|
+
: stage.swapData.data.input.currency.chain_id;
|
|
39
43
|
return {
|
|
40
44
|
tone: "complete",
|
|
41
|
-
explorerUrl: getChainExplorerTxUrl(
|
|
45
|
+
explorerUrl: transactionHash ? getChainExplorerTxUrl(chainId, transactionHash) : undefined,
|
|
42
46
|
};
|
|
43
47
|
}
|
|
44
48
|
if (stage.kind === "refunded") {
|
|
45
49
|
return {
|
|
46
50
|
tone: "warning",
|
|
47
51
|
label: "Refunded",
|
|
48
|
-
explorerUrl:
|
|
52
|
+
explorerUrl: stage.refundTxHash
|
|
53
|
+
? getChainExplorerTxUrl(stage.swapData.data.input.currency.chain_id, stage.refundTxHash)
|
|
54
|
+
: undefined,
|
|
49
55
|
};
|
|
50
56
|
}
|
|
51
57
|
return { tone: "muted", label: "Awaiting" };
|
|
@@ -28,6 +28,8 @@ export declare const Hint: import("styled-components").StyledComponent<"p", any,
|
|
|
28
28
|
export declare const CardWrap: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
29
29
|
export declare const CurrencyCard: import("styled-components").StyledComponent<"section", any, {}, never>;
|
|
30
30
|
export declare const CardTitle: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
31
|
+
export declare const QuickAmountActions: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
32
|
+
export declare const QuickAmountButton: import("styled-components").StyledComponent<"button", any, {}, never>;
|
|
31
33
|
export declare const AmountRow: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
32
34
|
export declare const AmountInput: import("styled-components").StyledComponent<"input", any, {}, never>;
|
|
33
35
|
export declare const AmountSkeleton: import("styled-components").StyledComponent<"span", any, {}, never>;
|
|
@@ -44,7 +46,6 @@ export declare const ChainBadge: import("styled-components").StyledComponent<"sp
|
|
|
44
46
|
}, never>;
|
|
45
47
|
export declare const CardFooter: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
46
48
|
export declare const InlineActions: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
47
|
-
export declare const TextButton: import("styled-components").StyledComponent<"button", any, {}, never>;
|
|
48
49
|
export declare const OutputWalletControl: import("styled-components").StyledComponent<"span", any, {}, never>;
|
|
49
50
|
export declare const OutputWalletTrigger: import("styled-components").StyledComponent<"button", any, {}, never>;
|
|
50
51
|
export declare const OutputWalletMenu: import("styled-components").StyledComponent<"span", any, {}, never>;
|
|
@@ -199,14 +199,30 @@ export const CardWrap = styled.div `
|
|
|
199
199
|
position: relative;
|
|
200
200
|
`;
|
|
201
201
|
export const CurrencyCard = styled.section `
|
|
202
|
+
position: relative;
|
|
202
203
|
width: 100%;
|
|
203
|
-
min-height:
|
|
204
|
-
padding: 14px;
|
|
204
|
+
min-height: 128px;
|
|
205
|
+
padding: 14px 14px 8px;
|
|
205
206
|
border: 0;
|
|
206
207
|
border-style: none;
|
|
207
208
|
border-radius: var(--sk-radius);
|
|
208
209
|
color: var(--sk-text);
|
|
209
210
|
background: var(--sk-surface);
|
|
211
|
+
|
|
212
|
+
&:hover [data-quick-amounts],
|
|
213
|
+
&:focus-within [data-quick-amounts] {
|
|
214
|
+
opacity: 1;
|
|
215
|
+
pointer-events: auto;
|
|
216
|
+
transform: translateY(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@media (hover: none) {
|
|
220
|
+
[data-quick-amounts] {
|
|
221
|
+
opacity: 1;
|
|
222
|
+
pointer-events: auto;
|
|
223
|
+
transform: translateY(0);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
210
226
|
`;
|
|
211
227
|
export const CardTitle = styled.div `
|
|
212
228
|
margin-bottom: 12px;
|
|
@@ -215,6 +231,47 @@ export const CardTitle = styled.div `
|
|
|
215
231
|
font-weight: 500;
|
|
216
232
|
line-height: 16px;
|
|
217
233
|
`;
|
|
234
|
+
export const QuickAmountActions = styled.div `
|
|
235
|
+
position: absolute;
|
|
236
|
+
top: 10px;
|
|
237
|
+
right: 12px;
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 4px;
|
|
241
|
+
opacity: 0;
|
|
242
|
+
pointer-events: none;
|
|
243
|
+
transform: translateY(-2px);
|
|
244
|
+
transition:
|
|
245
|
+
opacity 120ms ease,
|
|
246
|
+
transform 120ms ease;
|
|
247
|
+
|
|
248
|
+
@media (prefers-reduced-motion: reduce) {
|
|
249
|
+
transition: none;
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
export const QuickAmountButton = styled.button `
|
|
253
|
+
min-width: 34px;
|
|
254
|
+
height: 24px;
|
|
255
|
+
padding: 0 7px;
|
|
256
|
+
border: 1px solid var(--sk-border);
|
|
257
|
+
border-radius: 999px;
|
|
258
|
+
color: var(--sk-text);
|
|
259
|
+
background: var(--sk-surface-elevated);
|
|
260
|
+
cursor: pointer;
|
|
261
|
+
font-size: 11px;
|
|
262
|
+
font-weight: 600;
|
|
263
|
+
line-height: 22px;
|
|
264
|
+
|
|
265
|
+
&:hover:not(:disabled) {
|
|
266
|
+
border-color: var(--sk-accent);
|
|
267
|
+
color: var(--sk-accent);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
&:disabled {
|
|
271
|
+
cursor: not-allowed;
|
|
272
|
+
opacity: 0.5;
|
|
273
|
+
}
|
|
274
|
+
`;
|
|
218
275
|
export const AmountRow = styled.div `
|
|
219
276
|
display: grid;
|
|
220
277
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
@@ -371,20 +428,6 @@ export const InlineActions = styled.div `
|
|
|
371
428
|
align-items: center;
|
|
372
429
|
gap: 8px;
|
|
373
430
|
`;
|
|
374
|
-
export const TextButton = styled.button `
|
|
375
|
-
padding: 0;
|
|
376
|
-
border: 0;
|
|
377
|
-
color: var(--sk-accent);
|
|
378
|
-
background: transparent;
|
|
379
|
-
cursor: pointer;
|
|
380
|
-
font-size: 12px;
|
|
381
|
-
font-weight: 600;
|
|
382
|
-
|
|
383
|
-
&:disabled {
|
|
384
|
-
cursor: not-allowed;
|
|
385
|
-
opacity: 0.5;
|
|
386
|
-
}
|
|
387
|
-
`;
|
|
388
431
|
export const OutputWalletControl = styled.span `
|
|
389
432
|
position: relative;
|
|
390
433
|
display: inline-flex;
|
|
@@ -1,2 +1,28 @@
|
|
|
1
|
-
import type
|
|
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?: number;
|
|
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;
|
|
2
27
|
export declare function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaultSlippageBps, onComplete, onError, }?: SwapProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export {};
|
package/dist/components/swap.js
CHANGED
|
@@ -16,7 +16,7 @@ import { ExecutionModal } from "./execution-modal";
|
|
|
16
16
|
import { ConnectedWallets } from "./connected-wallets";
|
|
17
17
|
import { ArrowDownIcon, ChevronDownIcon, FuelIcon, InfoIcon, PasteIcon, SettingsIcon, WalletIcon } from "./icons";
|
|
18
18
|
import { Modal } from "./modal";
|
|
19
|
-
import { AmountInput, AmountRow, AmountSkeleton, BreakdownBody, BreakdownCard, BreakdownMeta, BreakdownToggle, CardFooter, CardTitle, CardWrap, CompactInput, CurrencyCard, DetailRow, ErrorBox, FieldLabel, FormStack, FreeFeeBadge, Hint, IconButton, InlineActions, InfoButton, InfoWrap, NetworkCost, OutputWalletControl, OutputWalletMenu, OutputWalletOption, OutputWalletTrigger, PrimaryButton, QuoteValueSkeleton, SettingsLabel, SettingsPanel, SettingsRow, SettingsTooltip, SlippageControl, SlippageInput, SlippageInputWrap, SlippageModeButton, Spinner, SwitchButton,
|
|
19
|
+
import { AmountInput, AmountRow, AmountSkeleton, BreakdownBody, BreakdownCard, BreakdownMeta, BreakdownToggle, CardFooter, CardTitle, CardWrap, CompactInput, CurrencyCard, DetailRow, ErrorBox, FieldLabel, FormStack, FreeFeeBadge, Hint, IconButton, InlineActions, InfoButton, InfoWrap, NetworkCost, OutputWalletControl, OutputWalletMenu, OutputWalletOption, OutputWalletTrigger, PrimaryButton, QuoteValueSkeleton, QuickAmountActions, QuickAmountButton, SettingsLabel, SettingsPanel, SettingsRow, SettingsTooltip, SlippageControl, SlippageInput, SlippageInputWrap, SlippageModeButton, Spinner, SwitchButton, TokenButton, Toolbar, ToolbarGroup, WalletRequirementButton, } from "./styles";
|
|
20
20
|
import { TokenIcon } from "./token-icon";
|
|
21
21
|
import { TokenPicker } from "./token-picker";
|
|
22
22
|
import { WalletConnector } from "./wallet-connector";
|
|
@@ -54,24 +54,30 @@ function SlippageInfoTooltip({ id }) {
|
|
|
54
54
|
const open = hovered || focused;
|
|
55
55
|
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 }))] }));
|
|
56
56
|
}
|
|
57
|
-
function useSwapNotifications({ stage, quoteError, tokenError, onComplete, onError, }) {
|
|
57
|
+
export function useSwapNotifications({ stage, quoteError, tokenError, onBalancesChanged, onComplete, onError, }) {
|
|
58
58
|
const onCompleteRef = useRef(onComplete);
|
|
59
59
|
const onErrorRef = useRef(onError);
|
|
60
|
+
const onBalancesChangedRef = useRef(onBalancesChanged);
|
|
60
61
|
const lastNotification = useRef(undefined);
|
|
61
62
|
useEffect(() => {
|
|
62
63
|
onCompleteRef.current = onComplete;
|
|
63
64
|
onErrorRef.current = onError;
|
|
64
|
-
|
|
65
|
+
onBalancesChangedRef.current = onBalancesChanged;
|
|
66
|
+
}, [onBalancesChanged, onComplete, onError]);
|
|
65
67
|
useEffect(() => {
|
|
66
68
|
if (stage.kind === "idle") {
|
|
67
69
|
lastNotification.current = undefined;
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
72
|
if (stage.kind === "done") {
|
|
71
|
-
const
|
|
73
|
+
const transactionHash = stage.destinationTxHash ?? stage.sourceTxHash;
|
|
74
|
+
if (!transactionHash)
|
|
75
|
+
return;
|
|
76
|
+
const key = `complete:${stage.orderId}:${transactionHash}`;
|
|
72
77
|
if (lastNotification.current !== key) {
|
|
73
78
|
lastNotification.current = key;
|
|
74
|
-
|
|
79
|
+
onBalancesChangedRef.current();
|
|
80
|
+
onCompleteRef.current?.({ orderId: stage.orderId, transactionHash });
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
else if (stage.kind === "error") {
|
|
@@ -131,12 +137,26 @@ function tokenUsdValue(token, amount) {
|
|
|
131
137
|
return undefined;
|
|
132
138
|
return token.currency.price_usd * numericAmount;
|
|
133
139
|
}
|
|
134
|
-
|
|
140
|
+
const QUICK_AMOUNTS = [
|
|
141
|
+
{ label: "25%", fraction: 0.25 },
|
|
142
|
+
{ label: "50%", fraction: 0.5 },
|
|
143
|
+
{ label: "75%", fraction: 0.75 },
|
|
144
|
+
{ label: "Max", fraction: 1 },
|
|
145
|
+
];
|
|
146
|
+
function balanceFraction(balance, fraction, decimals) {
|
|
147
|
+
if (fraction === 1)
|
|
148
|
+
return balance.toString();
|
|
149
|
+
return Number((balance * fraction).toFixed(Math.min(Math.max(decimals, 0), 18))).toString();
|
|
150
|
+
}
|
|
151
|
+
export function CurrencyPanel({ side, token, amount, balance, quoteUsd, quoteLoading, disabled, onAmountChange, onSelectToken, footerAction, }) {
|
|
135
152
|
const label = side === "input" ? "SELL" : "BUY";
|
|
136
|
-
|
|
153
|
+
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
|
|
155
|
+
? `Use maximum ${token.currency.symbol} balance`
|
|
156
|
+
: `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) => {
|
|
137
157
|
if (isValidSwapAmountInput(event.target.value))
|
|
138
158
|
onAmountChange(event.target.value);
|
|
139
|
-
} })), _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()] }),
|
|
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] })] })] }));
|
|
140
160
|
}
|
|
141
161
|
function ManualWalletModal({ open, chainId, initialValue, onClose, onSave, }) {
|
|
142
162
|
const [value, setValue] = useState(initialValue ?? "");
|
|
@@ -156,7 +176,7 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
|
|
|
156
176
|
const { mode, theme } = useSwapKit();
|
|
157
177
|
const slippageInputId = useId();
|
|
158
178
|
const slippageTooltipId = useId();
|
|
159
|
-
const { tokens, accounts, isLoading, error: tokenError } = useTokenData();
|
|
179
|
+
const { tokens, accounts, isLoading, error: tokenError, refreshBalances } = useTokenData();
|
|
160
180
|
const { accounts: connectedAccounts } = useAccount();
|
|
161
181
|
const [sourcePickerOpen, setSourcePickerOpen] = useState(false);
|
|
162
182
|
const [destinationPickerOpen, setDestinationPickerOpen] = useState(false);
|
|
@@ -223,9 +243,16 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
|
|
|
223
243
|
slippageBps,
|
|
224
244
|
sourceToken?.currency,
|
|
225
245
|
]);
|
|
226
|
-
const execution = useSwapExecution(sourceChainType);
|
|
246
|
+
const execution = useSwapExecution(sourceChainType, inputAccount?.address);
|
|
227
247
|
const quoteState = useSwapQuote(quoteRequest, execution.stage.kind === "idle");
|
|
228
|
-
useSwapNotifications({
|
|
248
|
+
useSwapNotifications({
|
|
249
|
+
stage: execution.stage,
|
|
250
|
+
quoteError: quoteState.error,
|
|
251
|
+
tokenError,
|
|
252
|
+
onBalancesChanged: refreshBalances,
|
|
253
|
+
onComplete,
|
|
254
|
+
onError,
|
|
255
|
+
});
|
|
229
256
|
const displayedInputAmount = activeSide === "output" && quoteState.quote ? quoteState.quote.input.amount.ui : inputAmount;
|
|
230
257
|
const displayedOutputAmount = activeSide === "input" && quoteState.quote ? quoteState.quote.output.amount.ui : outputAmount;
|
|
231
258
|
const sourceBalance = sourceToken?.balance;
|
|
@@ -271,11 +298,7 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
|
|
|
271
298
|
} }), _jsx("span", { children: "%" })] })] })] }), slippageMode === "custom" &&
|
|
272
299
|
(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) => {
|
|
273
300
|
swapState.setAmount("input", value);
|
|
274
|
-
}, onSelectToken: () => setSourcePickerOpen(true),
|
|
275
|
-
? undefined
|
|
276
|
-
: () => {
|
|
277
|
-
swapState.setAmount("input", sourceBalance.toString());
|
|
278
|
-
} }), _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
|
+
}, 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) => {
|
|
279
302
|
swapState.setAmount("output", value);
|
|
280
303
|
}, onSelectToken: () => setDestinationPickerOpen(true), footerAction: showOutputWalletSelector ? (_jsx(OutputWalletSelector, { label: outputAddress ? truncateAddress(outputAddress) : "Connect Wallet", open: outputWalletMenuOpen, onOpenChange: setOutputWalletMenuOpen, onConnect: () => {
|
|
281
304
|
setOutputWalletMenuOpen(false);
|
|
@@ -290,6 +313,6 @@ export function Swap({ defaultSourceCurrency, defaultDestinationCurrency, defaul
|
|
|
290
313
|
const terminal = ["done", "refunded"].includes(execution.stage.kind);
|
|
291
314
|
execution.reset();
|
|
292
315
|
if (terminal)
|
|
293
|
-
swapState.reset();
|
|
316
|
+
swapState.reset({ preserveCurrencies: execution.stage.kind === "done" });
|
|
294
317
|
} })] }) }));
|
|
295
318
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChainType, SwapExecuteRequest } from "@coin-voyage/shared/types";
|
|
2
2
|
import { type SwapStage } from "../lib/execution";
|
|
3
|
-
export declare function useSwapExecution(chainType: ChainType | undefined): {
|
|
3
|
+
export declare function useSwapExecution(chainType: ChainType | undefined, currentFromAddress: string | undefined): {
|
|
4
4
|
stage: SwapStage;
|
|
5
5
|
execute: (request: SwapExecuteRequest, fromAddress: string) => Promise<void>;
|
|
6
6
|
reset: () => void;
|
|
@@ -1,34 +1,51 @@
|
|
|
1
1
|
import { usePrepareTransaction } from "@coin-voyage/crypto/hooks";
|
|
2
|
+
import { assertActiveWallet, getErrorMessage, isUserRejectedError, prepareForActiveWallet, } from "@coin-voyage/crypto/utils";
|
|
2
3
|
import { WebhookEventType } from "@coin-voyage/shared/types";
|
|
3
|
-
import { useCallback,
|
|
4
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
5
|
import { buildSwapExecuteTransactions } from "../lib/execution";
|
|
5
6
|
import { useSwapKit } from "../providers/swapkit-context";
|
|
6
7
|
import { useOrderStatus } from "./use-order-status";
|
|
7
8
|
function errorMessage(error, fallback) {
|
|
8
|
-
const message = error
|
|
9
|
+
const message = getErrorMessage(error).trim();
|
|
9
10
|
return message || fallback;
|
|
10
11
|
}
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
function watchedOrderId(stage) {
|
|
13
|
+
if (stage.kind === "awaiting_payment" || stage.kind === "source_confirming" || stage.kind === "executing") {
|
|
14
|
+
return stage.orderId;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
14
17
|
}
|
|
15
|
-
export function useSwapExecution(chainType) {
|
|
18
|
+
export function useSwapExecution(chainType, currentFromAddress) {
|
|
16
19
|
const { api } = useSwapKit();
|
|
17
20
|
const transactionActions = usePrepareTransaction(chainType);
|
|
18
|
-
const [stage,
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
+
const [stage, setStageState] = useState({ kind: "idle" });
|
|
22
|
+
const stageRef = useRef({ kind: "idle" });
|
|
23
|
+
const currentWalletRef = useRef({ address: currentFromAddress, chainType });
|
|
24
|
+
const transactionActionsRef = useRef(transactionActions);
|
|
21
25
|
const runId = useRef(0);
|
|
22
|
-
|
|
23
|
-
stageRef.current =
|
|
24
|
-
|
|
26
|
+
const setStage = useCallback((next) => {
|
|
27
|
+
stageRef.current = next;
|
|
28
|
+
setStageState(next);
|
|
29
|
+
}, []);
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
const chainChanged = currentWalletRef.current?.chainType !== chainType;
|
|
32
|
+
currentWalletRef.current = { address: currentFromAddress, chainType };
|
|
33
|
+
transactionActionsRef.current = transactionActions;
|
|
34
|
+
if (chainChanged) {
|
|
35
|
+
runId.current += 1;
|
|
36
|
+
if (stageRef.current.kind !== "idle")
|
|
37
|
+
setStage({ kind: "idle" });
|
|
38
|
+
}
|
|
39
|
+
}, [chainType, currentFromAddress, setStage, transactionActions]);
|
|
25
40
|
const reset = useCallback(() => {
|
|
26
41
|
runId.current += 1;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}, []);
|
|
42
|
+
if (stageRef.current.kind !== "idle")
|
|
43
|
+
setStage({ kind: "idle" });
|
|
44
|
+
}, [setStage]);
|
|
30
45
|
const onOrderEvent = useCallback((event) => {
|
|
31
46
|
const current = stageRef.current;
|
|
47
|
+
if (current.kind === "done" || current.kind === "refunded" || current.kind === "error")
|
|
48
|
+
return;
|
|
32
49
|
if (!("swapData" in current) || !current.swapData || event.order.id !== current.swapData.order_id)
|
|
33
50
|
return;
|
|
34
51
|
const swapData = event.order.payment
|
|
@@ -36,55 +53,71 @@ export function useSwapExecution(chainType) {
|
|
|
36
53
|
: { ...current.swapData, status: event.order.status };
|
|
37
54
|
const sourceTxHash = event.order.payment?.source_tx_hash ?? ("sourceTxHash" in current ? current.sourceTxHash : undefined);
|
|
38
55
|
if (event.event === WebhookEventType.ORDER_COMPLETED) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
const destinationTxHash = event.order.payment?.destination_tx_hash ?? undefined;
|
|
57
|
+
runId.current += 1;
|
|
58
|
+
if (destinationTxHash) {
|
|
59
|
+
setStage({ kind: "done", swapData, orderId: event.order.id, sourceTxHash, destinationTxHash });
|
|
60
|
+
}
|
|
61
|
+
else if (sourceTxHash) {
|
|
62
|
+
setStage({ kind: "done", swapData, orderId: event.order.id, sourceTxHash });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
setStage({ kind: "error", swapData, message: "Swap completed without a transaction hash" });
|
|
66
|
+
}
|
|
43
67
|
}
|
|
44
|
-
else if (event.event === WebhookEventType.ORDER_REFUNDED
|
|
68
|
+
else if (event.event === WebhookEventType.ORDER_REFUNDED) {
|
|
69
|
+
const refundTxHash = event.order.payment?.refund_tx_hash ?? undefined;
|
|
70
|
+
runId.current += 1;
|
|
45
71
|
setStage({
|
|
46
72
|
kind: "refunded",
|
|
47
73
|
swapData,
|
|
48
74
|
orderId: event.order.id,
|
|
49
75
|
sourceTxHash,
|
|
50
|
-
refundTxHash:
|
|
76
|
+
...(refundTxHash ? { refundTxHash } : {}),
|
|
51
77
|
});
|
|
52
|
-
setOrderId(undefined);
|
|
53
78
|
}
|
|
54
79
|
else if (event.event === WebhookEventType.ORDER_ERROR) {
|
|
80
|
+
runId.current += 1;
|
|
55
81
|
setStage({ kind: "error", swapData, message: "Swap failed" });
|
|
56
|
-
setOrderId(undefined);
|
|
57
82
|
}
|
|
58
83
|
else if (event.event === WebhookEventType.ORDER_EXECUTING) {
|
|
59
84
|
setStage({ kind: "executing", swapData, orderId: event.order.id, sourceTxHash });
|
|
60
85
|
}
|
|
61
86
|
else if (event.event === WebhookEventType.ORDER_CONFIRMING ||
|
|
62
87
|
event.event === WebhookEventType.ORDER_PARTIAL_PAYMENT) {
|
|
88
|
+
if (current.kind === "executing")
|
|
89
|
+
return;
|
|
63
90
|
setStage({ kind: "source_confirming", swapData, orderId: event.order.id, sourceTxHash });
|
|
64
91
|
}
|
|
65
|
-
}, []);
|
|
66
|
-
useOrderStatus({ api, orderId, onEvent: onOrderEvent });
|
|
92
|
+
}, [setStage]);
|
|
93
|
+
useOrderStatus({ api, orderId: watchedOrderId(stage), onEvent: onOrderEvent });
|
|
67
94
|
const execute = useCallback(async (request, fromAddress) => {
|
|
68
|
-
if (!
|
|
95
|
+
if (!transactionActionsRef.current || !chainType)
|
|
69
96
|
return;
|
|
70
97
|
const currentRun = ++runId.current;
|
|
71
98
|
let swapData = null;
|
|
72
99
|
setStage({ kind: "confirming" });
|
|
73
100
|
try {
|
|
74
|
-
const response = await api.swapExecute(
|
|
101
|
+
const { wallet, value: response } = await prepareForActiveWallet({ address: fromAddress, chainType }, () => currentWalletRef.current, ({ address }) => api.swapExecute({
|
|
102
|
+
...request,
|
|
103
|
+
source: { ...request.source, sender_address: address },
|
|
104
|
+
}));
|
|
75
105
|
if (currentRun !== runId.current)
|
|
76
106
|
return;
|
|
77
107
|
if (response.error || !response.data) {
|
|
78
108
|
setStage({ kind: "error", swapData: null, message: response.error?.message || "Failed to prepare swap" });
|
|
79
109
|
return;
|
|
80
110
|
}
|
|
111
|
+
const paymentTransactionActions = transactionActionsRef.current;
|
|
112
|
+
if (!paymentTransactionActions)
|
|
113
|
+
throw new Error("Transaction actions must be defined");
|
|
81
114
|
swapData = response.data;
|
|
82
|
-
|
|
83
|
-
const transactions = buildSwapExecuteTransactions(swapData, fromAddress);
|
|
115
|
+
const transactions = buildSwapExecuteTransactions(swapData, wallet.address);
|
|
84
116
|
let sourceTxHash;
|
|
85
117
|
for (const [paymentStepIndex, transaction] of transactions.entries()) {
|
|
86
|
-
setStage({ kind: "awaiting_payment", swapData, paymentStepIndex });
|
|
87
|
-
|
|
118
|
+
setStage({ kind: "awaiting_payment", swapData, orderId: swapData.order_id, paymentStepIndex });
|
|
119
|
+
assertActiveWallet(wallet, currentWalletRef.current);
|
|
120
|
+
sourceTxHash = await paymentTransactionActions.execute(transaction);
|
|
88
121
|
if (currentRun !== runId.current)
|
|
89
122
|
return;
|
|
90
123
|
if (!sourceTxHash)
|
|
@@ -93,13 +126,14 @@ export function useSwapExecution(chainType) {
|
|
|
93
126
|
setStage({ kind: "source_confirming", swapData, orderId: swapData.order_id, sourceTxHash });
|
|
94
127
|
}
|
|
95
128
|
catch (error) {
|
|
96
|
-
if (
|
|
129
|
+
if (currentRun !== runId.current)
|
|
130
|
+
return;
|
|
131
|
+
if (isUserRejectedError(error)) {
|
|
97
132
|
reset();
|
|
98
133
|
return;
|
|
99
134
|
}
|
|
100
|
-
setOrderId(undefined);
|
|
101
135
|
setStage({ kind: "error", swapData, message: errorMessage(error, "Transaction failed") });
|
|
102
136
|
}
|
|
103
|
-
}, [api, reset,
|
|
104
|
-
return { stage, execute, reset };
|
|
137
|
+
}, [api, chainType, reset, setStage]);
|
|
138
|
+
return useMemo(() => ({ stage, execute, reset }), [execute, reset, stage]);
|
|
105
139
|
}
|
|
@@ -47,6 +47,7 @@ export type SwapStateAction = {
|
|
|
47
47
|
wallet?: ManualOutputWallet;
|
|
48
48
|
} | {
|
|
49
49
|
type: "reset";
|
|
50
|
+
preserveCurrencies?: boolean;
|
|
50
51
|
};
|
|
51
52
|
export declare function createSwapState(defaultSlippageBps?: number): SwapState;
|
|
52
53
|
export declare function swapStateReducer(state: SwapState, action: SwapStateAction): SwapState;
|
|
@@ -65,6 +66,8 @@ export declare function useSwapState({ currencies, connectedChainTypes, defaultS
|
|
|
65
66
|
setSlippageMode: (mode: SlippageMode) => void;
|
|
66
67
|
setSlippagePercent: (value: string) => void;
|
|
67
68
|
setManualOutputWallet: (wallet?: ManualOutputWallet) => void;
|
|
68
|
-
reset: (
|
|
69
|
+
reset: (options?: {
|
|
70
|
+
preserveCurrencies?: boolean;
|
|
71
|
+
}) => void;
|
|
69
72
|
};
|
|
70
73
|
export {};
|
|
@@ -72,8 +72,8 @@ export function swapStateReducer(state, action) {
|
|
|
72
72
|
case "reset":
|
|
73
73
|
return {
|
|
74
74
|
...state,
|
|
75
|
-
sourceId: state.initialSourceId,
|
|
76
|
-
destinationId: state.initialDestinationId,
|
|
75
|
+
sourceId: action.preserveCurrencies ? state.sourceId : state.initialSourceId,
|
|
76
|
+
destinationId: action.preserveCurrencies ? state.destinationId : state.initialDestinationId,
|
|
77
77
|
inputAmount: "",
|
|
78
78
|
outputAmount: "",
|
|
79
79
|
activeSide: "input",
|
|
@@ -122,7 +122,7 @@ export function useSwapState({ currencies, connectedChainTypes, defaultSourceCur
|
|
|
122
122
|
const setSlippagePercent = useCallback((value) => dispatch({ type: "set_slippage", value }), []);
|
|
123
123
|
const setSlippageMode = useCallback((mode) => dispatch({ type: "set_slippage_mode", mode }), []);
|
|
124
124
|
const setManualOutputWallet = useCallback((wallet) => dispatch({ type: "set_manual_output_wallet", wallet }), []);
|
|
125
|
-
const reset = useCallback(() => dispatch({ type: "reset" }), []);
|
|
125
|
+
const reset = useCallback((options) => dispatch({ type: "reset", ...options }), []);
|
|
126
126
|
const resolvedState = !state.currenciesInitialized && currencies.length > 0
|
|
127
127
|
? swapStateReducer(state, {
|
|
128
128
|
type: "initialize_currencies",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useAccount } from "@coin-voyage/crypto/hooks";
|
|
2
2
|
import { fetchTokenList, tokenToCurrency } from "@coin-voyage/shared/currency";
|
|
3
3
|
import { currencyID } from "@coin-voyage/shared/types";
|
|
4
|
-
import { useEffect, useState } from "react";
|
|
4
|
+
import { useEffect, useReducer, useState } from "react";
|
|
5
5
|
import { useSwapKit } from "../providers/swapkit-context";
|
|
6
6
|
function mergeTokens(tokenList, portfolio) {
|
|
7
7
|
const portfolioById = new Map(portfolio.map((token) => [currencyID(token.currency), token]));
|
|
@@ -21,6 +21,7 @@ export function useTokenData() {
|
|
|
21
21
|
const [isLoading, setIsLoading] = useState(true);
|
|
22
22
|
const [tokenError, setTokenError] = useState(null);
|
|
23
23
|
const [portfolioError, setPortfolioError] = useState(null);
|
|
24
|
+
const [portfolioRevision, refreshBalances] = useReducer((revision) => revision + 1, 0);
|
|
24
25
|
useEffect(() => {
|
|
25
26
|
let active = true;
|
|
26
27
|
fetchTokenList()
|
|
@@ -73,8 +74,8 @@ export function useTokenData() {
|
|
|
73
74
|
}
|
|
74
75
|
});
|
|
75
76
|
return () => controller.abort();
|
|
76
|
-
}, [api, walletKey]);
|
|
77
|
+
}, [api, portfolioRevision, walletKey]);
|
|
77
78
|
const activePortfolioTokens = portfolioState.walletKey === walletKey ? portfolioState.tokens : [];
|
|
78
79
|
const tokens = mergeTokens(listedCurrencies, activePortfolioTokens);
|
|
79
|
-
return { tokens, accounts, isLoading, error: tokenError ?? portfolioError };
|
|
80
|
+
return { tokens, accounts, isLoading, error: tokenError ?? portfolioError, refreshBalances };
|
|
80
81
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { addressesMatch } from "@coin-voyage/crypto/utils";
|
|
1
2
|
import { getChainTypeByChainId } from "@coin-voyage/shared/chain";
|
|
2
3
|
import { currencyID } from "@coin-voyage/shared/types";
|
|
3
|
-
function normalizedAddress(currency) {
|
|
4
|
-
return currency.address?.toLowerCase() ?? null;
|
|
5
|
-
}
|
|
6
4
|
export function currenciesMatch(left, right) {
|
|
7
|
-
return left.chain_id === right.chain_id &&
|
|
5
|
+
return (left.chain_id === right.chain_id &&
|
|
6
|
+
addressesMatch(left.address, right.address, getChainTypeByChainId(left.chain_id)));
|
|
8
7
|
}
|
|
9
8
|
export function resolveCurrencySelection({ currencies, connectedChainTypes, defaultSourceCurrency, defaultDestinationCurrency, }) {
|
|
10
9
|
const preferredSource = defaultSourceCurrency
|
package/dist/lib/execution.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { ExecuteTransaction } from "@coin-voyage/crypto/types";
|
|
2
2
|
import type { SwapExecuteResponse } from "@coin-voyage/shared/types";
|
|
3
|
+
type CompletedTransactionHashes = {
|
|
4
|
+
sourceTxHash: string;
|
|
5
|
+
destinationTxHash?: string;
|
|
6
|
+
} | {
|
|
7
|
+
sourceTxHash?: string;
|
|
8
|
+
destinationTxHash: string;
|
|
9
|
+
};
|
|
3
10
|
export type SwapStage = {
|
|
4
11
|
kind: "idle";
|
|
5
12
|
} | {
|
|
@@ -7,6 +14,7 @@ export type SwapStage = {
|
|
|
7
14
|
} | {
|
|
8
15
|
kind: "awaiting_payment";
|
|
9
16
|
swapData: SwapExecuteResponse;
|
|
17
|
+
orderId: string;
|
|
10
18
|
paymentStepIndex: number;
|
|
11
19
|
} | {
|
|
12
20
|
kind: "source_confirming";
|
|
@@ -18,21 +26,20 @@ export type SwapStage = {
|
|
|
18
26
|
swapData: SwapExecuteResponse;
|
|
19
27
|
orderId: string;
|
|
20
28
|
sourceTxHash?: string;
|
|
21
|
-
} | {
|
|
29
|
+
} | ({
|
|
22
30
|
kind: "done";
|
|
23
31
|
swapData: SwapExecuteResponse;
|
|
24
32
|
orderId: string;
|
|
25
|
-
|
|
26
|
-
txHash: string;
|
|
27
|
-
} | {
|
|
33
|
+
} & CompletedTransactionHashes) | {
|
|
28
34
|
kind: "refunded";
|
|
29
35
|
swapData: SwapExecuteResponse;
|
|
30
36
|
orderId: string;
|
|
31
37
|
sourceTxHash?: string;
|
|
32
|
-
refundTxHash
|
|
38
|
+
refundTxHash?: string;
|
|
33
39
|
} | {
|
|
34
40
|
kind: "error";
|
|
35
41
|
swapData: SwapExecuteResponse | null;
|
|
36
42
|
message: string;
|
|
37
43
|
};
|
|
38
44
|
export declare function buildSwapExecuteTransactions(swapData: SwapExecuteResponse, fromAddress: string): ExecuteTransaction[];
|
|
45
|
+
export {};
|
package/dist/lib/quote.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CurrencyBase, SwapQuoteRequest } from "@coin-voyage/shared/types";
|
|
2
2
|
export type SwapSide = "input" | "output";
|
|
3
3
|
export declare function toCurrencyBase(currency: CurrencyBase): CurrencyBase;
|
|
4
4
|
interface BuildSwapQuoteRequestProps {
|
package/dist/lib/quote.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addressesMatch } from "@coin-voyage/crypto/utils";
|
|
2
|
+
import { getChainTypeByChainId } from "@coin-voyage/shared/chain";
|
|
3
|
+
import { currenciesMatch } from "./currency-selection";
|
|
2
4
|
export function toCurrencyBase(currency) {
|
|
3
5
|
return currency.address ? { chain_id: currency.chain_id, address: currency.address } : { chain_id: currency.chain_id };
|
|
4
6
|
}
|
|
@@ -15,10 +17,8 @@ export function buildSwapQuoteRequest({ amount, activeSide, source, destination,
|
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
export function isSameSwap(source, destination, inputWalletAddress, outputWalletAddress) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
outputWalletAddress &&
|
|
23
|
-
inputWalletAddress.toLowerCase() === outputWalletAddress.toLowerCase());
|
|
20
|
+
if (!source || !destination || !currenciesMatch(source, destination))
|
|
21
|
+
return false;
|
|
22
|
+
const chainType = getChainTypeByChainId(source.chain_id);
|
|
23
|
+
return Boolean(inputWalletAddress && outputWalletAddress && addressesMatch(inputWalletAddress, outputWalletAddress, chainType));
|
|
24
24
|
}
|
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.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"author": "Lars <lars@coinvoyage.io>",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@coin-voyage/crypto": "3.
|
|
25
|
+
"@coin-voyage/crypto": "3.4.0",
|
|
26
26
|
"@coin-voyage/shared": "3.4.1-beta.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|