@coin-voyage/swapkit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/components/currency-panel.d.ts +17 -0
  2. package/dist/components/currency-panel.js +22 -0
  3. package/dist/components/execution-modal.js +28 -5
  4. package/dist/components/execution-modal.test.d.ts +1 -0
  5. package/dist/components/execution-modal.test.js +92 -0
  6. package/dist/components/manual-wallet-modal.d.ts +8 -0
  7. package/dist/components/manual-wallet-modal.js +19 -0
  8. package/dist/components/output-wallet-selector.d.ts +7 -0
  9. package/dist/components/output-wallet-selector.js +10 -0
  10. package/dist/components/quote-breakdown.d.ts +5 -0
  11. package/dist/components/quote-breakdown.js +13 -0
  12. package/dist/components/styles.d.ts +1 -0
  13. package/dist/components/styles.js +14 -2
  14. package/dist/components/swap-recipient.test.js +29 -1
  15. package/dist/components/swap-settings.d.ts +9 -0
  16. package/dist/components/swap-settings.js +24 -0
  17. package/dist/components/swap.d.ts +2 -28
  18. package/dist/components/swap.js +27 -312
  19. package/dist/components/token-picker.test.js +11 -0
  20. package/dist/hooks/use-dismiss.d.ts +3 -0
  21. package/dist/hooks/use-dismiss.js +26 -0
  22. package/dist/hooks/use-swap-notifications.d.ts +10 -0
  23. package/dist/hooks/use-swap-notifications.js +53 -0
  24. package/dist/hooks/use-swap-quote.js +49 -51
  25. package/dist/hooks/use-swap.d.ts +47 -0
  26. package/dist/hooks/use-swap.js +189 -0
  27. package/dist/hooks/use-token-data.js +3 -3
  28. package/dist/hooks/use-token-data.test.js +33 -3
  29. package/dist/lib/currency-selection.js +6 -4
  30. package/dist/lib/currency-selection.test.d.ts +1 -0
  31. package/dist/lib/currency-selection.test.js +16 -0
  32. package/dist/lib/format.d.ts +5 -0
  33. package/dist/lib/format.js +20 -0
  34. package/dist/lib/quote.d.ts +10 -1
  35. package/dist/lib/quote.js +12 -0
  36. package/dist/lib/slippage.d.ts +11 -0
  37. package/dist/lib/slippage.js +17 -0
  38. package/package.json +3 -3
@@ -0,0 +1,47 @@
1
+ import { type SwapSide } from "../lib/quote";
2
+ import type { SwapProps } from "../types";
3
+ import { type SwapToken } from "./use-token-data";
4
+ /** Wires token data, wallets, quoting and execution together into the view model rendered by `<Swap />`. */
5
+ export declare function useSwap({ defaultSourceCurrency, defaultDestinationCurrency, defaultSlippageBps, onComplete, onError, }: SwapProps): {
6
+ tokens: SwapToken[];
7
+ sourceTokens: SwapToken[];
8
+ tokensLoading: boolean;
9
+ tokenError: string | null;
10
+ sourceId: string | undefined;
11
+ destinationId: string | undefined;
12
+ sourceToken: SwapToken | undefined;
13
+ destinationToken: SwapToken | undefined;
14
+ activeSide: SwapSide;
15
+ displayedInputAmount: string;
16
+ displayedOutputAmount: string;
17
+ sourceBalance: string | undefined;
18
+ destinationBalance: string | undefined;
19
+ slippageMode: import("./use-swap-state").SlippageMode;
20
+ slippagePercent: string;
21
+ slippage: import("../lib/slippage").ResolvedSlippage;
22
+ destinationChainType: import("@coin-voyage/shared/types").ChainType | undefined;
23
+ destinationChainName: string | undefined;
24
+ destinationHasWallet: boolean;
25
+ separateOutputWallet: boolean;
26
+ manualAddress: string | undefined;
27
+ outputWalletLabel: string;
28
+ showOutputWalletSelector: boolean;
29
+ walletRequirement: import("../lib/output-wallet").SwapWalletRequirement;
30
+ requiresPastedAddress: boolean;
31
+ quote: import("@coin-voyage/shared/types").SwapQuote | null;
32
+ quoteLoading: boolean;
33
+ quoteError: string | null;
34
+ stage: import("../lib/execution").SwapStage;
35
+ idle: boolean;
36
+ insufficientBalance: boolean;
37
+ sameSwap: boolean;
38
+ canExecute: boolean;
39
+ setAmount: (side: SwapSide, value: string) => void;
40
+ setSlippageMode: (mode: import("./use-swap-state").SlippageMode) => void;
41
+ setSlippagePercent: (value: string) => void;
42
+ selectToken: (side: SwapSide, token: SwapToken) => void;
43
+ reverse: () => void;
44
+ saveManualAddress: (address: string) => void;
45
+ execute: () => Promise<void>;
46
+ closeExecution: () => void;
47
+ };
@@ -0,0 +1,189 @@
1
+ import { useAccount } from "@coin-voyage/crypto/hooks";
2
+ import { getChainName, getChainTypeByChainId, supportsWalletPayments } from "@coin-voyage/shared/chain";
3
+ import { formatCurrencyAmount } from "@coin-voyage/shared/currency";
4
+ import { currencyID } from "@coin-voyage/shared/types";
5
+ import { truncateAddress } from "@coin-voyage/shared/utils";
6
+ import { useCallback, useMemo } from "react";
7
+ import { getSwapWalletRequirement, shouldShowOutputWalletSelector, shouldUseSeparateOutputWallet, } from "../lib/output-wallet";
8
+ import { buildSwapExecuteRequest, buildSwapQuoteRequest, isSameSwap } from "../lib/quote";
9
+ import { resolveSlippage } from "../lib/slippage";
10
+ import { useSwapExecution } from "./use-swap-execution";
11
+ import { useSwapNotifications } from "./use-swap-notifications";
12
+ import { useSwapQuote } from "./use-swap-quote";
13
+ import { useSwapState } from "./use-swap-state";
14
+ import { useTokenData } from "./use-token-data";
15
+ /** Wires token data, wallets, quoting and execution together into the view model rendered by `<Swap />`. */
16
+ export function useSwap({ defaultSourceCurrency, defaultDestinationCurrency, defaultSlippageBps, onComplete, onError, }) {
17
+ const { tokens, accounts, isLoading, error: tokenError, balancesLoaded, refreshBalances } = useTokenData();
18
+ const { accounts: connectedAccounts } = useAccount();
19
+ const connectedChainTypes = new Set(accounts.map((account) => account.chainType));
20
+ const swapState = useSwapState({
21
+ currencies: tokens.map((token) => token.currency),
22
+ connectedChainTypes,
23
+ defaultSourceCurrency,
24
+ defaultDestinationCurrency,
25
+ defaultSlippageBps,
26
+ });
27
+ const { sourceId, destinationId, inputAmount, outputAmount, activeSide, slippageMode, slippagePercent } = swapState.state;
28
+ const { manualOutputWallet } = swapState.state;
29
+ const sourceToken = tokens.find((token) => currencyID(token.currency) === sourceId);
30
+ const destinationToken = tokens.find((token) => currencyID(token.currency) === destinationId);
31
+ const sourceChainType = sourceToken ? getChainTypeByChainId(sourceToken.currency.chain_id) : undefined;
32
+ const destinationChainType = destinationToken ? getChainTypeByChainId(destinationToken.currency.chain_id) : undefined;
33
+ // Wallet-less chains (Zcash) share a chain type with a connectable one, so never match a wallet to them.
34
+ const sourceTokens = useMemo(() => tokens.filter((token) => supportsWalletPayments(token.currency.chain_id)), [tokens]);
35
+ const destinationHasWallet = supportsWalletPayments(destinationToken?.currency.chain_id);
36
+ const inputAccount = supportsWalletPayments(sourceToken?.currency.chain_id)
37
+ ? connectedAccounts.find((account) => account.chainType === sourceChainType)
38
+ : undefined;
39
+ const outputAccount = destinationHasWallet
40
+ ? connectedAccounts.find((account) => account.chainType === destinationChainType)
41
+ : undefined;
42
+ const separateOutputWallet = shouldUseSeparateOutputWallet(sourceChainType, destinationChainType);
43
+ const manualAddress = manualOutputWallet && manualOutputWallet.chainId === destinationToken?.currency.chain_id
44
+ ? manualOutputWallet.address
45
+ : undefined;
46
+ const connectedOutputAddress = separateOutputWallet ? outputAccount?.address : inputAccount?.address;
47
+ const outputAddress = manualAddress ?? (destinationHasWallet ? connectedOutputAddress : undefined);
48
+ const walletRequirement = getSwapWalletRequirement({
49
+ inputWalletAddress: inputAccount?.address,
50
+ inputChainType: sourceChainType,
51
+ shouldRestrictInputChainType: Boolean(sourceToken),
52
+ outputWalletAddress: outputAddress,
53
+ outputChainType: destinationChainType,
54
+ });
55
+ const showOutputWalletSelector = shouldShowOutputWalletSelector({
56
+ outputChainType: destinationChainType,
57
+ connectionRequirement: walletRequirement,
58
+ });
59
+ // Wallet-less destinations can only be satisfied by pasting an address.
60
+ const requiresPastedAddress = walletRequirement.target === "output" && !destinationHasWallet;
61
+ const outputWalletLabel = outputAddress
62
+ ? truncateAddress(outputAddress)
63
+ : destinationHasWallet
64
+ ? "Connect Wallet"
65
+ : "Paste Address";
66
+ const slippage = resolveSlippage(slippageMode, slippagePercent);
67
+ const sameSwap = isSameSwap(sourceToken?.currency, destinationToken?.currency, inputAccount?.address, outputAddress);
68
+ const activeAmount = activeSide === "input" ? inputAmount : outputAmount;
69
+ const quoteRequest = useMemo(() => slippage.invalid || sameSwap
70
+ ? null
71
+ : buildSwapQuoteRequest({
72
+ amount: activeAmount,
73
+ activeSide,
74
+ source: sourceToken?.currency,
75
+ destination: destinationToken?.currency,
76
+ slippageBps: slippage.bps,
77
+ }), [
78
+ activeAmount,
79
+ activeSide,
80
+ destinationToken?.currency,
81
+ sameSwap,
82
+ slippage.bps,
83
+ slippage.invalid,
84
+ sourceToken?.currency,
85
+ ]);
86
+ const execution = useSwapExecution(sourceChainType, inputAccount?.address);
87
+ const idle = execution.stage.kind === "idle";
88
+ const quoteState = useSwapQuote(quoteRequest, idle);
89
+ useSwapNotifications({
90
+ stage: execution.stage,
91
+ quoteError: quoteState.error,
92
+ tokenError,
93
+ onBalancesChanged: refreshBalances,
94
+ onComplete,
95
+ onError,
96
+ });
97
+ const { quote } = quoteState;
98
+ const displayedInputAmount = activeSide === "output" && quote
99
+ ? formatCurrencyAmount(quote.input.amount.raw, quote.input.currency.decimals)
100
+ : inputAmount;
101
+ const displayedOutputAmount = activeSide === "input" && quote
102
+ ? formatCurrencyAmount(quote.output.amount.raw, quote.output.currency.decimals)
103
+ : outputAmount;
104
+ const sourceBalance = sourceToken?.balance;
105
+ // A pasted recipient is not one of the connected wallets, so its balance is unknown.
106
+ const destinationBalance = manualAddress
107
+ ? undefined
108
+ : (destinationToken?.balance ?? (balancesLoaded && outputAccount?.address ? "0" : undefined));
109
+ const insufficientBalance = sourceBalance !== undefined && Number(displayedInputAmount) > Number(sourceBalance);
110
+ const canExecute = Boolean(quote &&
111
+ quoteRequest &&
112
+ inputAccount?.address &&
113
+ outputAddress &&
114
+ walletRequirement.target === "none" &&
115
+ !insufficientBalance &&
116
+ !sameSwap &&
117
+ !slippage.invalid);
118
+ const { reverse: reverseState, selectCurrency, setManualOutputWallet, reset: resetState } = swapState;
119
+ const reverse = useCallback(() => {
120
+ reverseState(displayedInputAmount, displayedOutputAmount);
121
+ }, [displayedInputAmount, displayedOutputAmount, reverseState]);
122
+ const selectToken = useCallback((side, token) => selectCurrency(side, currencyID(token.currency)), [selectCurrency]);
123
+ const saveManualAddress = useCallback((address) => {
124
+ if (destinationToken)
125
+ setManualOutputWallet({ chainId: destinationToken.currency.chain_id, address });
126
+ }, [destinationToken, setManualOutputWallet]);
127
+ const execute = useCallback(async () => {
128
+ if (!quoteRequest || !sourceToken || !destinationToken || !inputAccount?.address || !outputAddress)
129
+ return;
130
+ const request = buildSwapExecuteRequest({
131
+ quoteRequest,
132
+ source: sourceToken.currency,
133
+ destination: destinationToken.currency,
134
+ senderAddress: inputAccount.address,
135
+ recipient: outputAddress,
136
+ slippageBps: slippage.bps,
137
+ });
138
+ await execution.execute(request, inputAccount.address);
139
+ }, [destinationToken, execution, inputAccount, outputAddress, quoteRequest, slippage.bps, sourceToken]);
140
+ const closeExecution = useCallback(() => {
141
+ const { kind } = execution.stage;
142
+ execution.reset();
143
+ if (kind === "done" || kind === "refunded")
144
+ resetState({ preserveCurrencies: kind === "done" });
145
+ }, [execution, resetState]);
146
+ return {
147
+ tokens,
148
+ sourceTokens,
149
+ tokensLoading: isLoading,
150
+ tokenError,
151
+ sourceId,
152
+ destinationId,
153
+ sourceToken,
154
+ destinationToken,
155
+ activeSide,
156
+ displayedInputAmount,
157
+ displayedOutputAmount,
158
+ sourceBalance,
159
+ destinationBalance,
160
+ slippageMode,
161
+ slippagePercent,
162
+ slippage,
163
+ destinationChainType,
164
+ destinationChainName: getChainName(destinationToken?.currency.chain_id),
165
+ destinationHasWallet,
166
+ separateOutputWallet,
167
+ manualAddress,
168
+ outputWalletLabel,
169
+ showOutputWalletSelector,
170
+ walletRequirement,
171
+ requiresPastedAddress,
172
+ quote,
173
+ quoteLoading: quoteState.loading,
174
+ quoteError: quoteState.error,
175
+ stage: execution.stage,
176
+ idle,
177
+ insufficientBalance,
178
+ sameSwap,
179
+ canExecute,
180
+ setAmount: swapState.setAmount,
181
+ setSlippageMode: swapState.setSlippageMode,
182
+ setSlippagePercent: swapState.setSlippagePercent,
183
+ selectToken,
184
+ reverse,
185
+ saveManualAddress,
186
+ execute,
187
+ closeExecution,
188
+ };
189
+ }
@@ -23,7 +23,7 @@ function mergeTokens(tokenList, portfolio) {
23
23
  return [...listed, ...unlisted].sort((left, right) => (right.balanceUsd ?? 0) - (left.balanceUsd ?? 0));
24
24
  }
25
25
  export function useTokenData() {
26
- const { api } = useSwapKit();
26
+ const { api, environment } = useSwapKit();
27
27
  const { accounts } = useAccount();
28
28
  const [listedCurrencies, setListedCurrencies] = useState([]);
29
29
  const [portfolioState, setPortfolioState] = useState({
@@ -49,7 +49,7 @@ export function useTokenData() {
49
49
  useEffect(() => clearRefreshTimers, [clearRefreshTimers]);
50
50
  useEffect(() => {
51
51
  let active = true;
52
- fetchTokenList()
52
+ fetchTokenList({ preview: environment !== "production" })
53
53
  .then((data) => {
54
54
  if (!active)
55
55
  return;
@@ -67,7 +67,7 @@ export function useTokenData() {
67
67
  return () => {
68
68
  active = false;
69
69
  };
70
- }, []);
70
+ }, [environment]);
71
71
  const walletKey = JSON.stringify(accounts
72
72
  .flatMap((account) => (account.address ? [{ address: account.address, chain_type: account.chainType }] : []))
73
73
  .sort((left, right) => `${left.chain_type}:${left.address}`.localeCompare(`${right.chain_type}:${right.address}`)));
@@ -4,7 +4,10 @@ import { ChainId, ChainType } from "@coin-voyage/shared/types";
4
4
  import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
5
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
6
  import { useTokenData } from "./use-token-data";
7
- const { api } = vi.hoisted(() => ({ api: { portfolio: vi.fn() } }));
7
+ const { api, context } = vi.hoisted(() => ({
8
+ api: { portfolio: vi.fn() },
9
+ context: { environment: "production" },
10
+ }));
8
11
  vi.mock("@coin-voyage/crypto/hooks", () => ({
9
12
  useAccount: () => ({ accounts: [{ address: "0x123", chainType: ChainType.EVM }] }),
10
13
  }));
@@ -13,13 +16,38 @@ vi.mock("@coin-voyage/shared/currency", async (importOriginal) => ({
13
16
  fetchTokenList: vi.fn().mockResolvedValue({ chains: [] }),
14
17
  }));
15
18
  vi.mock("../providers/swapkit-context", () => ({
16
- useSwapKit: () => ({ api }),
19
+ useSwapKit: () => ({ api, ...context }),
17
20
  }));
18
21
  afterEach(() => {
19
22
  cleanup();
20
23
  vi.clearAllMocks();
24
+ context.environment = "production";
21
25
  });
22
26
  describe("useTokenData", () => {
27
+ it.each(["production", "development", "local"])("loads the token list for %s", async (environment) => {
28
+ context.environment = environment;
29
+ const preview = environment !== "production";
30
+ const zec = { name: "Zcash", ticker: "ZEC", decimals: 8, chainId: ChainId.ZEC, logoURI: "zec.png" };
31
+ vi.mocked(fetchTokenList).mockImplementationOnce(async (options) => ({
32
+ chains: typeof options === "object" && options.preview
33
+ ? [{ chainId: ChainId.ZEC, name: "Zcash", logoURI: "zec.png", tokens: [zec] }]
34
+ : [],
35
+ }));
36
+ api.portfolio.mockResolvedValue({ data: { portfolio: [] } });
37
+ const { result } = renderHook(() => useTokenData());
38
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
39
+ expect(fetchTokenList).toHaveBeenCalledWith({ preview });
40
+ expect(result.current.tokens.map(({ currency }) => currency.symbol)).toEqual(preview ? ["ZEC"] : []);
41
+ });
42
+ it("reloads the list when the provider environment changes", async () => {
43
+ api.portfolio.mockResolvedValue({ data: { portfolio: [] } });
44
+ const { result, rerender } = renderHook(() => useTokenData());
45
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
46
+ context.environment = "development";
47
+ rerender();
48
+ await waitFor(() => expect(fetchTokenList).toHaveBeenLastCalledWith({ preview: true }));
49
+ expect(fetchTokenList).toHaveBeenCalledTimes(2);
50
+ });
23
51
  it.each([
24
52
  { decimals: 6, raw: "1234567", ui: "1.23", expected: "1.234567" },
25
53
  { decimals: 9, raw: "1239999999", ui: "1.24", expected: "1.239999999" },
@@ -47,7 +75,9 @@ describe("useTokenData", () => {
47
75
  decimals: 6,
48
76
  };
49
77
  api.portfolio.mockResolvedValue({
50
- data: { portfolio: [{ currencies: [{ currency: held, balance: { raw: "1500000", ui: "1.5", value_usd: 1.5 } }] }] },
78
+ data: {
79
+ portfolio: [{ currencies: [{ currency: held, balance: { raw: "1500000", ui: "1.5", value_usd: 1.5 } }] }],
80
+ },
51
81
  });
52
82
  const { result, rerender } = renderHook(() => useTokenData());
53
83
  await waitFor(() => expect(result.current.tokens[0]?.balance).toBe("1.5"));
@@ -1,17 +1,19 @@
1
1
  import { addressesMatch } from "@coin-voyage/crypto/utils";
2
- import { getChainTypeByChainId } from "@coin-voyage/shared/chain";
2
+ import { getChainTypeByChainId, supportsWalletPayments } from "@coin-voyage/shared/chain";
3
3
  import { currencyID } from "@coin-voyage/shared/types";
4
4
  export function currenciesMatch(left, right) {
5
5
  return (left.chain_id === right.chain_id &&
6
6
  addressesMatch(left.address, right.address, getChainTypeByChainId(left.chain_id)));
7
7
  }
8
8
  export function resolveCurrencySelection({ currencies, connectedChainTypes, defaultSourceCurrency, defaultDestinationCurrency, }) {
9
+ // The source is paid from a connected wallet, so wallet-less chains can only be a destination.
10
+ const sourceCurrencies = currencies.filter((currency) => supportsWalletPayments(currency.chain_id));
9
11
  const preferredSource = defaultSourceCurrency
10
- ? currencies.find((currency) => currenciesMatch(currency, defaultSourceCurrency))
12
+ ? sourceCurrencies.find((currency) => currenciesMatch(currency, defaultSourceCurrency))
11
13
  : undefined;
12
14
  const source = preferredSource ??
13
- currencies.find((currency) => connectedChainTypes.has(getChainTypeByChainId(currency.chain_id))) ??
14
- currencies[0];
15
+ sourceCurrencies.find((currency) => connectedChainTypes.has(getChainTypeByChainId(currency.chain_id))) ??
16
+ sourceCurrencies[0];
15
17
  const preferredDestination = defaultDestinationCurrency
16
18
  ? currencies.find((currency) => currenciesMatch(currency, defaultDestinationCurrency))
17
19
  : undefined;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { ChainId, ChainType, currencyID } from "@coin-voyage/shared/types";
2
+ import { describe, expect, it } from "vitest";
3
+ import { resolveCurrencySelection } from "./currency-selection";
4
+ const zec = { id: "zec", chain_id: ChainId.ZEC, name: "Zcash", symbol: "ZEC", decimals: 8 };
5
+ const btc = { id: "btc", chain_id: ChainId.BTC, name: "Bitcoin", symbol: "BTC", decimals: 8 };
6
+ describe("resolveCurrencySelection", () => {
7
+ it("never selects a wallet-less chain as the source", () => {
8
+ const selection = resolveCurrencySelection({
9
+ currencies: [zec, btc],
10
+ connectedChainTypes: new Set([ChainType.UTXO]),
11
+ defaultSourceCurrency: { chain_id: ChainId.ZEC },
12
+ });
13
+ expect(selection.sourceId).toBe(currencyID(btc));
14
+ expect(selection.destinationId).toBe(currencyID(zec));
15
+ });
16
+ });
@@ -0,0 +1,5 @@
1
+ import type { SwapQuote } from "@coin-voyage/shared/types";
2
+ export declare function formatUsd(value?: number | null): string;
3
+ export declare function formatPriceImpact(value: number): string;
4
+ export declare function usdValue(priceUsd: number | null | undefined, amount: string): number | undefined;
5
+ export declare function formatQuoteRate(quote: SwapQuote): string;
@@ -0,0 +1,20 @@
1
+ export function formatUsd(value) {
2
+ if (value === undefined || value === null || !Number.isFinite(value))
3
+ return "$0.00";
4
+ return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 2 }).format(value);
5
+ }
6
+ export function formatPriceImpact(value) {
7
+ if (value > 0 && value < 0.01)
8
+ return "<0.01%";
9
+ return `${value.toFixed(2)}%`;
10
+ }
11
+ export function usdValue(priceUsd, amount) {
12
+ const numericAmount = Number(amount);
13
+ if (!priceUsd || !Number.isFinite(numericAmount))
14
+ return undefined;
15
+ return priceUsd * numericAmount;
16
+ }
17
+ export function formatQuoteRate(quote) {
18
+ const rate = Number(quote.output.amount.ui) / Math.max(Number(quote.input.amount.ui), Number.EPSILON);
19
+ return `1 ${quote.input.currency.symbol} ≈ ${rate.toLocaleString(undefined, { maximumSignificantDigits: 6 })} ${quote.output.currency.symbol}`;
20
+ }
@@ -1,4 +1,4 @@
1
- import type { CurrencyBase, SwapQuoteRequest } from "@coin-voyage/shared/types";
1
+ import type { CurrencyBase, SwapExecuteRequest, 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 {
@@ -9,5 +9,14 @@ interface BuildSwapQuoteRequestProps {
9
9
  slippageBps?: number;
10
10
  }
11
11
  export declare function buildSwapQuoteRequest({ amount, activeSide, source, destination, slippageBps, }: BuildSwapQuoteRequestProps): SwapQuoteRequest | null;
12
+ interface BuildSwapExecuteRequestProps {
13
+ quoteRequest: SwapQuoteRequest;
14
+ source: CurrencyBase;
15
+ destination: CurrencyBase;
16
+ senderAddress: string;
17
+ recipient: string;
18
+ slippageBps?: number;
19
+ }
20
+ export declare function buildSwapExecuteRequest({ quoteRequest, source, destination, senderAddress, recipient, slippageBps, }: BuildSwapExecuteRequestProps): SwapExecuteRequest;
12
21
  export declare function isSameSwap(source: CurrencyBase | undefined, destination: CurrencyBase | undefined, inputWalletAddress?: string, outputWalletAddress?: string): boolean;
13
22
  export {};
package/dist/lib/quote.js CHANGED
@@ -16,6 +16,18 @@ export function buildSwapQuoteRequest({ amount, activeSide, source, destination,
16
16
  ...(slippageBps === undefined ? {} : { slippage_bps: slippageBps }),
17
17
  };
18
18
  }
19
+ export function buildSwapExecuteRequest({ quoteRequest, source, destination, senderAddress, recipient, slippageBps, }) {
20
+ return {
21
+ trade_type: quoteRequest.trade_type,
22
+ amount: quoteRequest.amount,
23
+ source: {
24
+ currency: toCurrencyBase(source),
25
+ sender_address: senderAddress,
26
+ ...(slippageBps === undefined ? {} : { slippage_bps: slippageBps }),
27
+ },
28
+ destination: { currency: toCurrencyBase(destination), recipient },
29
+ };
30
+ }
19
31
  export function isSameSwap(source, destination, inputWalletAddress, outputWalletAddress) {
20
32
  if (!source || !destination || !currenciesMatch(source, destination))
21
33
  return false;
@@ -0,0 +1,11 @@
1
+ import type { SlippageMode } from "../hooks/use-swap-state";
2
+ export interface ResolvedSlippage {
3
+ invalid: boolean;
4
+ /** Undefined in auto mode, or while the custom value is invalid. */
5
+ bps?: number;
6
+ warning?: {
7
+ tone: "warning" | "danger";
8
+ message: string;
9
+ };
10
+ }
11
+ export declare function resolveSlippage(mode: SlippageMode, percent: string): ResolvedSlippage;
@@ -0,0 +1,17 @@
1
+ const HIGH_SLIPPAGE_PERCENT = 6;
2
+ const SEVERE_SLIPPAGE_PERCENT = 40;
3
+ export function resolveSlippage(mode, percent) {
4
+ if (mode !== "custom")
5
+ return { invalid: false };
6
+ const value = Number(percent);
7
+ const invalid = percent.length === 0 || !Number.isFinite(value) || value < 0 || value > 100;
8
+ return {
9
+ invalid,
10
+ bps: invalid ? undefined : Math.round(value * 100),
11
+ warning: value >= SEVERE_SLIPPAGE_PERCENT
12
+ ? { tone: "danger", message: "This slippage tolerance may cause a severely unfavorable trade." }
13
+ : value >= HIGH_SLIPPAGE_PERCENT
14
+ ? { tone: "warning", message: "High slippage can result in a worse execution price." }
15
+ : undefined,
16
+ };
17
+ }
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.0",
4
+ "version": "0.5.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.6.0",
26
- "@coin-voyage/shared": "3.6.0"
25
+ "@coin-voyage/crypto": "3.7.0",
26
+ "@coin-voyage/shared": "3.7.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@testing-library/react": "^16.3.0",