-
Swap Widget
-
+
+
+
+
Swap Widget
+
Embeddable multi-chain swap widget powered by ShapeShift
-
+
{showCustomizer && (
-
-
Customize Widget
-
-
-
Presets
-
- {THEME_PRESETS.map((preset) => {
- const previewColors =
- theme === "dark" ? preset.dark : preset.light;
+
+
Customize Widget
+
+
+
Presets
+
+ {THEME_PRESETS.map(preset => {
+ const previewColors = theme === 'dark' ? preset.dark : preset.light
return (
applyPreset(preset)}
title={preset.name}
- type="button"
+ type='button'
>
-
- {preset.name}
-
+ {preset.name}
- );
+ )
})}
-
-
Theme
-
+
+
Theme
+
setTheme("light")}
- type="button"
+ className={`demo-theme-btn ${theme === 'light' ? 'active' : ''}`}
+ onClick={() => setTheme('light')}
+ type='button'
>
-
-
+
+
Light
setTheme("dark")}
- type="button"
+ className={`demo-theme-btn ${theme === 'dark' ? 'active' : ''}`}
+ onClick={() => setTheme('dark')}
+ type='button'
>
-
+
Dark
-
-
- Background Color
-
-
+
-
-
Card Color
-
+
-
-
Accent Color
-
+
-
-
Connection
-
+
+
Connection
+
{isConnected ? (
<>
- Connected
-
+ Connected
+
{address?.slice(0, 6)}...{address?.slice(-4)}
>
) : (
- Not connected
+ Not connected
)}
-
-
+
+
{copied ? (
<>
-
+
Copied!
>
) : (
<>
-
-
+
+
Copy Theme Config
>
@@ -425,9 +403,9 @@ const DemoContent = () => {
)}
-
- );
-};
+ )
+}
export const App = () => {
return (
@@ -460,5 +438,5 @@ export const App = () => {
- );
-};
+ )
+}
diff --git a/packages/swap-widget-poc/src/utils/addressValidation.ts b/packages/swap-widget-poc/src/utils/addressValidation.ts
new file mode 100644
index 00000000000..70db8c36b1f
--- /dev/null
+++ b/packages/swap-widget-poc/src/utils/addressValidation.ts
@@ -0,0 +1,218 @@
+import { isAddress } from "viem";
+import type { ChainId } from "../types";
+import { getChainType, COSMOS_CHAIN_IDS } from "../types";
+
+/**
+ * Validates an EVM address using viem
+ */
+export const isValidEvmAddress = (address: string): boolean => {
+ return isAddress(address, { strict: false });
+};
+
+/**
+ * Validates a Bitcoin address (Legacy, SegWit, Native SegWit, Taproot)
+ */
+export const isValidBitcoinAddress = (address: string): boolean => {
+ // Legacy (P2PKH) - starts with 1
+ const legacyRegex = /^1[a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ // Legacy (P2SH) - starts with 3
+ const p2shRegex = /^3[a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ // Native SegWit (Bech32) - starts with bc1q
+ const nativeSegwitRegex = /^bc1q[a-z0-9]{38,58}$/i;
+ // Taproot (Bech32m) - starts with bc1p
+ const taprootRegex = /^bc1p[a-z0-9]{58}$/i;
+
+ return (
+ legacyRegex.test(address) ||
+ p2shRegex.test(address) ||
+ nativeSegwitRegex.test(address) ||
+ taprootRegex.test(address)
+ );
+};
+
+/**
+ * Validates a Bitcoin Cash address
+ */
+export const isValidBitcoinCashAddress = (address: string): boolean => {
+ // CashAddr format - starts with bitcoincash: or just q/p
+ const cashAddrRegex = /^(bitcoincash:)?[qp][a-z0-9]{41}$/i;
+ // Legacy format (same as Bitcoin)
+ const legacyRegex = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+
+ return cashAddrRegex.test(address) || legacyRegex.test(address);
+};
+
+/**
+ * Validates a Litecoin address
+ */
+export const isValidLitecoinAddress = (address: string): boolean => {
+ // Legacy (P2PKH) - starts with L
+ const legacyRegex = /^L[a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ // Legacy (P2SH) - starts with M or 3
+ const p2shRegex = /^[M3][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ // Native SegWit (Bech32) - starts with ltc1
+ const nativeSegwitRegex = /^ltc1[a-z0-9]{38,58}$/i;
+
+ return (
+ legacyRegex.test(address) ||
+ p2shRegex.test(address) ||
+ nativeSegwitRegex.test(address)
+ );
+};
+
+/**
+ * Validates a Dogecoin address
+ */
+export const isValidDogecoinAddress = (address: string): boolean => {
+ // P2PKH - starts with D
+ const p2pkhRegex = /^D[5-9A-HJ-NP-U][a-km-zA-HJ-NP-Z1-9]{32}$/;
+ // P2SH - starts with 9 or A
+ const p2shRegex = /^[9A][a-km-zA-HJ-NP-Z1-9]{33}$/;
+
+ return p2pkhRegex.test(address) || p2shRegex.test(address);
+};
+
+/**
+ * Validates a Cosmos SDK address (bech32 format)
+ */
+export const isValidCosmosAddress = (
+ address: string,
+ expectedPrefix?: string,
+): boolean => {
+ // Basic bech32 validation - prefix + 1 + base32 characters
+ const bech32Regex = /^[a-z]{1,83}1[a-z0-9]{38,58}$/i;
+
+ if (!bech32Regex.test(address)) {
+ return false;
+ }
+
+ // If prefix is specified, validate it
+ if (expectedPrefix) {
+ return address.toLowerCase().startsWith(expectedPrefix.toLowerCase());
+ }
+
+ return true;
+};
+
+/**
+ * Gets the expected bech32 prefix for a Cosmos chain
+ */
+const getCosmosPrefix = (chainId: ChainId): string | undefined => {
+ const prefixMap: Record = {
+ [COSMOS_CHAIN_IDS.cosmos]: "cosmos",
+ [COSMOS_CHAIN_IDS.thorchain]: "thor",
+ [COSMOS_CHAIN_IDS.mayachain]: "maya",
+ };
+ return prefixMap[chainId];
+};
+
+/**
+ * Validates a Solana address (base58, 32-44 chars)
+ */
+export const isValidSolanaAddress = (address: string): boolean => {
+ // Solana addresses are base58 encoded, typically 32-44 characters
+ // Base58 alphabet excludes 0, O, I, l
+ const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
+ return base58Regex.test(address);
+};
+
+/**
+ * Validates an address for a specific chain
+ */
+export const validateAddress = (
+ address: string,
+ chainId: ChainId,
+): { valid: boolean; error?: string } => {
+ if (!address || address.trim() === "") {
+ return { valid: false, error: "Address is required" };
+ }
+
+ const trimmedAddress = address.trim();
+ const chainType = getChainType(chainId);
+
+ switch (chainType) {
+ case "evm":
+ if (!isValidEvmAddress(trimmedAddress)) {
+ return { valid: false, error: "Invalid EVM address" };
+ }
+ break;
+
+ case "utxo":
+ // Determine specific UTXO chain
+ if (chainId.includes("000000000019d6689c085ae165831e93")) {
+ // Bitcoin
+ if (!isValidBitcoinAddress(trimmedAddress)) {
+ return { valid: false, error: "Invalid Bitcoin address" };
+ }
+ } else if (chainId.includes("000000000000000000651ef99cb9fcbe")) {
+ // Bitcoin Cash
+ if (!isValidBitcoinCashAddress(trimmedAddress)) {
+ return { valid: false, error: "Invalid Bitcoin Cash address" };
+ }
+ } else if (chainId.includes("12a765e31ffd4059bada1e25190f6e98")) {
+ // Litecoin
+ if (!isValidLitecoinAddress(trimmedAddress)) {
+ return { valid: false, error: "Invalid Litecoin address" };
+ }
+ } else if (chainId.includes("00000000001a91e3dace36e2be3bf030")) {
+ // Dogecoin
+ if (!isValidDogecoinAddress(trimmedAddress)) {
+ return { valid: false, error: "Invalid Dogecoin address" };
+ }
+ } else {
+ return { valid: false, error: "Unsupported UTXO chain" };
+ }
+ break;
+
+ case "cosmos":
+ const expectedPrefix = getCosmosPrefix(chainId);
+ if (!isValidCosmosAddress(trimmedAddress, expectedPrefix)) {
+ const chainName = expectedPrefix
+ ? expectedPrefix.charAt(0).toUpperCase() + expectedPrefix.slice(1)
+ : "Cosmos";
+ return { valid: false, error: `Invalid ${chainName} address` };
+ }
+ break;
+
+ case "solana":
+ if (!isValidSolanaAddress(trimmedAddress)) {
+ return { valid: false, error: "Invalid Solana address" };
+ }
+ break;
+
+ default:
+ return { valid: false, error: "Unsupported chain type" };
+ }
+
+ return { valid: true };
+};
+
+/**
+ * Gets the expected address format hint for a chain
+ */
+export const getAddressFormatHint = (chainId: ChainId): string => {
+ const chainType = getChainType(chainId);
+
+ switch (chainType) {
+ case "evm":
+ return "0x...";
+ case "utxo":
+ if (chainId.includes("000000000019d6689c085ae165831e93")) {
+ return "bc1... or 1... or 3...";
+ } else if (chainId.includes("000000000000000000651ef99cb9fcbe")) {
+ return "bitcoincash:q... or 1...";
+ } else if (chainId.includes("12a765e31ffd4059bada1e25190f6e98")) {
+ return "ltc1... or L... or M...";
+ } else if (chainId.includes("00000000001a91e3dace36e2be3bf030")) {
+ return "D...";
+ }
+ return "Enter address";
+ case "cosmos":
+ const prefix = getCosmosPrefix(chainId);
+ return prefix ? `${prefix}1...` : "Enter address";
+ case "solana":
+ return "Base58 address";
+ default:
+ return "Enter address";
+ }
+};
From 1937f0fa6109b4b2bb89aeccff8a70f19859a26c Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 13 Jan 2026 01:21:53 +0100
Subject: [PATCH 16/41] feat: throttler and windowing
---
packages/swap-widget-poc/package.json | 1 +
.../src/components/TokenSelectModal.tsx | 162 +++++++++--------
.../swap-widget-poc/src/hooks/useBalances.ts | 164 ++++++++++++------
yarn.lock | 11 ++
4 files changed, 215 insertions(+), 123 deletions(-)
diff --git a/packages/swap-widget-poc/package.json b/packages/swap-widget-poc/package.json
index 374b99a3fb2..d804b55a2d9 100644
--- a/packages/swap-widget-poc/package.json
+++ b/packages/swap-widget-poc/package.json
@@ -17,6 +17,7 @@
"@tanstack/react-query": "^5.60.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-virtuoso": "^4.18.1",
"viem": "^2.21.0",
"wagmi": "^2.14.0"
},
diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx
index a5a0c925423..eccb1b23413 100644
--- a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx
+++ b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx
@@ -1,10 +1,13 @@
import { useState, useMemo, useCallback, useEffect } from "react";
+import { Virtuoso } from "react-virtuoso";
import type { Asset, AssetId, ChainId } from "../types";
import { useAssets, useChains, type ChainInfo } from "../hooks/useAssets";
import { useEvmBalances } from "../hooks/useBalances";
import { useAllMarketData } from "../hooks/useMarketData";
import "./TokenSelectModal.css";
+const VISIBLE_BUFFER = 10;
+
const useLockBodyScroll = (isLocked: boolean) => {
useEffect(() => {
if (!isLocked) return;
@@ -61,6 +64,10 @@ export const TokenSelectModal = ({
const [searchQuery, setSearchQuery] = useState("");
const [chainSearchQuery, setChainSearchQuery] = useState("");
const [selectedChainId, setSelectedChainId] = useState(null);
+ const [visibleRange, setVisibleRange] = useState({
+ startIndex: 0,
+ endIndex: 20,
+ });
const { data: allAssets, isLoading: isLoadingAssets } = useAssets();
const { data: chains, isLoading: isLoadingChains } = useChains();
@@ -125,17 +132,26 @@ export const TokenSelectModal = ({
disabledChainIds,
]);
+ const visibleAssets = useMemo(() => {
+ const start = Math.max(0, visibleRange.startIndex - VISIBLE_BUFFER);
+ const end = Math.min(
+ filteredAssets.length,
+ visibleRange.endIndex + VISIBLE_BUFFER,
+ );
+ return filteredAssets.slice(start, end);
+ }, [filteredAssets, visibleRange]);
+
const assetPrecisions = useMemo(() => {
const precisions: Record = {};
- for (const asset of filteredAssets) {
+ for (const asset of visibleAssets) {
precisions[asset.assetId] = asset.precision;
}
return precisions;
- }, [filteredAssets]);
+ }, [visibleAssets]);
const assetIds = useMemo(
- () => filteredAssets.map((a) => a.assetId),
- [filteredAssets],
+ () => visibleAssets.map((a) => a.assetId),
+ [visibleAssets],
);
const { data: balances, loadingAssetIds } = useEvmBalances(
@@ -292,73 +308,79 @@ export const TokenSelectModal = ({
) : filteredAssets.length === 0 ? (
No tokens found
) : (
- filteredAssets.map((asset) => {
- const chainInfo = chainInfoMap.get(asset.chainId);
- const balance = balances?.[asset.assetId];
- return (
- handleAssetSelect(asset)}
- type="button"
- >
-
- {asset.icon ? (
-
- ) : (
-
- {asset.symbol?.charAt(0) ?? "?"}
-
- )}
- {chainInfo?.icon && (
-
- )}
-
-
- {asset.symbol}
-
- {chainInfo?.name ?? asset.networkName ?? asset.name}
-
-
-
- {walletAddress &&
- (loadingAssetIds.has(asset.assetId) ? (
-
- ) : balance && balance.balance !== "0" ? (
- <>
- {marketData?.[asset.assetId]?.price && (
-
- $
- {(
- (Number(balance.balance) /
- Math.pow(10, asset.precision)) *
- Number(marketData[asset.assetId].price)
- ).toLocaleString(undefined, {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}
+ {
+ const chainInfo = chainInfoMap.get(asset.chainId);
+ const balance = balances?.[asset.assetId];
+ return (
+ handleAssetSelect(asset)}
+ type="button"
+ >
+
+ {asset.icon ? (
+
+ ) : (
+
+ {asset.symbol?.charAt(0) ?? "?"}
+
+ )}
+ {chainInfo?.icon && (
+
+ )}
+
+
+
+ {asset.symbol}
+
+
+ {chainInfo?.name ?? asset.networkName ?? asset.name}
+
+
+
+ {walletAddress &&
+ (loadingAssetIds.has(asset.assetId) ? (
+
+ ) : balance && balance.balance !== "0" ? (
+ <>
+ {marketData?.[asset.assetId]?.price && (
+
+ $
+ {(
+ (Number(balance.balance) /
+ Math.pow(10, asset.precision)) *
+ Number(marketData[asset.assetId].price)
+ ).toLocaleString(undefined, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ })}
+
+ )}
+
+ {balance.balanceFormatted}
- )}
-
- {balance.balanceFormatted}
-
- >
- ) : null)}
-
-
- );
- })
+ >
+ ) : null)}
+
+
+ );
+ }}
+ />
)}
diff --git a/packages/swap-widget-poc/src/hooks/useBalances.ts b/packages/swap-widget-poc/src/hooks/useBalances.ts
index d3ea6e219dd..0fb49c4e86f 100644
--- a/packages/swap-widget-poc/src/hooks/useBalances.ts
+++ b/packages/swap-widget-poc/src/hooks/useBalances.ts
@@ -1,12 +1,53 @@
-import { useBalance, useReadContracts } from "wagmi";
+import { useBalance } from "wagmi";
import { useMemo } from "react";
import { useQueries } from "@tanstack/react-query";
-import { getBalance } from "@wagmi/core";
+import { getBalance, readContract } from "@wagmi/core";
import { useConfig } from "wagmi";
import type { AssetId } from "../types";
import { formatAmount, getEvmChainIdNumber } from "../types";
import { erc20Abi } from "viem";
+const CONCURRENCY_LIMIT = 5;
+const DELAY_BETWEEN_BATCHES_MS = 50;
+
+class ThrottledQueue {
+ private running = 0;
+ private queue: Array<() => Promise
> = [];
+
+ async add(fn: () => Promise): Promise {
+ return new Promise((resolve, reject) => {
+ const run = async () => {
+ this.running++;
+ try {
+ const result = await fn();
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ } finally {
+ this.running--;
+ await new Promise((r) => setTimeout(r, DELAY_BETWEEN_BATCHES_MS));
+ this.processQueue();
+ }
+ };
+
+ if (this.running < CONCURRENCY_LIMIT) {
+ run();
+ } else {
+ this.queue.push(run);
+ }
+ });
+ }
+
+ private processQueue() {
+ if (this.queue.length > 0 && this.running < CONCURRENCY_LIMIT) {
+ const next = this.queue.shift();
+ next?.();
+ }
+ }
+}
+
+const balanceQueue = new ThrottledQueue();
+
type BalanceResult = {
assetId: AssetId;
balance: string;
@@ -56,6 +97,8 @@ export const useAssetBalance = (
chainId: isNative ? parsed.chainId : undefined,
query: {
enabled: !!address && !!isNative,
+ staleTime: 60_000,
+ refetchOnWindowFocus: false,
},
});
@@ -69,6 +112,8 @@ export const useAssetBalance = (
token: isErc20 ? parsed.tokenAddress : undefined,
query: {
enabled: !!address && !!isErc20,
+ staleTime: 60_000,
+ refetchOnWindowFocus: false,
},
});
@@ -141,44 +186,60 @@ export const useEvmBalances = (
queryKey: ["nativeBalance", address, asset.chainId],
queryFn: async () => {
if (!address) return null;
- try {
- const result = await getBalance(config, {
- address: address as `0x${string}`,
- chainId: asset.chainId,
- });
- return {
- assetId: asset.assetId,
- balance: result.value.toString(),
- precision: asset.precision,
- };
- } catch {
- return null;
- }
+ return balanceQueue.add(async () => {
+ try {
+ const result = await getBalance(config, {
+ address: address as `0x${string}`,
+ chainId: asset.chainId,
+ });
+ return {
+ assetId: asset.assetId,
+ balance: result.value.toString(),
+ precision: asset.precision,
+ };
+ } catch {
+ return null;
+ }
+ });
},
enabled: !!address,
- staleTime: 30_000,
+ staleTime: 60_000,
+ refetchOnWindowFocus: false,
})),
});
- const erc20Contracts = useMemo(
- () =>
- erc20Assets.map((asset) => ({
- address: asset.tokenAddress!,
- abi: erc20Abi,
- functionName: "balanceOf" as const,
- args: [address as `0x${string}`],
- chainId: asset.chainId,
- })),
- [erc20Assets, address],
- );
-
- const { data: erc20Results, isLoading: isErc20Loading } = useReadContracts({
- contracts: erc20Contracts,
- query: {
- enabled: !!address && erc20Contracts.length > 0,
- },
+ const erc20Queries = useQueries({
+ queries: erc20Assets.map((asset) => ({
+ queryKey: ["erc20Balance", address, asset.chainId, asset.tokenAddress],
+ queryFn: async () => {
+ if (!address) return null;
+ return balanceQueue.add(async () => {
+ try {
+ const result = await readContract(config, {
+ address: asset.tokenAddress!,
+ abi: erc20Abi,
+ functionName: "balanceOf",
+ args: [address as `0x${string}`],
+ chainId: asset.chainId,
+ });
+ return {
+ assetId: asset.assetId,
+ balance: (result as bigint).toString(),
+ precision: asset.precision,
+ };
+ } catch {
+ return null;
+ }
+ });
+ },
+ enabled: !!address,
+ staleTime: 60_000,
+ refetchOnWindowFocus: false,
+ })),
});
+ const isErc20Loading = erc20Queries.some((q) => q.isLoading);
+
const balances = useMemo((): BalancesMap => {
const result: BalancesMap = {};
@@ -193,22 +254,19 @@ export const useEvmBalances = (
}
});
- if (erc20Results) {
- erc20Assets.forEach((asset, index) => {
- const balanceResult = erc20Results[index];
- if (balanceResult?.status === "success" && balanceResult.result) {
- const balance = (balanceResult.result as bigint).toString();
- result[asset.assetId] = {
- assetId: asset.assetId,
- balance,
- balanceFormatted: formatAmount(balance, asset.precision),
- };
- }
- });
- }
+ erc20Queries.forEach((query) => {
+ if (query.data) {
+ const { assetId, balance, precision } = query.data;
+ result[assetId] = {
+ assetId,
+ balance,
+ balanceFormatted: formatAmount(balance, precision),
+ };
+ }
+ });
return result;
- }, [nativeQueries, erc20Results, erc20Assets]);
+ }, [nativeQueries, erc20Queries]);
const isLoading = nativeQueries.some((q) => q.isLoading) || isErc20Loading;
@@ -219,13 +277,13 @@ export const useEvmBalances = (
loading.add(nativeAssets[index].assetId);
}
});
- if (isErc20Loading) {
- erc20Assets.forEach((asset) => {
- loading.add(asset.assetId);
- });
- }
+ erc20Queries.forEach((query, index) => {
+ if (query.isLoading) {
+ loading.add(erc20Assets[index].assetId);
+ }
+ });
return loading;
- }, [nativeQueries, nativeAssets, isErc20Loading, erc20Assets]);
+ }, [nativeQueries, nativeAssets, erc20Queries, erc20Assets]);
return { data: balances, isLoading, loadingAssetIds };
};
diff --git a/yarn.lock b/yarn.lock
index 1e409a035cc..9a719cb4ffb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12703,6 +12703,7 @@ __metadata:
"@vitejs/plugin-react": ^4.2.0
react: ^18.2.0
react-dom: ^18.2.0
+ react-virtuoso: ^4.18.1
typescript: ^5.2.2
viem: ^2.21.0
vite: ^5.0.0
@@ -33233,6 +33234,16 @@ pvutils@latest:
languageName: node
linkType: hard
+"react-virtuoso@npm:^4.18.1":
+ version: 4.18.1
+ resolution: "react-virtuoso@npm:4.18.1"
+ peerDependencies:
+ react: ">=16 || >=17 || >= 18 || >= 19"
+ react-dom: ">=16 || >=17 || >= 18 || >=19"
+ checksum: 613261e9555a4d6fbb17ffc3443bc2d0b877c2f8ec52b5cd45ab40c91183e0e14d74151d1cf16195b0df91e41017d733d78aaf28b26987e55642d2b13aa733ed
+ languageName: node
+ linkType: hard
+
"react-virtuoso@npm:^4.7.11":
version: 4.7.11
resolution: "react-virtuoso@npm:4.7.11"
From c990620d8065f8d9375b79aada6828086ad6d63e Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 13 Jan 2026 11:43:44 +0100
Subject: [PATCH 17/41] chore: code quality improvements for swap widget MVP
- Fix all ESLint and Prettier errors across all files
- Replace custom ThrottledQueue with p-queue library
- Add proper ARIA roles and accessibility to modals
- Remove non-null assertions in favor of proper guards
- Add lint/type-check scripts to package.json
- Update README with new props (allowedChainIds, defaultReceiveAddress, enableWalletConnection, walletConnectProjectId)
- Fix demo app to use ShapeShift WalletConnect project ID
- Add min-height to token modal for better UX
---
packages/swap-widget-poc/README.md | 78 +-
packages/swap-widget-poc/package.json | 6 +-
packages/swap-widget-poc/src/api/client.ts | 84 +-
.../src/components/AddressInputModal.tsx | 229 ++---
.../src/components/QuoteSelector.tsx | 133 ++-
.../src/components/QuotesModal.tsx | 230 +++--
.../src/components/SettingsModal.tsx | 192 ++--
.../src/components/SwapWidget.css | 37 +
.../src/components/SwapWidget.tsx | 871 +++++++++---------
.../src/components/TokenSelectModal.css | 1 +
.../src/components/TokenSelectModal.tsx | 391 ++++----
.../src/components/WalletProvider.tsx | 125 +++
.../swap-widget-poc/src/constants/swappers.ts | 69 +-
packages/swap-widget-poc/src/demo/App.tsx | 16 +-
packages/swap-widget-poc/src/demo/main.tsx | 18 +-
.../swap-widget-poc/src/hooks/useAssets.ts | 194 ++--
.../swap-widget-poc/src/hooks/useBalances.ts | 246 +++--
.../src/hooks/useMarketData.ts | 165 ++--
.../swap-widget-poc/src/hooks/useSwapQuote.ts | 44 +-
.../swap-widget-poc/src/hooks/useSwapRates.ts | 52 +-
packages/swap-widget-poc/src/index.ts | 10 +-
packages/swap-widget-poc/src/types/index.ts | 351 ++++---
.../src/utils/addressValidation.ts | 184 ++--
.../swap-widget-poc/src/utils/redirect.ts | 61 +-
packages/swap-widget-poc/src/vite-env.d.ts | 6 +-
25 files changed, 1932 insertions(+), 1861 deletions(-)
create mode 100644 packages/swap-widget-poc/src/components/WalletProvider.tsx
diff --git a/packages/swap-widget-poc/README.md b/packages/swap-widget-poc/README.md
index 210cbe8a4d4..bbe7778ee31 100644
--- a/packages/swap-widget-poc/README.md
+++ b/packages/swap-widget-poc/README.md
@@ -57,24 +57,27 @@ function App() {
### SwapWidgetProps
-| Prop | Type | Default | Description |
-| ------------------ | ----------------------------------------------- | ---------------- | ------------------------------------------------------------------------ |
-| `apiKey` | `string` | - | ShapeShift API key for fetching swap rates. Required for production use. |
-| `apiBaseUrl` | `string` | - | Custom API base URL. Useful for testing or custom deployments. |
-| `defaultSellAsset` | `Asset` | ETH on Ethereum | Initial asset to sell. |
-| `defaultBuyAsset` | `Asset` | USDC on Ethereum | Initial asset to buy. |
-| `disabledChainIds` | `ChainId[]` | `[]` | Chain IDs to hide from the asset selector. |
-| `disabledAssetIds` | `AssetId[]` | `[]` | Asset IDs to hide from the asset selector. |
-| `allowedChainIds` | `ChainId[]` | - | If provided, only show assets from these chains. |
-| `allowedAssetIds` | `AssetId[]` | - | If provided, only show these specific assets. |
-| `walletClient` | `WalletClient` | - | Viem wallet client for executing EVM transactions. |
-| `onConnectWallet` | `() => void` | - | Callback when user clicks "Connect Wallet" button. |
-| `onSwapSuccess` | `(txHash: string) => void` | - | Callback when a swap transaction succeeds. |
-| `onSwapError` | `(error: Error) => void` | - | Callback when a swap transaction fails. |
-| `onAssetSelect` | `(type: "sell" \| "buy", asset: Asset) => void` | - | Callback when user selects an asset. |
-| `theme` | `ThemeMode \| ThemeConfig` | `"dark"` | Theme mode (`"light"` or `"dark"`) or full theme configuration. |
-| `defaultSlippage` | `string` | `"0.5"` | Default slippage tolerance percentage. |
-| `showPoweredBy` | `boolean` | `true` | Show "Powered by ShapeShift" branding. |
+| Prop | Type | Default | Description |
+| ------------------------ | ----------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------- |
+| `apiKey` | `string` | - | ShapeShift API key for fetching swap rates. Required for production use. |
+| `apiBaseUrl` | `string` | - | Custom API base URL. Useful for testing or custom deployments. |
+| `defaultSellAsset` | `Asset` | ETH on Ethereum | Initial asset to sell. |
+| `defaultBuyAsset` | `Asset` | USDC on Ethereum | Initial asset to buy. |
+| `disabledChainIds` | `ChainId[]` | `[]` | Chain IDs to hide from the asset selector. |
+| `disabledAssetIds` | `AssetId[]` | `[]` | Asset IDs to hide from the asset selector. |
+| `allowedChainIds` | `ChainId[]` | - | If provided, only show assets from these chains. Use this to restrict the widget to specific chains. |
+| `allowedAssetIds` | `AssetId[]` | - | If provided, only show these specific assets. |
+| `walletClient` | `WalletClient` | - | Viem wallet client for executing EVM transactions. |
+| `onConnectWallet` | `() => void` | - | Callback when user clicks "Connect Wallet" button. |
+| `onSwapSuccess` | `(txHash: string) => void` | - | Callback when a swap transaction succeeds. |
+| `onSwapError` | `(error: Error) => void` | - | Callback when a swap transaction fails. |
+| `onAssetSelect` | `(type: "sell" \| "buy", asset: Asset) => void` | - | Callback when user selects an asset. |
+| `theme` | `ThemeMode \| ThemeConfig` | `"dark"` | Theme mode (`"light"` or `"dark"`) or full theme configuration. |
+| `defaultSlippage` | `string` | `"0.5"` | Default slippage tolerance percentage. |
+| `showPoweredBy` | `boolean` | `true` | Show "Powered by ShapeShift" branding. |
+| `enableWalletConnection` | `boolean` | `false` | Enable built-in wallet connection UI using RainbowKit. Requires `walletConnectProjectId`. |
+| `walletConnectProjectId` | `string` | - | WalletConnect project ID for the built-in wallet connection. Get one at https://cloud.walletconnect.com. |
+| `defaultReceiveAddress` | `string` | - | Fixed receive address for swaps. When set, users cannot change the receive address. |
## Theming
@@ -204,6 +207,8 @@ function App() {
### Restricting Available Chains and Assets
+Use `allowedChainIds` to restrict the widget to only show specific chains. This is useful when you want to limit swaps to certain networks.
+
```tsx
import { SwapWidget, EVM_CHAIN_IDS } from "@shapeshiftoss/swap-widget-poc";
@@ -225,6 +230,43 @@ function App() {
}
```
+### With Built-in Wallet Connection
+
+The widget can manage wallet connections internally using RainbowKit. This is useful when you don't have an existing wallet connection setup.
+
+```tsx
+import { SwapWidget } from "@shapeshiftoss/swap-widget-poc";
+
+function App() {
+ return (
+
+ );
+}
+```
+
+### With Fixed Receive Address
+
+Use `defaultReceiveAddress` to lock the receive address. When set, users cannot change the destination address. This is useful for integrations where you want all swaps to go to a specific address.
+
+```tsx
+import { SwapWidget } from "@shapeshiftoss/swap-widget-poc";
+
+function App() {
+ return (
+
+ );
+}
+```
+
## Exported Types
```typescript
diff --git a/packages/swap-widget-poc/package.json b/packages/swap-widget-poc/package.json
index d804b55a2d9..9b44aa4cddf 100644
--- a/packages/swap-widget-poc/package.json
+++ b/packages/swap-widget-poc/package.json
@@ -9,12 +9,16 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "lint": "eslint src --ext .ts,.tsx",
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
+ "type-check": "tsc --noEmit"
},
"dependencies": {
"@rainbow-me/rainbowkit": "^2.2.3",
"@shapeshiftoss/caip": "^8.16.5",
"@tanstack/react-query": "^5.60.0",
+ "p-queue": "^8.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-virtuoso": "^4.18.1",
diff --git a/packages/swap-widget-poc/src/api/client.ts b/packages/swap-widget-poc/src/api/client.ts
index a7a9981b895..62085ec798a 100644
--- a/packages/swap-widget-poc/src/api/client.ts
+++ b/packages/swap-widget-poc/src/api/client.ts
@@ -1,75 +1,70 @@
-import type {
- RatesResponse,
- QuoteResponse,
- AssetsResponse,
- AssetId,
-} from "../types";
+import type { AssetId, AssetsResponse, QuoteResponse, RatesResponse } from '../types'
-const DEFAULT_API_BASE_URL = "https://api.shapeshift.com";
+const DEFAULT_API_BASE_URL = 'https://api.shapeshift.com'
export type ApiClientConfig = {
- baseUrl?: string;
- apiKey?: string;
-};
+ baseUrl?: string
+ apiKey?: string
+}
export const createApiClient = (config: ApiClientConfig = {}) => {
- const baseUrl = config.baseUrl ?? DEFAULT_API_BASE_URL;
+ const baseUrl = config.baseUrl ?? DEFAULT_API_BASE_URL
const fetchWithConfig = async (
endpoint: string,
params?: Record,
- method: "GET" | "POST" = "GET",
+ method: 'GET' | 'POST' = 'GET',
): Promise => {
- const url = new URL(`${baseUrl}${endpoint}`);
+ const url = new URL(`${baseUrl}${endpoint}`)
const headers: Record = {
- "Content-Type": "application/json",
- };
+ 'Content-Type': 'application/json',
+ }
if (config.apiKey) {
- headers["x-api-key"] = config.apiKey;
+ headers['x-api-key'] = config.apiKey
}
- const fetchOptions: RequestInit = { headers, method };
+ const fetchOptions: RequestInit = { headers, method }
- if (method === "GET" && params) {
+ if (method === 'GET' && params) {
Object.entries(params).forEach(([key, value]) => {
- url.searchParams.append(key, value);
- });
- } else if (method === "POST" && params) {
- fetchOptions.body = JSON.stringify(params);
+ url.searchParams.append(key, value)
+ })
+ } else if (method === 'POST' && params) {
+ fetchOptions.body = JSON.stringify(params)
}
- const response = await fetch(url.toString(), fetchOptions);
+ const response = await fetch(url.toString(), fetchOptions)
if (!response.ok) {
- throw new Error(`API error: ${response.status} ${response.statusText}`);
+ throw new Error(`API error: ${response.status} ${response.statusText}`)
}
- return response.json() as Promise;
- };
+ return response.json() as Promise
+ }
return {
- getAssets: () => fetchWithConfig("/v1/assets"),
+ getAssets: () => fetchWithConfig('/v1/assets'),
getRates: (params: {
- sellAssetId: AssetId;
- buyAssetId: AssetId;
- sellAmountCryptoBaseUnit: string;
+ sellAssetId: AssetId
+ buyAssetId: AssetId
+ sellAmountCryptoBaseUnit: string
}) =>
- fetchWithConfig("/v1/swap/rates", {
+ fetchWithConfig('/v1/swap/rates', {
sellAssetId: params.sellAssetId,
buyAssetId: params.buyAssetId,
sellAmountCryptoBaseUnit: params.sellAmountCryptoBaseUnit,
}),
getQuote: (params: {
- sellAssetId: AssetId;
- buyAssetId: AssetId;
- sellAmountCryptoBaseUnit: string;
- sendAddress: string;
- receiveAddress: string;
- swapperName: string;
- slippageTolerancePercentageDecimal?: string;
+ sellAssetId: AssetId
+ buyAssetId: AssetId
+ sellAmountCryptoBaseUnit: string
+ sendAddress: string
+ receiveAddress: string
+ swapperName: string
+ slippageTolerancePercentageDecimal?: string
}) =>
fetchWithConfig(
- "/v1/swap/quote",
+ '/v1/swap/quote',
{
sellAssetId: params.sellAssetId,
buyAssetId: params.buyAssetId,
@@ -78,13 +73,12 @@ export const createApiClient = (config: ApiClientConfig = {}) => {
receiveAddress: params.receiveAddress,
swapperName: params.swapperName,
...(params.slippageTolerancePercentageDecimal && {
- slippageTolerancePercentageDecimal:
- params.slippageTolerancePercentageDecimal,
+ slippageTolerancePercentageDecimal: params.slippageTolerancePercentageDecimal,
}),
},
- "POST",
+ 'POST',
),
- };
-};
+ }
+}
-export type ApiClient = ReturnType;
+export type ApiClient = ReturnType
diff --git a/packages/swap-widget-poc/src/components/AddressInputModal.tsx b/packages/swap-widget-poc/src/components/AddressInputModal.tsx
index 54075b3d5e3..200aa0d7a99 100644
--- a/packages/swap-widget-poc/src/components/AddressInputModal.tsx
+++ b/packages/swap-widget-poc/src/components/AddressInputModal.tsx
@@ -1,31 +1,30 @@
-import { useState, useCallback, useEffect, useMemo } from "react";
-import type { ChainId } from "../types";
-import {
- validateAddress,
- getAddressFormatHint,
-} from "../utils/addressValidation";
-import "./AddressInputModal.css";
+import './AddressInputModal.css'
+
+import { useCallback, useEffect, useMemo, useState } from 'react'
+
+import type { ChainId } from '../types'
+import { getAddressFormatHint, validateAddress } from '../utils/addressValidation'
const useLockBodyScroll = (isLocked: boolean) => {
useEffect(() => {
- if (!isLocked) return;
- const originalOverflow = document.body.style.overflow;
- document.body.style.overflow = "hidden";
+ if (!isLocked) return
+ const originalOverflow = document.body.style.overflow
+ document.body.style.overflow = 'hidden'
return () => {
- document.body.style.overflow = originalOverflow;
- };
- }, [isLocked]);
-};
+ document.body.style.overflow = originalOverflow
+ }
+ }, [isLocked])
+}
type AddressInputModalProps = {
- isOpen: boolean;
- onClose: () => void;
- chainId: ChainId;
- chainName: string;
- currentAddress: string;
- onAddressChange: (address: string) => void;
- walletAddress?: string;
-};
+ isOpen: boolean
+ onClose: () => void
+ chainId: ChainId
+ chainName: string
+ currentAddress: string
+ onAddressChange: (address: string) => void
+ walletAddress?: string
+}
export const AddressInputModal = ({
isOpen,
@@ -36,182 +35,188 @@ export const AddressInputModal = ({
onAddressChange,
walletAddress,
}: AddressInputModalProps) => {
- useLockBodyScroll(isOpen);
- const [inputValue, setInputValue] = useState(currentAddress);
- const [hasInteracted, setHasInteracted] = useState(false);
+ useLockBodyScroll(isOpen)
+ const [inputValue, setInputValue] = useState(currentAddress)
+ const [hasInteracted, setHasInteracted] = useState(false)
useEffect(() => {
if (isOpen) {
- setInputValue(currentAddress);
- setHasInteracted(false);
+ setInputValue(currentAddress)
+ setHasInteracted(false)
}
- }, [isOpen, currentAddress]);
+ }, [isOpen, currentAddress])
const validation = useMemo(() => {
if (!inputValue || !hasInteracted) {
- return { valid: true, error: undefined };
+ return { valid: true, error: undefined }
}
- return validateAddress(inputValue, chainId);
- }, [inputValue, chainId, hasInteracted]);
+ return validateAddress(inputValue, chainId)
+ }, [inputValue, chainId, hasInteracted])
- const formatHint = useMemo(() => getAddressFormatHint(chainId), [chainId]);
+ const formatHint = useMemo(() => getAddressFormatHint(chainId), [chainId])
const handleInputChange = useCallback((value: string) => {
- setInputValue(value);
- setHasInteracted(true);
- }, []);
+ setInputValue(value)
+ setHasInteracted(true)
+ }, [])
const handleUseWalletAddress = useCallback(() => {
if (walletAddress) {
- setInputValue(walletAddress);
- setHasInteracted(true);
+ setInputValue(walletAddress)
+ setHasInteracted(true)
}
- }, [walletAddress]);
+ }, [walletAddress])
const handleConfirm = useCallback(() => {
- const result = validateAddress(inputValue, chainId);
+ const result = validateAddress(inputValue, chainId)
if (result.valid) {
- onAddressChange(inputValue);
- onClose();
+ onAddressChange(inputValue)
+ onClose()
}
- }, [inputValue, chainId, onAddressChange, onClose]);
+ }, [inputValue, chainId, onAddressChange, onClose])
const handleClear = useCallback(() => {
- setInputValue("");
- setHasInteracted(false);
- onAddressChange("");
- onClose();
- }, [onAddressChange, onClose]);
+ setInputValue('')
+ setHasInteracted(false)
+ onAddressChange('')
+ onClose()
+ }, [onAddressChange, onClose])
const handleBackdropClick = useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
- onClose();
+ onClose()
}
},
[onClose],
- );
+ )
const isConfirmDisabled = useMemo(() => {
- if (!inputValue) return true;
- return !validateAddress(inputValue, chainId).valid;
- }, [inputValue, chainId]);
+ if (!inputValue) return true
+ return !validateAddress(inputValue, chainId).valid
+ }, [inputValue, chainId])
- if (!isOpen) return null;
+ if (!isOpen) return null
return (
-
-
-
-
Receive Address
-
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
+ e.key === 'Escape' && onClose()}
+ role='dialog'
+ aria-modal='true'
+ aria-labelledby='address-modal-title'
+ >
+
+
+
+ Receive Address
+
+
-
+
-
-
+
+
Enter {chainName} address
{!validation.valid && hasInteracted && validation.error && (
-
+
-
-
+
+
{validation.error}
)}
{walletAddress && (
-
+
-
-
+
+
Use connected wallet
)}
-
+
Reset to Wallet
Confirm
@@ -219,5 +224,5 @@ export const AddressInputModal = ({
- );
-};
+ )
+}
diff --git a/packages/swap-widget-poc/src/components/QuoteSelector.tsx b/packages/swap-widget-poc/src/components/QuoteSelector.tsx
index cd3292e646a..a73e5ac833a 100644
--- a/packages/swap-widget-poc/src/components/QuoteSelector.tsx
+++ b/packages/swap-widget-poc/src/components/QuoteSelector.tsx
@@ -1,21 +1,23 @@
-import { useState, useMemo, useCallback } from "react";
-import type { TradeRate, Asset } from "../types";
-import { formatAmount } from "../types";
-import { getSwapperIcon, getSwapperColor } from "../constants/swappers";
-import { QuotesModal } from "./QuotesModal";
-import { formatUsdValue } from "../hooks/useMarketData";
-import "./QuoteSelector.css";
+import './QuoteSelector.css'
+
+import { useCallback, useMemo, useState } from 'react'
+
+import { getSwapperColor, getSwapperIcon } from '../constants/swappers'
+import { formatUsdValue } from '../hooks/useMarketData'
+import type { Asset, TradeRate } from '../types'
+import { formatAmount } from '../types'
+import { QuotesModal } from './QuotesModal'
type QuoteSelectorProps = {
- rates: TradeRate[];
- selectedRate: TradeRate | null;
- onSelectRate: (rate: TradeRate) => void;
- buyAsset: Asset;
- sellAsset: Asset;
- sellAmountBaseUnit: string;
- isLoading: boolean;
- buyAssetUsdPrice?: string;
-};
+ rates: TradeRate[]
+ selectedRate: TradeRate | null
+ onSelectRate: (rate: TradeRate) => void
+ buyAsset: Asset
+ sellAsset: Asset
+ sellAmountBaseUnit: string
+ isLoading: boolean
+ buyAssetUsdPrice?: string
+}
export const QuoteSelector = ({
rates,
@@ -27,107 +29,92 @@ export const QuoteSelector = ({
isLoading,
buyAssetUsdPrice,
}: QuoteSelectorProps) => {
- const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false)
- const bestRate = useMemo(() => rates[0], [rates]);
- const alternativeRatesCount = useMemo(
- () => Math.max(0, rates.length - 1),
- [rates],
- );
+ const bestRate = useMemo(() => rates[0], [rates])
+ const alternativeRatesCount = useMemo(() => Math.max(0, rates.length - 1), [rates])
const handleOpenModal = useCallback(() => {
if (rates.length > 0) {
- setIsModalOpen(true);
+ setIsModalOpen(true)
}
- }, [rates.length]);
+ }, [rates.length])
const handleCloseModal = useCallback(() => {
- setIsModalOpen(false);
- }, []);
+ setIsModalOpen(false)
+ }, [])
const handleSelectRate = useCallback(
(rate: TradeRate) => {
- onSelectRate(rate);
+ onSelectRate(rate)
},
[onSelectRate],
- );
+ )
if (isLoading) {
return (
-
-
-
+
- );
+ )
}
if (!bestRate) {
- return null;
+ return null
}
- const displayRate = selectedRate ?? bestRate;
- const buyAmount = displayRate.buyAmountCryptoBaseUnit ?? "0";
- const swapperIcon = getSwapperIcon(displayRate.swapperName);
- const swapperColor = getSwapperColor(displayRate.swapperName);
- const formattedBuyAmount = formatAmount(buyAmount, buyAsset.precision);
- const usdValue = formatUsdValue(
- buyAmount,
- buyAsset.precision,
- buyAssetUsdPrice,
- );
+ const displayRate = selectedRate ?? bestRate
+ const buyAmount = displayRate.buyAmountCryptoBaseUnit ?? '0'
+ const swapperIcon = getSwapperIcon(displayRate.swapperName)
+ const swapperColor = getSwapperColor(displayRate.swapperName)
+ const formattedBuyAmount = formatAmount(buyAmount, buyAsset.precision)
+ const usdValue = formatUsdValue(buyAmount, buyAsset.precision, buyAssetUsdPrice)
return (
<>
-
-
-
+
+
+
{swapperIcon ? (
) : (
{displayRate.swapperName.charAt(0)}
)}
-
- {displayRate.swapperName}
-
- {displayRate === bestRate && (
-
Best
- )}
+
{displayRate.swapperName}
+ {displayRate === bestRate &&
Best }
-
{usdValue}
+
{usdValue}
-
-
-
{formattedBuyAmount}
-
{buyAsset.symbol}
+
+
+ {formattedBuyAmount}
+ {buyAsset.symbol}
{alternativeRatesCount > 0 && (
-
+
+{alternativeRatesCount} more
-
+
)}
@@ -146,5 +133,5 @@ export const QuoteSelector = ({
buyAssetUsdPrice={buyAssetUsdPrice}
/>
>
- );
-};
+ )
+}
diff --git a/packages/swap-widget-poc/src/components/QuotesModal.tsx b/packages/swap-widget-poc/src/components/QuotesModal.tsx
index c77863142c7..482f3011e66 100644
--- a/packages/swap-widget-poc/src/components/QuotesModal.tsx
+++ b/packages/swap-widget-poc/src/components/QuotesModal.tsx
@@ -1,45 +1,42 @@
-import "./QuotesModal.css";
+import './QuotesModal.css'
-import { useCallback, useEffect, useMemo } from "react";
+import { useCallback, useEffect, useMemo } from 'react'
-import { getSwapperColor, getSwapperIcon } from "../constants/swappers";
-import { formatUsdValue } from "../hooks/useMarketData";
-import type { Asset, TradeRate } from "../types";
-import { formatAmount } from "../types";
+import { getSwapperColor, getSwapperIcon } from '../constants/swappers'
+import { formatUsdValue } from '../hooks/useMarketData'
+import type { Asset, TradeRate } from '../types'
+import { formatAmount } from '../types'
const useLockBodyScroll = (isLocked: boolean) => {
useEffect(() => {
- if (!isLocked) return;
- const originalOverflow = document.body.style.overflow;
- document.body.style.overflow = "hidden";
+ if (!isLocked) return
+ const originalOverflow = document.body.style.overflow
+ document.body.style.overflow = 'hidden'
return () => {
- document.body.style.overflow = originalOverflow;
- };
- }, [isLocked]);
-};
+ document.body.style.overflow = originalOverflow
+ }
+ }, [isLocked])
+}
type QuotesModalProps = {
- isOpen: boolean;
- onClose: () => void;
- rates: TradeRate[];
- selectedRate: TradeRate | null;
- onSelectRate: (rate: TradeRate) => void;
- buyAsset: Asset;
- sellAsset: Asset;
- sellAmountBaseUnit: string;
- buyAssetUsdPrice?: string;
-};
+ isOpen: boolean
+ onClose: () => void
+ rates: TradeRate[]
+ selectedRate: TradeRate | null
+ onSelectRate: (rate: TradeRate) => void
+ buyAsset: Asset
+ sellAsset: Asset
+ sellAmountBaseUnit: string
+ buyAssetUsdPrice?: string
+}
-const calculateSavingsPercent = (
- bestAmount: string,
- currentAmount: string,
-): string | null => {
- const best = parseFloat(bestAmount || "0");
- const current = parseFloat(currentAmount || "0");
- if (best === 0) return null;
- const diff = ((best - current) / best) * 100;
- return diff > 0.1 ? diff.toFixed(2) : null;
-};
+const calculateSavingsPercent = (bestAmount: string, currentAmount: string): string | null => {
+ const best = parseFloat(bestAmount || '0')
+ const current = parseFloat(currentAmount || '0')
+ if (best === 0) return null
+ const diff = ((best - current) / best) * 100
+ return diff > 0.1 ? diff.toFixed(2) : null
+}
export const QuotesModal = ({
isOpen,
@@ -52,154 +49,133 @@ export const QuotesModal = ({
sellAmountBaseUnit,
buyAssetUsdPrice,
}: QuotesModalProps) => {
- useLockBodyScroll(isOpen);
+ useLockBodyScroll(isOpen)
const handleBackdropClick = useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
- onClose();
+ onClose()
}
},
[onClose],
- );
+ )
const handleSelectRate = useCallback(
(rate: TradeRate) => {
- onSelectRate(rate);
- onClose();
+ onSelectRate(rate)
+ onClose()
},
[onSelectRate, onClose],
- );
+ )
const sortedRates = useMemo(() => {
return [...rates]
- .filter((r) => !r.error && r.buyAmountCryptoBaseUnit !== "0")
+ .filter(r => !r.error && r.buyAmountCryptoBaseUnit !== '0')
.sort((a, b) => {
- const aAmount = parseFloat(a.buyAmountCryptoBaseUnit || "0");
- const bAmount = parseFloat(b.buyAmountCryptoBaseUnit || "0");
- return bAmount - aAmount;
- });
- }, [rates]);
+ const aAmount = parseFloat(a.buyAmountCryptoBaseUnit || '0')
+ const bAmount = parseFloat(b.buyAmountCryptoBaseUnit || '0')
+ return bAmount - aAmount
+ })
+ }, [rates])
- const bestRate = useMemo(() => sortedRates[0], [sortedRates]);
- const bestBuyAmount = bestRate?.buyAmountCryptoBaseUnit ?? "0";
+ const bestRate = useMemo(() => sortedRates[0], [sortedRates])
+ const bestBuyAmount = bestRate?.buyAmountCryptoBaseUnit ?? '0'
- if (!isOpen) return null;
+ if (!isOpen) return null
return (
-
-
-
-
-
Select Route
-
- {formatAmount(sellAmountBaseUnit, sellAsset.precision)}{" "}
- {sellAsset.symbol} → {buyAsset.symbol}
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
+ e.key === 'Escape' && onClose()}
+ role='dialog'
+ aria-modal='true'
+ aria-labelledby='quotes-modal-title'
+ >
+
+
+
+
+ Select Route
+
+
+ {formatAmount(sellAmountBaseUnit, sellAsset.precision)} {sellAsset.symbol} →{' '}
+ {buyAsset.symbol}
-
+
-
+
-
+
{sortedRates.map((rate, index) => {
- const buyAmount = rate.buyAmountCryptoBaseUnit ?? "0";
- const estimatedTime = rate.estimatedExecutionTimeMs;
- const isBest = index === 0;
- const isSelected = selectedRate?.id === rate.id;
- const swapperIcon = getSwapperIcon(rate.swapperName);
- const swapperColor = getSwapperColor(rate.swapperName);
- const formattedBuyAmount = formatAmount(
- buyAmount,
- buyAsset.precision,
- );
- const usdValue = formatUsdValue(
- buyAmount,
- buyAsset.precision,
- buyAssetUsdPrice,
- );
- const savingsPercent = isBest
- ? null
- : calculateSavingsPercent(bestBuyAmount, buyAmount);
- const estimatedSeconds = estimatedTime
- ? Math.round(estimatedTime / 1000)
- : 0;
- const hasTime = estimatedSeconds > 0;
+ const buyAmount = rate.buyAmountCryptoBaseUnit ?? '0'
+ const estimatedTime = rate.estimatedExecutionTimeMs
+ const isBest = index === 0
+ const isSelected = selectedRate?.id === rate.id
+ const swapperIcon = getSwapperIcon(rate.swapperName)
+ const swapperColor = getSwapperColor(rate.swapperName)
+ const formattedBuyAmount = formatAmount(buyAmount, buyAsset.precision)
+ const usdValue = formatUsdValue(buyAmount, buyAsset.precision, buyAssetUsdPrice)
+ const savingsPercent = isBest ? null : calculateSavingsPercent(bestBuyAmount, buyAmount)
+ const estimatedSeconds = estimatedTime ? Math.round(estimatedTime / 1000) : 0
+ const hasTime = estimatedSeconds > 0
return (
handleSelectRate(rate)}
- type="button"
+ type='button'
>
-
+
{swapperIcon ? (
-
+
) : (
{rate.swapperName.charAt(0)}
)}
-
-
-
- {rate.swapperName}
-
- {isBest && (
-
Best
- )}
+
+
+ {rate.swapperName}
+ {isBest && Best }
{savingsPercent && (
-
- -{savingsPercent}%
-
+ -{savingsPercent}%
)}
- {hasTime && (
-
- ~{estimatedSeconds}s
-
- )}
+ {hasTime &&
~{estimatedSeconds}s }
-
-
- {formattedBuyAmount}{" "}
-
- {buyAsset.symbol}
-
+
+
+ {formattedBuyAmount}{' '}
+ {buyAsset.symbol}
- {usdValue}
+ {usdValue}
- );
+ )
})}
- );
-};
+ )
+}
diff --git a/packages/swap-widget-poc/src/components/SettingsModal.tsx b/packages/swap-widget-poc/src/components/SettingsModal.tsx
index 850211b5f54..07911019821 100644
--- a/packages/swap-widget-poc/src/components/SettingsModal.tsx
+++ b/packages/swap-widget-poc/src/components/SettingsModal.tsx
@@ -1,25 +1,26 @@
-import { useState, useCallback, useEffect } from "react";
-import "./SettingsModal.css";
+import './SettingsModal.css'
-const SLIPPAGE_PRESETS = ["0.1", "0.5", "1.0"];
+import { useCallback, useEffect, useState } from 'react'
+
+const SLIPPAGE_PRESETS = ['0.1', '0.5', '1.0']
const useLockBodyScroll = (isLocked: boolean) => {
useEffect(() => {
- if (!isLocked) return;
- const originalOverflow = document.body.style.overflow;
- document.body.style.overflow = "hidden";
+ if (!isLocked) return
+ const originalOverflow = document.body.style.overflow
+ document.body.style.overflow = 'hidden'
return () => {
- document.body.style.overflow = originalOverflow;
- };
- }, [isLocked]);
-};
+ document.body.style.overflow = originalOverflow
+ }
+ }, [isLocked])
+}
type SettingsModalProps = {
- isOpen: boolean;
- onClose: () => void;
- slippage: string;
- onSlippageChange: (slippage: string) => void;
-};
+ isOpen: boolean
+ onClose: () => void
+ slippage: string
+ onSlippageChange: (slippage: string) => void
+}
export const SettingsModal = ({
isOpen,
@@ -27,148 +28,147 @@ export const SettingsModal = ({
slippage,
onSlippageChange,
}: SettingsModalProps) => {
- useLockBodyScroll(isOpen);
- const [customSlippage, setCustomSlippage] = useState("");
- const [isCustom, setIsCustom] = useState(
- !SLIPPAGE_PRESETS.includes(slippage),
- );
+ useLockBodyScroll(isOpen)
+ const [customSlippage, setCustomSlippage] = useState('')
+ const [isCustom, setIsCustom] = useState(!SLIPPAGE_PRESETS.includes(slippage))
const handlePresetClick = useCallback(
(preset: string) => {
- setIsCustom(false);
- setCustomSlippage("");
- onSlippageChange(preset);
+ setIsCustom(false)
+ setCustomSlippage('')
+ onSlippageChange(preset)
},
[onSlippageChange],
- );
+ )
const handleCustomChange = useCallback(
(value: string) => {
- const sanitized = value.replace(/[^0-9.]/g, "");
- const parts = sanitized.split(".");
- const formatted =
- parts.length > 2 ? `${parts[0]}.${parts.slice(1).join("")}` : sanitized;
+ const sanitized = value.replace(/[^0-9.]/g, '')
+ const parts = sanitized.split('.')
+ const formatted = parts.length > 2 ? `${parts[0]}.${parts.slice(1).join('')}` : sanitized
- setCustomSlippage(formatted);
- setIsCustom(true);
+ setCustomSlippage(formatted)
+ setIsCustom(true)
- const numValue = parseFloat(formatted);
+ const numValue = parseFloat(formatted)
if (!isNaN(numValue) && numValue > 0 && numValue <= 50) {
- onSlippageChange(formatted);
+ onSlippageChange(formatted)
}
},
[onSlippageChange],
- );
+ )
const handleBackdropClick = useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
- onClose();
+ onClose()
}
},
[onClose],
- );
+ )
- if (!isOpen) return null;
+ if (!isOpen) return null
- const currentSlippageNum = parseFloat(slippage);
- const isHighSlippage = currentSlippageNum > 1;
- const isVeryHighSlippage = currentSlippageNum > 5;
+ const currentSlippageNum = parseFloat(slippage)
+ const isHighSlippage = currentSlippageNum > 1
+ const isVeryHighSlippage = currentSlippageNum > 5
return (
-
-
-
-
Settings
-
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
+ e.key === 'Escape' && onClose()}
+ role='dialog'
+ aria-modal='true'
+ aria-labelledby='settings-modal-title'
+ >
+
+
-
-
-
+
+
+
Slippage Tolerance
-
-
+
+
-
- {SLIPPAGE_PRESETS.map((preset) => (
+
+ {SLIPPAGE_PRESETS.map(preset => (
handlePresetClick(preset)}
- type="button"
+ type='button'
>
{preset}%
))}
-
{isHighSlippage && (
-
+
-
+
{isVeryHighSlippage
- ? "Very high slippage. Your transaction may be frontrun."
- : "High slippage may result in unfavorable rates."}
+ ? 'Very high slippage. Your transaction may be frontrun.'
+ : 'High slippage may result in unfavorable rates.'}
)}
@@ -176,5 +176,5 @@ export const SettingsModal = ({
- );
-};
+ )
+}
diff --git a/packages/swap-widget-poc/src/components/SwapWidget.css b/packages/swap-widget-poc/src/components/SwapWidget.css
index c1fef415615..d7d5407e62d 100644
--- a/packages/swap-widget-poc/src/components/SwapWidget.css
+++ b/packages/swap-widget-poc/src/components/SwapWidget.css
@@ -66,6 +66,43 @@
color: var(--ssw-text-primary);
}
+.ssw-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.ssw-connect-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ border: 1px solid var(--ssw-border);
+ background: var(--ssw-bg-tertiary);
+ border-radius: 10px;
+ color: var(--ssw-text-primary);
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.ssw-connect-btn:hover {
+ background: var(--ssw-bg-hover);
+ border-color: var(--ssw-accent);
+}
+
+.ssw-connect-btn.ssw-connected {
+ background: color-mix(in srgb, var(--ssw-accent) 15%, var(--ssw-bg-tertiary));
+ border-color: color-mix(in srgb, var(--ssw-accent) 40%, transparent);
+}
+
+.ssw-connect-btn.ssw-wrong-network {
+ background: color-mix(in srgb, var(--ssw-error) 15%, var(--ssw-bg-tertiary));
+ border-color: var(--ssw-error);
+ color: var(--ssw-error);
+}
+
.ssw-settings-btn {
padding: 8px;
border: none;
diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget-poc/src/components/SwapWidget.tsx
index 0da1c267da3..dc0e325605e 100644
--- a/packages/swap-widget-poc/src/components/SwapWidget.tsx
+++ b/packages/swap-widget-poc/src/components/SwapWidget.tsx
@@ -1,43 +1,46 @@
-import { useState, useMemo, useCallback } from "react";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { encodeFunctionData } from "viem";
-import type { WalletClient } from "viem";
-import { createApiClient } from "../api/client";
-import { useSwapRates } from "../hooks/useSwapRates";
-import { useChainInfo } from "../hooks/useAssets";
-import { useMarketData, formatUsdValue } from "../hooks/useMarketData";
-import { useAssetBalance } from "../hooks/useBalances";
-import type { Asset, TradeRate, SwapWidgetProps, ThemeMode } from "../types";
+import './SwapWidget.css'
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { useCallback, useMemo, useState } from 'react'
+import type { WalletClient } from 'viem'
+import { encodeFunctionData } from 'viem'
+
+import { createApiClient } from '../api/client'
+import { useChainInfo } from '../hooks/useAssets'
+import { useAssetBalance } from '../hooks/useBalances'
+import { formatUsdValue, useMarketData } from '../hooks/useMarketData'
+import { useSwapRates } from '../hooks/useSwapRates'
+import type { Asset, SwapWidgetProps, ThemeMode, TradeRate } from '../types'
import {
- getEvmChainIdNumber,
- parseAmount,
formatAmount,
getChainType,
+ getEvmChainIdNumber,
+ parseAmount,
truncateAddress,
-} from "../types";
-import { TokenSelectModal } from "./TokenSelectModal";
-import { SettingsModal } from "./SettingsModal";
-import { QuoteSelector } from "./QuoteSelector";
-import { AddressInputModal } from "./AddressInputModal";
-import "./SwapWidget.css";
+} from '../types'
+import { AddressInputModal } from './AddressInputModal'
+import { QuoteSelector } from './QuoteSelector'
+import { SettingsModal } from './SettingsModal'
+import { TokenSelectModal } from './TokenSelectModal'
+import { ConnectWalletButton, InternalWalletProvider } from './WalletProvider'
const DEFAULT_SELL_ASSET: Asset = {
- assetId: "eip155:1/slip44:60",
- chainId: "eip155:1",
- symbol: "ETH",
- name: "Ethereum",
+ assetId: 'eip155:1/slip44:60',
+ chainId: 'eip155:1',
+ symbol: 'ETH',
+ name: 'Ethereum',
precision: 18,
- icon: "https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/ethereum/info/logo.png",
-};
+ icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/ethereum/info/logo.png',
+}
const DEFAULT_BUY_ASSET: Asset = {
- assetId: "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
- chainId: "eip155:1",
- symbol: "USDC",
- name: "USD Coin",
+ assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ chainId: 'eip155:1',
+ symbol: 'USDC',
+ name: 'USD Coin',
precision: 6,
- icon: "https://assets.coingecko.com/coins/images/6319/standard/usdc.png",
-};
+ icon: 'https://assets.coingecko.com/coins/images/6319/standard/usdc.png',
+}
const queryClient = new QueryClient({
defaultOptions: {
@@ -46,58 +49,62 @@ const queryClient = new QueryClient({
staleTime: 30_000,
},
},
-});
+})
type SwapWidgetInnerProps = SwapWidgetProps & {
- apiClient: ReturnType
;
-};
+ apiClient: ReturnType
+}
-const SwapWidgetInner = ({
+type SwapWidgetCoreProps = SwapWidgetInnerProps & {
+ enableWalletConnection?: boolean
+}
+
+const SwapWidgetCore = ({
defaultSellAsset = DEFAULT_SELL_ASSET,
defaultBuyAsset = DEFAULT_BUY_ASSET,
disabledChainIds = [],
disabledAssetIds = [],
+ allowedChainIds,
walletClient,
onConnectWallet,
onSwapSuccess,
onSwapError,
onAssetSelect,
- theme = "dark",
- defaultSlippage = "0.5",
+ theme = 'dark',
+ defaultSlippage = '0.5',
showPoweredBy = true,
+ defaultReceiveAddress,
+ enableWalletConnection = false,
apiClient,
-}: SwapWidgetInnerProps) => {
- const [sellAsset, setSellAsset] = useState(defaultSellAsset);
- const [buyAsset, setBuyAsset] = useState(defaultBuyAsset);
- const [sellAmount, setSellAmount] = useState("");
- const [selectedRate, setSelectedRate] = useState(null);
- const [slippage, setSlippage] = useState(defaultSlippage);
- const [isExecuting, setIsExecuting] = useState(false);
+}: SwapWidgetCoreProps) => {
+ const [sellAsset, setSellAsset] = useState(defaultSellAsset)
+ const [buyAsset, setBuyAsset] = useState(defaultBuyAsset)
+ const [sellAmount, setSellAmount] = useState('')
+ const [selectedRate, setSelectedRate] = useState(null)
+ const [slippage, setSlippage] = useState(defaultSlippage)
+ const [isExecuting, setIsExecuting] = useState(false)
const [txStatus, setTxStatus] = useState<{
- status: "pending" | "success" | "error";
- txHash?: string;
- message?: string;
- } | null>(null);
+ status: 'pending' | 'success' | 'error'
+ txHash?: string
+ message?: string
+ } | null>(null)
- const [tokenModalType, setTokenModalType] = useState<"sell" | "buy" | null>(
- null,
- );
- const [isSettingsOpen, setIsSettingsOpen] = useState(false);
- const [isAddressModalOpen, setIsAddressModalOpen] = useState(false);
- const [customReceiveAddress, setCustomReceiveAddress] = useState("");
+ const [tokenModalType, setTokenModalType] = useState<'sell' | 'buy' | null>(null)
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false)
+ const [isAddressModalOpen, setIsAddressModalOpen] = useState(false)
+ const [customReceiveAddress, setCustomReceiveAddress] = useState('')
- const themeMode: ThemeMode = typeof theme === "string" ? theme : theme.mode;
- const themeConfig = typeof theme === "object" ? theme : undefined;
+ const themeMode: ThemeMode = typeof theme === 'string' ? theme : theme.mode
+ const themeConfig = typeof theme === 'object' ? theme : undefined
const sellAmountBaseUnit = useMemo(
- () =>
- sellAmount ? parseAmount(sellAmount, sellAsset.precision) : undefined,
+ () => (sellAmount ? parseAmount(sellAmount, sellAsset.precision) : undefined),
[sellAmount, sellAsset.precision],
- );
+ )
- const isSellAssetEvm = getChainType(sellAsset.chainId) === "evm";
- const isBuyAssetEvm = getChainType(buyAsset.chainId) === "evm";
- const canExecuteDirectly = isSellAssetEvm && isBuyAssetEvm;
+ const isSellAssetEvm = getChainType(sellAsset.chainId) === 'evm'
+ const isBuyAssetEvm = getChainType(buyAsset.chainId) === 'evm'
+ const canExecuteDirectly = isSellAssetEvm && isBuyAssetEvm
const {
data: rates,
@@ -107,59 +114,58 @@ const SwapWidgetInner = ({
sellAssetId: sellAsset.assetId,
buyAssetId: buyAsset.assetId,
sellAmountCryptoBaseUnit: sellAmountBaseUnit,
- enabled:
- !!sellAmountBaseUnit && sellAmountBaseUnit !== "0" && isSellAssetEvm,
- });
+ enabled: !!sellAmountBaseUnit && sellAmountBaseUnit !== '0' && isSellAssetEvm,
+ })
const walletAddress = useMemo(() => {
- if (!walletClient) return undefined;
- return (walletClient as WalletClient).account?.address;
- }, [walletClient]);
+ if (!walletClient) return undefined
+ return (walletClient as WalletClient).account?.address
+ }, [walletClient])
const effectiveReceiveAddress = useMemo(() => {
- return customReceiveAddress || walletAddress || "";
- }, [customReceiveAddress, walletAddress]);
+ return customReceiveAddress || defaultReceiveAddress || walletAddress || ''
+ }, [customReceiveAddress, defaultReceiveAddress, walletAddress])
const isCustomAddress = useMemo(() => {
- return !!customReceiveAddress && customReceiveAddress !== walletAddress;
- }, [customReceiveAddress, walletAddress]);
+ return !!customReceiveAddress && customReceiveAddress !== walletAddress
+ }, [customReceiveAddress, walletAddress])
const {
data: sellAssetBalance,
isLoading: isSellBalanceLoading,
refetch: refetchSellBalance,
- } = useAssetBalance(walletAddress, sellAsset.assetId, sellAsset.precision);
+ } = useAssetBalance(walletAddress, sellAsset.assetId, sellAsset.precision)
const {
data: buyAssetBalance,
isLoading: isBuyBalanceLoading,
refetch: refetchBuyBalance,
- } = useAssetBalance(walletAddress, buyAsset.assetId, buyAsset.precision);
+ } = useAssetBalance(walletAddress, buyAsset.assetId, buyAsset.precision)
const handleSwapTokens = useCallback(() => {
- const tempSell = sellAsset;
- setSellAsset(buyAsset);
- setBuyAsset(tempSell);
- setSellAmount("");
- setSelectedRate(null);
- }, [sellAsset, buyAsset]);
+ const tempSell = sellAsset
+ setSellAsset(buyAsset)
+ setBuyAsset(tempSell)
+ setSellAmount('')
+ setSelectedRate(null)
+ }, [sellAsset, buyAsset])
const handleSellAssetSelect = useCallback(
(asset: Asset) => {
- setSellAsset(asset);
- setSelectedRate(null);
- onAssetSelect?.("sell", asset);
+ setSellAsset(asset)
+ setSelectedRate(null)
+ onAssetSelect?.('sell', asset)
},
[onAssetSelect],
- );
+ )
const handleBuyAssetSelect = useCallback(
(asset: Asset) => {
- setBuyAsset(asset);
- setSelectedRate(null);
- onAssetSelect?.("buy", asset);
+ setBuyAsset(asset)
+ setSelectedRate(null)
+ onAssetSelect?.('buy', asset)
},
[onAssetSelect],
- );
+ )
const handleExecuteSwap = useCallback(async () => {
if (!isSellAssetEvm) {
@@ -167,80 +173,75 @@ const SwapWidgetInner = ({
sellAssetId: sellAsset.assetId,
buyAssetId: buyAsset.assetId,
sellAmount,
- });
- window.open(
- `https://app.shapeshift.com/trade?${params.toString()}`,
- "_blank",
- );
- return;
+ })
+ window.open(`https://app.shapeshift.com/trade?${params.toString()}`, '_blank')
+ return
}
- const rateToUse = selectedRate ?? rates?.[0];
- if (!rateToUse || !walletClient || !walletAddress) return;
+ const rateToUse = selectedRate ?? rates?.[0]
+ if (!rateToUse || !walletClient || !walletAddress) return
if (!canExecuteDirectly) {
const params = new URLSearchParams({
sellAssetId: sellAsset.assetId,
buyAssetId: buyAsset.assetId,
sellAmount,
- });
- window.open(
- `https://app.shapeshift.com/trade?${params.toString()}`,
- "_blank",
- );
- return;
+ })
+ window.open(`https://app.shapeshift.com/trade?${params.toString()}`, '_blank')
+ return
}
- setIsExecuting(true);
+ setIsExecuting(true)
try {
- const requiredChainId = getEvmChainIdNumber(sellAsset.chainId);
- const client = walletClient as WalletClient;
+ const requiredChainId = getEvmChainIdNumber(sellAsset.chainId)
+ const client = walletClient as WalletClient
- const currentChainId = await client.getChainId();
+ const currentChainId = await client.getChainId()
if (currentChainId !== requiredChainId) {
- await client.switchChain({ id: requiredChainId });
+ await client.switchChain({ id: requiredChainId })
+ }
+
+ if (!sellAmountBaseUnit) {
+ throw new Error('Sell amount is required')
}
- const slippageDecimal = (parseFloat(slippage) / 100).toString();
+ const slippageDecimal = (parseFloat(slippage) / 100).toString()
const quoteResponse = await apiClient.getQuote({
sellAssetId: sellAsset.assetId,
buyAssetId: buyAsset.assetId,
- sellAmountCryptoBaseUnit: sellAmountBaseUnit!,
+ sellAmountCryptoBaseUnit: sellAmountBaseUnit,
sendAddress: walletAddress,
receiveAddress: walletAddress,
swapperName: rateToUse.swapperName,
slippageTolerancePercentageDecimal: slippageDecimal,
- });
+ })
const chain = {
id: requiredChainId,
- name: "Chain",
- nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
+ name: 'Chain',
+ nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: [] } },
- };
+ }
if (quoteResponse.approval?.isRequired) {
- const sellAssetAddress = sellAsset.assetId.split("/")[1]?.split(":")[1];
+ const sellAssetAddress = sellAsset.assetId.split('/')[1]?.split(':')[1]
if (sellAssetAddress) {
const approvalData = encodeFunctionData({
abi: [
{
- name: "approve",
- type: "function",
+ name: 'approve',
+ type: 'function',
inputs: [
- { name: "spender", type: "address" },
- { name: "amount", type: "uint256" },
+ { name: 'spender', type: 'address' },
+ { name: 'amount', type: 'uint256' },
],
- outputs: [{ name: "", type: "bool" }],
+ outputs: [{ name: '', type: 'bool' }],
},
],
- functionName: "approve",
- args: [
- quoteResponse.approval.spender as `0x${string}`,
- BigInt(sellAmountBaseUnit!),
- ],
- });
+ functionName: 'approve',
+ args: [quoteResponse.approval.spender as `0x${string}`, BigInt(sellAmountBaseUnit)],
+ })
await client.sendTransaction({
to: sellAssetAddress as `0x${string}`,
@@ -248,12 +249,12 @@ const SwapWidgetInner = ({
value: BigInt(0),
chain,
account: walletAddress as `0x${string}`,
- });
+ })
}
}
- const outerStep = quoteResponse.steps?.[0];
- const innerStep = quoteResponse.quote?.steps?.[0];
+ const outerStep = quoteResponse.steps?.[0]
+ const innerStep = quoteResponse.quote?.steps?.[0]
const transactionData =
quoteResponse.transactionData ??
@@ -262,25 +263,23 @@ const SwapWidgetInner = ({
outerStep?.butterSwapTransactionMetadata ??
innerStep?.transactionData ??
innerStep?.relayTransactionMetadata ??
- innerStep?.butterSwapTransactionMetadata;
+ innerStep?.butterSwapTransactionMetadata
if (!transactionData) {
throw new Error(
- `No transaction data returned. Response keys: ${Object.keys(
- quoteResponse,
- ).join(", ")}`,
- );
+ `No transaction data returned. Response keys: ${Object.keys(quoteResponse).join(', ')}`,
+ )
}
- const to = transactionData.to as string;
- const data = transactionData.data as string;
- const value = transactionData.value ?? "0";
- const gasLimit = transactionData.gasLimit as string | undefined;
+ const to = transactionData.to as string
+ const data = transactionData.data as string
+ const value = transactionData.value ?? '0'
+ const gasLimit = transactionData.gasLimit as string | undefined
setTxStatus({
- status: "pending",
- message: "Waiting for confirmation...",
- });
+ status: 'pending',
+ message: 'Waiting for confirmation...',
+ })
const txHash = await client.sendTransaction({
to: to as `0x${string}`,
@@ -289,25 +288,24 @@ const SwapWidgetInner = ({
gas: gasLimit ? BigInt(gasLimit) : undefined,
chain,
account: walletAddress as `0x${string}`,
- });
+ })
- setTxStatus({ status: "success", txHash, message: "Swap successful!" });
- onSwapSuccess?.(txHash);
+ setTxStatus({ status: 'success', txHash, message: 'Swap successful!' })
+ onSwapSuccess?.(txHash)
- setSellAmount("");
- setSelectedRate(null);
+ setSellAmount('')
+ setSelectedRate(null)
setTimeout(() => {
- refetchSellBalance?.();
- refetchBuyBalance?.();
- }, 3000);
+ refetchSellBalance?.()
+ refetchBuyBalance?.()
+ }, 3000)
} catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Transaction failed";
- setTxStatus({ status: "error", message: errorMessage });
- onSwapError?.(error as Error);
+ const errorMessage = error instanceof Error ? error.message : 'Transaction failed'
+ setTxStatus({ status: 'error', message: errorMessage })
+ onSwapError?.(error as Error)
} finally {
- setIsExecuting(false);
+ setIsExecuting(false)
}
}, [
selectedRate,
@@ -326,26 +324,26 @@ const SwapWidgetInner = ({
onSwapError,
refetchSellBalance,
refetchBuyBalance,
- ]);
+ ])
const handleButtonClick = useCallback(() => {
if (!walletClient && canExecuteDirectly && onConnectWallet) {
- onConnectWallet();
- return;
+ onConnectWallet()
+ return
}
- handleExecuteSwap();
- }, [walletClient, canExecuteDirectly, onConnectWallet, handleExecuteSwap]);
+ handleExecuteSwap()
+ }, [walletClient, canExecuteDirectly, onConnectWallet, handleExecuteSwap])
const buttonText = useMemo(() => {
- if (!isSellAssetEvm) return "Proceed on ShapeShift";
- if (!sellAmount) return "Enter an amount";
- if (!walletClient && canExecuteDirectly) return "Connect Wallet";
- if (isLoadingRates) return "Finding rates...";
- if (ratesError) return "No routes available";
- if (!rates?.length) return "No routes found";
- if (isExecuting) return "Executing...";
- if (!canExecuteDirectly) return "Proceed on ShapeShift";
- return "Swap";
+ if (!isSellAssetEvm) return 'Proceed on ShapeShift'
+ if (!sellAmount) return 'Enter an amount'
+ if (!walletClient && canExecuteDirectly) return 'Connect Wallet'
+ if (isLoadingRates) return 'Finding rates...'
+ if (ratesError) return 'No routes available'
+ if (!rates?.length) return 'No routes found'
+ if (isExecuting) return 'Executing...'
+ if (!canExecuteDirectly) return 'Proceed on ShapeShift'
+ return 'Swap'
}, [
walletClient,
canExecuteDirectly,
@@ -355,402 +353,360 @@ const SwapWidgetInner = ({
ratesError,
rates,
isExecuting,
- ]);
+ ])
const isButtonDisabled = useMemo(() => {
- if (!isSellAssetEvm) return false; // Allow non-EVM without amount
- if (!sellAmount) return true;
- if (isLoadingRates) return true;
- if (ratesError) return true;
- if (!rates?.length) return true;
- if (isExecuting) return true;
- return false;
- }, [
- isSellAssetEvm,
- sellAmount,
- isLoadingRates,
- ratesError,
- rates,
- isExecuting,
- ]);
-
- const { data: sellChainInfo } = useChainInfo(sellAsset.chainId);
- const { data: buyChainInfo } = useChainInfo(buyAsset.chainId);
- const displayRate = selectedRate ?? rates?.[0];
- const buyAmount = displayRate?.buyAmountCryptoBaseUnit;
+ if (!isSellAssetEvm) return false // Allow non-EVM without amount
+ if (!sellAmount) return true
+ if (isLoadingRates) return true
+ if (ratesError) return true
+ if (!rates?.length) return true
+ if (isExecuting) return true
+ return false
+ }, [isSellAssetEvm, sellAmount, isLoadingRates, ratesError, rates, isExecuting])
+
+ const { data: sellChainInfo } = useChainInfo(sellAsset.chainId)
+ const { data: buyChainInfo } = useChainInfo(buyAsset.chainId)
+ const displayRate = selectedRate ?? rates?.[0]
+ const buyAmount = displayRate?.buyAmountCryptoBaseUnit
const assetIdsForPrices = useMemo(
() => [sellAsset.assetId, buyAsset.assetId],
[sellAsset.assetId, buyAsset.assetId],
- );
- const { data: marketData } = useMarketData(assetIdsForPrices);
- const sellAssetUsdPrice = marketData?.[sellAsset.assetId]?.price;
- const buyAssetUsdPrice = marketData?.[buyAsset.assetId]?.price;
+ )
+ const { data: marketData } = useMarketData(assetIdsForPrices)
+ const sellAssetUsdPrice = marketData?.[sellAsset.assetId]?.price
+ const buyAssetUsdPrice = marketData?.[buyAsset.assetId]?.price
const sellUsdValue = useMemo(() => {
- if (!sellAmountBaseUnit || !sellAssetUsdPrice) return "$0.00";
- return formatUsdValue(
- sellAmountBaseUnit,
- sellAsset.precision,
- sellAssetUsdPrice,
- );
- }, [sellAmountBaseUnit, sellAsset.precision, sellAssetUsdPrice]);
+ if (!sellAmountBaseUnit || !sellAssetUsdPrice) return '$0.00'
+ return formatUsdValue(sellAmountBaseUnit, sellAsset.precision, sellAssetUsdPrice)
+ }, [sellAmountBaseUnit, sellAsset.precision, sellAssetUsdPrice])
const buyUsdValue = useMemo(() => {
- if (!buyAmount || !buyAssetUsdPrice) return "$0.00";
- return formatUsdValue(buyAmount, buyAsset.precision, buyAssetUsdPrice);
- }, [buyAmount, buyAsset.precision, buyAssetUsdPrice]);
+ if (!buyAmount || !buyAssetUsdPrice) return '$0.00'
+ return formatUsdValue(buyAmount, buyAsset.precision, buyAssetUsdPrice)
+ }, [buyAmount, buyAsset.precision, buyAssetUsdPrice])
const widgetStyle = useMemo(() => {
- if (!themeConfig) return undefined;
- const style: Record = {};
+ if (!themeConfig) return undefined
+ const style: Record = {}
if (themeConfig.accentColor) {
- style["--ssw-accent"] = themeConfig.accentColor;
- style["--ssw-accent-light"] = `${themeConfig.accentColor}1a`;
+ style['--ssw-accent'] = themeConfig.accentColor
+ style['--ssw-accent-light'] = `${themeConfig.accentColor}1a`
}
if (themeConfig.backgroundColor) {
- style["--ssw-bg-secondary"] = themeConfig.backgroundColor;
- style["--ssw-bg-primary"] = themeConfig.backgroundColor;
+ style['--ssw-bg-secondary'] = themeConfig.backgroundColor
+ style['--ssw-bg-primary'] = themeConfig.backgroundColor
}
if (themeConfig.cardColor) {
- style["--ssw-bg-tertiary"] = themeConfig.cardColor;
- style["--ssw-bg-input"] = themeConfig.cardColor;
+ style['--ssw-bg-tertiary'] = themeConfig.cardColor
+ style['--ssw-bg-input'] = themeConfig.cardColor
}
if (themeConfig.textColor) {
- style["--ssw-text-primary"] = themeConfig.textColor;
+ style['--ssw-text-primary'] = themeConfig.textColor
}
if (themeConfig.borderRadius) {
- style["--ssw-border-radius"] = themeConfig.borderRadius;
+ style['--ssw-border-radius'] = themeConfig.borderRadius
}
- return Object.keys(style).length > 0
- ? (style as React.CSSProperties)
- : undefined;
- }, [themeConfig]);
+ return Object.keys(style).length > 0 ? (style as React.CSSProperties) : undefined
+ }, [themeConfig])
return (
-
-
Swap
-
setIsSettingsOpen(true)}
- type="button"
- title="Settings"
- >
-
+ Swap
+
+ {enableWalletConnection &&
}
+
setIsSettingsOpen(true)}
+ type='button'
+ title='Settings'
>
-
-
-
-
+
+
+
+
+
+
-
-
-
-
Sell
+
+
+
+ Sell
{walletAddress && isSellAssetEvm && (
-
- {truncateAddress(walletAddress)}
-
+ {truncateAddress(walletAddress)}
)}
-
+
{
- setSellAmount(e.target.value.replace(/[^0-9.]/g, ""));
- setSelectedRate(null);
+ onChange={e => {
+ setSellAmount(e.target.value.replace(/[^0-9.]/g, ''))
+ setSelectedRate(null)
}}
/>
setTokenModalType("sell")}
- type="button"
+ className='ssw-token-btn'
+ onClick={() => setTokenModalType('sell')}
+ type='button'
>
{sellAsset.icon ? (
-
+
) : (
-
- {sellAsset.symbol.charAt(0)}
-
+ {sellAsset.symbol.charAt(0)}
)}
-
-
{sellAsset.symbol}
-
- {sellChainInfo?.name ??
- sellAsset.networkName ??
- sellAsset.name}
+
+ {sellAsset.symbol}
+
+ {sellChainInfo?.name ?? sellAsset.networkName ?? sellAsset.name}
-
+
-
-
{sellUsdValue}
+
+ {sellUsdValue}
{walletAddress &&
(isSellBalanceLoading ? (
-
+
) : sellAssetBalance ? (
-
- Balance: {sellAssetBalance.balanceFormatted}
-
+ Balance: {sellAssetBalance.balanceFormatted}
) : null)}
-
-
+
-
-
-
Buy
-
setIsAddressModalOpen(true)}
- type="button"
- >
-
- {effectiveReceiveAddress
- ? truncateAddress(effectiveReceiveAddress, 4)
- : "Enter address"}
+
+
+
Buy
+ {defaultReceiveAddress ? (
+
+
+ {truncateAddress(defaultReceiveAddress, 4)}
+
-
setIsAddressModalOpen(true)}
+ type='button'
>
-
-
-
-
+
+ {effectiveReceiveAddress
+ ? truncateAddress(effectiveReceiveAddress, 4)
+ : 'Enter address'}
+
+
+
+
+
+
+ )}
-
+
setTokenModalType("buy")}
- type="button"
+ className='ssw-token-btn'
+ onClick={() => setTokenModalType('buy')}
+ type='button'
>
{buyAsset.icon ? (
-
+
) : (
-
- {buyAsset.symbol.charAt(0)}
-
+ {buyAsset.symbol.charAt(0)}
)}
-
-
{buyAsset.symbol}
-
+
+ {buyAsset.symbol}
+
{buyChainInfo?.name ?? buyAsset.networkName ?? buyAsset.name}
-
+
-
-
{buyUsdValue}
+
+ {buyUsdValue}
{walletAddress &&
(isBuyBalanceLoading ? (
-
+
) : buyAssetBalance ? (
-
- Balance: {buyAssetBalance.balanceFormatted}
-
+ Balance: {buyAssetBalance.balanceFormatted}
) : null)}
- {sellAmountBaseUnit &&
- sellAmountBaseUnit !== "0" &&
- (rates?.length || isLoadingRates) && (
-
-
-
- )}
+ {sellAmountBaseUnit && sellAmountBaseUnit !== '0' && (rates?.length || isLoadingRates) && (
+
+
+
+ )}
{buttonText}
{txStatus && (
-
- {txStatus.status === "pending" && (
+
+ {txStatus.status === 'pending' && (
-
-
+
+
)}
- {txStatus.status === "success" && (
+ {txStatus.status === 'success' && (
-
+
)}
- {txStatus.status === "error" && (
+ {txStatus.status === 'error' && (
-
-
+
+
)}
-
-
{txStatus.message}
+
-
setTxStatus(null)}
- type="button"
- >
+ setTxStatus(null)} type='button'>
-
+
)}
{showPoweredBy && (
-
- Powered by{" "}
+
+ Powered by{' '}
-
-
+
+
ShapeShift
@@ -760,13 +716,10 @@ const SwapWidgetInner = ({
setTokenModalType(null)}
- onSelect={
- tokenModalType === "sell"
- ? handleSellAssetSelect
- : handleBuyAssetSelect
- }
+ onSelect={tokenModalType === 'sell' ? handleSellAssetSelect : handleBuyAssetSelect}
disabledAssetIds={disabledAssetIds}
disabledChainIds={disabledChainIds}
+ allowedChainIds={allowedChainIds}
walletAddress={walletAddress}
/>
@@ -782,23 +735,57 @@ const SwapWidgetInner = ({
onClose={() => setIsAddressModalOpen(false)}
chainId={buyAsset.chainId}
chainName={buyChainInfo?.name ?? buyAsset.networkName ?? buyAsset.name}
- currentAddress={customReceiveAddress || walletAddress || ""}
+ currentAddress={customReceiveAddress || walletAddress || ''}
onAddressChange={setCustomReceiveAddress}
walletAddress={walletAddress}
/>
- );
-};
+ )
+}
-export const SwapWidget = (props: SwapWidgetProps) => {
+const SwapWidgetWithExternalWallet = (props: SwapWidgetProps) => {
const apiClient = useMemo(
() => createApiClient({ baseUrl: props.apiBaseUrl, apiKey: props.apiKey }),
[props.apiBaseUrl, props.apiKey],
- );
+ )
return (
-
+
- );
-};
+ )
+}
+
+const SwapWidgetWithInternalWallet = (
+ props: SwapWidgetProps & { walletConnectProjectId: string },
+) => {
+ const apiClient = useMemo(
+ () => createApiClient({ baseUrl: props.apiBaseUrl, apiKey: props.apiKey }),
+ [props.apiBaseUrl, props.apiKey],
+ )
+
+ const themeMode = typeof props.theme === 'string' ? props.theme : props.theme?.mode ?? 'dark'
+
+ return (
+
+ {walletClient => (
+
+
+
+ )}
+
+ )
+}
+
+export const SwapWidget = (props: SwapWidgetProps) => {
+ if (props.enableWalletConnection && props.walletConnectProjectId) {
+ return (
+
+ )
+ }
+
+ return
+}
diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.css b/packages/swap-widget-poc/src/components/TokenSelectModal.css
index 5f318a44fe3..63fddab199d 100644
--- a/packages/swap-widget-poc/src/components/TokenSelectModal.css
+++ b/packages/swap-widget-poc/src/components/TokenSelectModal.css
@@ -109,6 +109,7 @@
padding: 0 8px 16px;
scrollbar-width: thin;
scrollbar-color: var(--ssw-border) transparent;
+ min-height: 300px;
}
.ssw-chain-list::-webkit-scrollbar {
diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx
index eccb1b23413..9b0a5a95ed6 100644
--- a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx
+++ b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx
@@ -1,56 +1,60 @@
-import { useState, useMemo, useCallback, useEffect } from "react";
-import { Virtuoso } from "react-virtuoso";
-import type { Asset, AssetId, ChainId } from "../types";
-import { useAssets, useChains, type ChainInfo } from "../hooks/useAssets";
-import { useEvmBalances } from "../hooks/useBalances";
-import { useAllMarketData } from "../hooks/useMarketData";
-import "./TokenSelectModal.css";
+import './TokenSelectModal.css'
-const VISIBLE_BUFFER = 10;
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { Virtuoso } from 'react-virtuoso'
+
+import type { ChainInfo } from '../hooks/useAssets'
+import { useAssets, useChains } from '../hooks/useAssets'
+import { useEvmBalances } from '../hooks/useBalances'
+import { useAllMarketData } from '../hooks/useMarketData'
+import type { Asset, AssetId, ChainId } from '../types'
+
+const VISIBLE_BUFFER = 10
const useLockBodyScroll = (isLocked: boolean) => {
useEffect(() => {
- if (!isLocked) return;
- const originalOverflow = document.body.style.overflow;
- document.body.style.overflow = "hidden";
+ if (!isLocked) return
+ const originalOverflow = document.body.style.overflow
+ document.body.style.overflow = 'hidden'
return () => {
- document.body.style.overflow = originalOverflow;
- };
- }, [isLocked]);
-};
+ document.body.style.overflow = originalOverflow
+ }
+ }, [isLocked])
+}
type TokenSelectModalProps = {
- isOpen: boolean;
- onClose: () => void;
- onSelect: (asset: Asset) => void;
- disabledAssetIds?: string[];
- disabledChainIds?: ChainId[];
- walletAddress?: string;
-};
+ isOpen: boolean
+ onClose: () => void
+ onSelect: (asset: Asset) => void
+ disabledAssetIds?: string[]
+ disabledChainIds?: ChainId[]
+ allowedChainIds?: ChainId[]
+ walletAddress?: string
+}
const isNativeAsset = (assetId: string): boolean => {
- return assetId.includes("/slip44:") || assetId.endsWith("/native");
-};
+ return assetId.includes('/slip44:') || assetId.endsWith('/native')
+}
const scoreAsset = (asset: Asset, query: string): number => {
- const lowerQuery = query.toLowerCase();
- const symbolLower = asset.symbol.toLowerCase();
- const nameLower = asset.name.toLowerCase();
+ const lowerQuery = query.toLowerCase()
+ const symbolLower = asset.symbol.toLowerCase()
+ const nameLower = asset.name.toLowerCase()
- let score = 0;
+ let score = 0
- if (symbolLower === lowerQuery) score += 1000;
- else if (symbolLower.startsWith(lowerQuery)) score += 500;
- else if (symbolLower.includes(lowerQuery)) score += 100;
+ if (symbolLower === lowerQuery) score += 1000
+ else if (symbolLower.startsWith(lowerQuery)) score += 500
+ else if (symbolLower.includes(lowerQuery)) score += 100
- if (nameLower === lowerQuery) score += 800;
- else if (nameLower.startsWith(lowerQuery)) score += 300;
- else if (nameLower.includes(lowerQuery)) score += 50;
+ if (nameLower === lowerQuery) score += 800
+ else if (nameLower.startsWith(lowerQuery)) score += 300
+ else if (nameLower.includes(lowerQuery)) score += 50
- if (isNativeAsset(asset.assetId)) score += 200;
+ if (isNativeAsset(asset.assetId)) score += 200
- return score;
-};
+ return score
+}
export const TokenSelectModal = ({
isOpen,
@@ -58,312 +62,301 @@ export const TokenSelectModal = ({
onSelect,
disabledAssetIds = [],
disabledChainIds = [],
+ allowedChainIds,
walletAddress,
}: TokenSelectModalProps) => {
- useLockBodyScroll(isOpen);
- const [searchQuery, setSearchQuery] = useState("");
- const [chainSearchQuery, setChainSearchQuery] = useState("");
- const [selectedChainId, setSelectedChainId] = useState
(null);
+ useLockBodyScroll(isOpen)
+ const [searchQuery, setSearchQuery] = useState('')
+ const [chainSearchQuery, setChainSearchQuery] = useState('')
+ const [selectedChainId, setSelectedChainId] = useState(null)
const [visibleRange, setVisibleRange] = useState({
startIndex: 0,
endIndex: 20,
- });
+ })
- const { data: allAssets, isLoading: isLoadingAssets } = useAssets();
- const { data: chains, isLoading: isLoadingChains } = useChains();
+ const { data: allAssets, isLoading: isLoadingAssets } = useAssets()
+ const { data: chains, isLoading: isLoadingChains } = useChains()
const chainInfoMap = useMemo(() => {
- const map = new Map();
+ const map = new Map()
for (const chain of chains) {
- map.set(chain.chainId, chain);
+ map.set(chain.chainId, chain)
}
- return map;
- }, [chains]);
+ return map
+ }, [chains])
const filteredChains = useMemo(() => {
- const enabledChains = chains.filter(
- (chain) => !disabledChainIds.includes(chain.chainId),
- );
+ let enabledChains = chains.filter(chain => !disabledChainIds.includes(chain.chainId))
+
+ if (allowedChainIds && allowedChainIds.length > 0) {
+ enabledChains = enabledChains.filter(chain => allowedChainIds.includes(chain.chainId))
+ }
- if (!chainSearchQuery.trim()) return enabledChains;
+ if (!chainSearchQuery.trim()) return enabledChains
- const lowerQuery = chainSearchQuery.toLowerCase();
- return enabledChains.filter((chain) =>
- chain.name.toLowerCase().includes(lowerQuery),
- );
- }, [chains, chainSearchQuery, disabledChainIds]);
+ const lowerQuery = chainSearchQuery.toLowerCase()
+ return enabledChains.filter(chain => chain.name.toLowerCase().includes(lowerQuery))
+ }, [chains, chainSearchQuery, disabledChainIds, allowedChainIds])
const filteredAssets = useMemo(() => {
let assets = allAssets.filter(
- (asset) =>
- !disabledAssetIds.includes(asset.assetId) &&
- !disabledChainIds.includes(asset.chainId),
- );
+ asset =>
+ !disabledAssetIds.includes(asset.assetId) && !disabledChainIds.includes(asset.chainId),
+ )
+
+ if (allowedChainIds && allowedChainIds.length > 0) {
+ assets = assets.filter(asset => allowedChainIds.includes(asset.chainId))
+ }
if (selectedChainId) {
- assets = assets.filter((asset) => asset.chainId === selectedChainId);
+ assets = assets.filter(asset => asset.chainId === selectedChainId)
}
if (!searchQuery.trim()) {
- const natives = assets.filter((a) => isNativeAsset(a.assetId));
+ const natives = assets.filter(a => isNativeAsset(a.assetId))
const others = assets
- .filter((a) => !isNativeAsset(a.assetId))
- .slice(0, Math.max(50 - natives.length, 30));
- return [...natives, ...others];
+ .filter(a => !isNativeAsset(a.assetId))
+ .slice(0, Math.max(50 - natives.length, 30))
+ return [...natives, ...others]
}
- const lowerQuery = searchQuery.toLowerCase();
+ const lowerQuery = searchQuery.toLowerCase()
return assets
.filter(
- (asset) =>
+ asset =>
asset.symbol.toLowerCase().includes(lowerQuery) ||
asset.name.toLowerCase().includes(lowerQuery) ||
asset.assetId.toLowerCase().includes(lowerQuery),
)
- .map((asset) => ({ asset, score: scoreAsset(asset, searchQuery) }))
+ .map(asset => ({ asset, score: scoreAsset(asset, searchQuery) }))
.sort((a, b) => b.score - a.score)
.slice(0, 100)
- .map((item) => item.asset);
- }, [
- allAssets,
- selectedChainId,
- searchQuery,
- disabledAssetIds,
- disabledChainIds,
- ]);
+ .map(item => item.asset)
+ }, [allAssets, selectedChainId, searchQuery, disabledAssetIds, disabledChainIds, allowedChainIds])
const visibleAssets = useMemo(() => {
- const start = Math.max(0, visibleRange.startIndex - VISIBLE_BUFFER);
- const end = Math.min(
- filteredAssets.length,
- visibleRange.endIndex + VISIBLE_BUFFER,
- );
- return filteredAssets.slice(start, end);
- }, [filteredAssets, visibleRange]);
+ const start = Math.max(0, visibleRange.startIndex - VISIBLE_BUFFER)
+ const end = Math.min(filteredAssets.length, visibleRange.endIndex + VISIBLE_BUFFER)
+ return filteredAssets.slice(start, end)
+ }, [filteredAssets, visibleRange])
const assetPrecisions = useMemo(() => {
- const precisions: Record = {};
+ const precisions: Record = {}
for (const asset of visibleAssets) {
- precisions[asset.assetId] = asset.precision;
+ precisions[asset.assetId] = asset.precision
}
- return precisions;
- }, [visibleAssets]);
+ return precisions
+ }, [visibleAssets])
- const assetIds = useMemo(
- () => visibleAssets.map((a) => a.assetId),
- [visibleAssets],
- );
+ const assetIds = useMemo(() => visibleAssets.map(a => a.assetId), [visibleAssets])
const { data: balances, loadingAssetIds } = useEvmBalances(
walletAddress,
assetIds,
assetPrecisions,
- );
+ )
- const { data: marketData } = useAllMarketData();
+ const { data: marketData } = useAllMarketData()
const handleAssetSelect = useCallback(
(asset: Asset) => {
- onSelect(asset);
- onClose();
- setSearchQuery("");
- setSelectedChainId(null);
+ onSelect(asset)
+ onClose()
+ setSearchQuery('')
+ setSelectedChainId(null)
},
[onSelect, onClose],
- );
+ )
const handleChainSelect = useCallback((chainId: ChainId | null) => {
- setSelectedChainId(chainId);
- }, []);
+ setSelectedChainId(chainId)
+ }, [])
const handleBackdropClick = useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
- onClose();
+ onClose()
}
},
[onClose],
- );
+ )
- if (!isOpen) return null;
+ if (!isOpen) return null
- const isLoading = isLoadingAssets || isLoadingChains;
+ const isLoading = isLoadingAssets || isLoadingChains
return (
-
-
-
-
Select Token
-
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
+ e.key === 'Escape' && onClose()}
+ role='dialog'
+ aria-modal='true'
+ aria-labelledby='token-modal-title'
+ >
+
+
-
-
-
+
+
+
-
+
handleChainSelect(null)}
- type="button"
+ type='button'
>
-
+
🔗
-
All Chains
+
All Chains
- {filteredChains.map((chain) => (
+ {filteredChains.map(chain => (
handleChainSelect(chain.chainId)}
- type="button"
+ type='button'
>
{chain.icon ? (
-
+
) : (
{chain.name.charAt(0)}
)}
- {chain.name}
+ {chain.name}
))}
-
-
+
+
-
+
{isLoading ? (
-
-
+
) : filteredAssets.length === 0 ? (
-
No tokens found
+
No tokens found
) : (
{
- const chainInfo = chainInfoMap.get(asset.chainId);
- const balance = balances?.[asset.assetId];
+ const chainInfo = chainInfoMap.get(asset.chainId)
+ const balance = balances?.[asset.assetId]
return (
handleAssetSelect(asset)}
- type="button"
+ type='button'
>
-
+
{asset.icon ? (
-
+
) : (
- {asset.symbol?.charAt(0) ?? "?"}
+ {asset.symbol?.charAt(0) ?? '?'}
)}
{chainInfo?.icon && (
)}
-
-
- {asset.symbol}
-
-
+
+ {asset.symbol}
+
{chainInfo?.name ?? asset.networkName ?? asset.name}
-
+
{walletAddress &&
(loadingAssetIds.has(asset.assetId) ? (
-
- ) : balance && balance.balance !== "0" ? (
+
+ ) : balance && balance.balance !== '0' ? (
<>
{marketData?.[asset.assetId]?.price && (
-
+
$
{(
- (Number(balance.balance) /
- Math.pow(10, asset.precision)) *
+ (Number(balance.balance) / Math.pow(10, asset.precision)) *
Number(marketData[asset.assetId].price)
).toLocaleString(undefined, {
minimumFractionDigits: 2,
@@ -371,14 +364,14 @@ export const TokenSelectModal = ({
})}
)}
-
+
{balance.balanceFormatted}
>
) : null)}
- );
+ )
}}
/>
)}
@@ -387,5 +380,5 @@ export const TokenSelectModal = ({
- );
-};
+ )
+}
diff --git a/packages/swap-widget-poc/src/components/WalletProvider.tsx b/packages/swap-widget-poc/src/components/WalletProvider.tsx
new file mode 100644
index 00000000000..3989d2fe3bd
--- /dev/null
+++ b/packages/swap-widget-poc/src/components/WalletProvider.tsx
@@ -0,0 +1,125 @@
+import {
+ ConnectButton,
+ darkTheme,
+ getDefaultConfig,
+ lightTheme,
+ RainbowKitProvider,
+} from '@rainbow-me/rainbowkit'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import type { ReactNode } from 'react'
+import { useMemo } from 'react'
+import { useWalletClient, WagmiProvider } from 'wagmi'
+import { arbitrum, avalanche, base, bsc, gnosis, mainnet, optimism, polygon } from 'wagmi/chains'
+
+import type { ThemeMode } from '../types'
+
+const queryClient = new QueryClient()
+
+type InternalWalletProviderProps = {
+ projectId: string
+ children: (walletClient: unknown) => ReactNode
+ themeMode: ThemeMode
+}
+
+const InternalWalletContent = ({
+ children,
+}: {
+ children: (walletClient: unknown) => ReactNode
+}) => {
+ const { data: walletClient } = useWalletClient()
+ return <>{children(walletClient)}>
+}
+
+export const InternalWalletProvider = ({
+ projectId,
+ children,
+ themeMode,
+}: InternalWalletProviderProps) => {
+ const config = useMemo(
+ () =>
+ getDefaultConfig({
+ appName: 'ShapeShift Swap Widget',
+ projectId,
+ chains: [mainnet, polygon, arbitrum, optimism, base, avalanche, bsc, gnosis],
+ ssr: false,
+ }),
+ [projectId],
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+export const ConnectWalletButton = () => {
+ return (
+
+ {({ account, chain, openAccountModal, openChainModal, openConnectModal, mounted }) => {
+ const ready = mounted
+ const connected = ready && account && chain
+
+ return (
+
+ {(() => {
+ if (!connected) {
+ return (
+
+
+
+
+
+ Connect
+
+ )
+ }
+
+ if (chain.unsupported) {
+ return (
+
+ Wrong network
+
+ )
+ }
+
+ return (
+
+ {account.displayName}
+
+ )
+ })()}
+
+ )
+ }}
+
+ )
+}
diff --git a/packages/swap-widget-poc/src/constants/swappers.ts b/packages/swap-widget-poc/src/constants/swappers.ts
index 4515876b336..8ea2ce2f78d 100644
--- a/packages/swap-widget-poc/src/constants/swappers.ts
+++ b/packages/swap-widget-poc/src/constants/swappers.ts
@@ -1,52 +1,49 @@
-import type { SwapperName } from "../types";
+import type { SwapperName } from '../types'
export const SWAPPER_ICONS: Record = {
THORChain:
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/thorchain-icon.png",
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/thorchain-icon.png',
MAYAChain:
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/maya_logo.png",
- "CoW Swap":
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/cow-icon.png",
- "0x": "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/0x-icon.png",
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/maya_logo.png',
+ 'CoW Swap':
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/cow-icon.png',
+ '0x': 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/0x-icon.png',
Portals:
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/portals-icon.png",
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/portals-icon.png',
Chainflip:
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/chainflip-icon.png",
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/chainflip-icon.png',
Relay:
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/relay-icon.svg",
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/relay-icon.svg',
Bebop:
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/bebop-icon.png",
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/bebop-icon.png',
Jupiter:
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/jupiter-icon.svg",
- "1inch":
- "https://raw.githubusercontent.com/trustwallet/assets/master/dapps/1inch.exchange.png",
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/jupiter-icon.svg',
+ '1inch': 'https://raw.githubusercontent.com/trustwallet/assets/master/dapps/1inch.exchange.png',
ButterSwap:
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/butterswap.png",
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/butterswap.png',
ArbitrumBridge:
- "https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/arbitrum-bridge-icon.png",
-};
+ 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/arbitrum-bridge-icon.png',
+}
export const SWAPPER_COLORS: Partial> = {
- THORChain: "#00CCFF",
- MAYAChain: "#4169E1",
- "CoW Swap": "#012d73",
- "0x": "#000000",
- Portals: "#8B5CF6",
- Chainflip: "#FF4081",
- Relay: "#6366F1",
- Bebop: "#E91E63",
- Jupiter: "#C4A962",
- "1inch": "#1B314F",
- ButterSwap: "#FFD700",
- ArbitrumBridge: "#28A0F0",
-};
+ THORChain: '#00CCFF',
+ MAYAChain: '#4169E1',
+ 'CoW Swap': '#012d73',
+ '0x': '#000000',
+ Portals: '#8B5CF6',
+ Chainflip: '#FF4081',
+ Relay: '#6366F1',
+ Bebop: '#E91E63',
+ Jupiter: '#C4A962',
+ '1inch': '#1B314F',
+ ButterSwap: '#FFD700',
+ ArbitrumBridge: '#28A0F0',
+}
-export const getSwapperIcon = (
- swapperName: SwapperName,
-): string | undefined => {
- return SWAPPER_ICONS[swapperName];
-};
+export const getSwapperIcon = (swapperName: SwapperName): string | undefined => {
+ return SWAPPER_ICONS[swapperName]
+}
export const getSwapperColor = (swapperName: SwapperName): string => {
- return SWAPPER_COLORS[swapperName] ?? "#6366F1";
-};
+ return SWAPPER_COLORS[swapperName] ?? '#6366F1'
+}
diff --git a/packages/swap-widget-poc/src/demo/App.tsx b/packages/swap-widget-poc/src/demo/App.tsx
index f974987a851..61182f4433a 100644
--- a/packages/swap-widget-poc/src/demo/App.tsx
+++ b/packages/swap-widget-poc/src/demo/App.tsx
@@ -18,7 +18,7 @@ import type { ThemeConfig } from '../types'
const config = getDefaultConfig({
appName: 'ShapeShift Swap Widget',
- projectId: 'demo-project-id',
+ projectId: 'f58c0242def84c3b9befe9b1e6086bbd',
chains: [mainnet, polygon, arbitrum, optimism, base],
ssr: false,
})
@@ -203,7 +203,7 @@ const DemoContent = () => {
Customize Widget
-
Presets
+
Presets
{THEME_PRESETS.map(preset => {
const previewColors = theme === 'dark' ? preset.dark : preset.light
@@ -236,7 +236,7 @@ const DemoContent = () => {
-
Theme
+
Theme
{
-
Background Color
+
Background Color
{
-
Card Color
+
Card Color
{
-
Accent Color
+
Accent Color
{
-
Connection
+
Connection
{isConnected ? (
<>
@@ -411,6 +411,8 @@ const DemoContent = () => {
onSwapSuccess={handleSwapSuccess}
onSwapError={handleSwapError}
showPoweredBy={true}
+ enableWalletConnection={true}
+ defaultReceiveAddress={'0x1234567890123456789012345678901234567890'}
/>
diff --git a/packages/swap-widget-poc/src/demo/main.tsx b/packages/swap-widget-poc/src/demo/main.tsx
index a49a4804fb1..f9cd0e181b8 100644
--- a/packages/swap-widget-poc/src/demo/main.tsx
+++ b/packages/swap-widget-poc/src/demo/main.tsx
@@ -1,7 +1,8 @@
-import React from "react";
-import ReactDOM from "react-dom/client";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { App } from "./App";
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+
+import { App } from './App'
const queryClient = new QueryClient({
defaultOptions: {
@@ -10,12 +11,15 @@ const queryClient = new QueryClient({
refetchOnWindowFocus: false,
},
},
-});
+})
+
+const rootElement = document.getElementById('root')
+if (!rootElement) throw new Error('Root element not found')
-ReactDOM.createRoot(document.getElementById("root")!).render(
+ReactDOM.createRoot(rootElement).render(
,
-);
+)
diff --git a/packages/swap-widget-poc/src/hooks/useAssets.ts b/packages/swap-widget-poc/src/hooks/useAssets.ts
index 2682ca3d3d1..88053baf3f9 100644
--- a/packages/swap-widget-poc/src/hooks/useAssets.ts
+++ b/packages/swap-widget-poc/src/hooks/useAssets.ts
@@ -1,194 +1,176 @@
-import { useQuery } from "@tanstack/react-query";
-import type { Asset, AssetId, ChainId } from "../types";
+import { useQuery } from '@tanstack/react-query'
-const SHAPESHIFT_ASSET_CDN = "https://app.shapeshift.com";
-const ASSET_QUERY_STALE_TIME = 5 * 60 * 1000;
+import type { Asset, AssetId, ChainId } from '../types'
+
+const SHAPESHIFT_ASSET_CDN = 'https://app.shapeshift.com'
+const ASSET_QUERY_STALE_TIME = 5 * 60 * 1000
type AssetManifest = {
- assetData: string;
- relatedAssetIndex: string;
-};
+ assetData: string
+ relatedAssetIndex: string
+}
type RawAssetData = {
- byId: Record
;
- ids: AssetId[];
-};
+ byId: Record
+ ids: AssetId[]
+}
const fetchAssetManifest = async (): Promise => {
- const response = await fetch(
- `${SHAPESHIFT_ASSET_CDN}/generated/asset-manifest.json`,
- );
+ const response = await fetch(`${SHAPESHIFT_ASSET_CDN}/generated/asset-manifest.json`)
if (!response.ok) {
- return { assetData: Date.now().toString(), relatedAssetIndex: "" };
+ return { assetData: Date.now().toString(), relatedAssetIndex: '' }
}
- return response.json();
-};
+ return response.json()
+}
const fetchAssetData = async (): Promise => {
- const manifest = await fetchAssetManifest();
+ const manifest = await fetchAssetManifest()
const response = await fetch(
`${SHAPESHIFT_ASSET_CDN}/generated/generatedAssetData.json?v=${manifest.assetData}`,
- );
+ )
if (!response.ok) {
- throw new Error("Failed to fetch asset data");
+ throw new Error('Failed to fetch asset data')
}
- return response.json();
-};
+ return response.json()
+}
export const useAssetData = () => {
return useQuery({
- queryKey: ["assetData"],
+ queryKey: ['assetData'],
queryFn: fetchAssetData,
staleTime: ASSET_QUERY_STALE_TIME,
gcTime: 30 * 60 * 1000,
- });
-};
+ })
+}
export const useAssets = () => {
- const { data, ...rest } = useAssetData();
+ const { data, ...rest } = useAssetData()
- const assets = data
- ? data.ids.map((id) => data.byId[id]).filter(Boolean)
- : [];
+ const assets = data ? data.ids.map(id => data.byId[id]).filter(Boolean) : []
- return { data: assets, ...rest };
-};
+ return { data: assets, ...rest }
+}
export const useAssetsById = () => {
- const { data, ...rest } = useAssetData();
- return { data: data?.byId ?? {}, ...rest };
-};
+ const { data, ...rest } = useAssetData()
+ return { data: data?.byId ?? {}, ...rest }
+}
export const useAssetById = (assetId: AssetId | undefined) => {
- const { data: assetsById, ...rest } = useAssetsById();
+ const { data: assetsById, ...rest } = useAssetsById()
return {
data: assetId ? assetsById[assetId] : undefined,
...rest,
- };
-};
+ }
+}
export type ChainInfo = {
- chainId: ChainId;
- name: string;
- icon?: string;
- color?: string;
- nativeAsset: Asset;
-};
+ chainId: ChainId
+ name: string
+ icon?: string
+ color?: string
+ nativeAsset: Asset
+}
export const useChains = () => {
- const { data: assets, ...rest } = useAssets();
+ const { data: assets, ...rest } = useAssets()
const chains = (() => {
- if (!assets.length) return [];
+ if (!assets.length) return []
- const chainMap = new Map();
+ const chainMap = new Map()
for (const asset of assets) {
- if (chainMap.has(asset.chainId)) continue;
+ if (chainMap.has(asset.chainId)) continue
- const isNativeAsset =
- asset.assetId.includes("/slip44:") || asset.assetId.endsWith("/native");
+ const isNativeAsset = asset.assetId.includes('/slip44:') || asset.assetId.endsWith('/native')
if (isNativeAsset) {
chainMap.set(asset.chainId, {
chainId: asset.chainId,
name: asset.networkName ?? asset.name,
- icon:
- (asset as Asset & { networkIcon?: string }).networkIcon ??
- asset.icon,
- color:
- (asset as Asset & { networkColor?: string }).networkColor ??
- asset.color,
+ icon: (asset as Asset & { networkIcon?: string }).networkIcon ?? asset.icon,
+ color: (asset as Asset & { networkColor?: string }).networkColor ?? asset.color,
nativeAsset: asset,
- });
+ })
}
}
- return Array.from(chainMap.values()).sort((a, b) =>
- a.name.localeCompare(b.name),
- );
- })();
+ return Array.from(chainMap.values()).sort((a, b) => a.name.localeCompare(b.name))
+ })()
- return { data: chains, ...rest };
-};
+ return { data: chains, ...rest }
+}
export const useChainInfo = (chainId: ChainId | undefined) => {
- const { data: chains, ...rest } = useChains();
- const chainInfo = chainId
- ? chains.find((c) => c.chainId === chainId)
- : undefined;
- return { data: chainInfo, ...rest };
-};
+ const { data: chains, ...rest } = useChains()
+ const chainInfo = chainId ? chains.find(c => c.chainId === chainId) : undefined
+ return { data: chainInfo, ...rest }
+}
export const useAssetsByChainId = (chainId: ChainId | undefined) => {
- const { data: assets, ...rest } = useAssets();
+ const { data: assets, ...rest } = useAssets()
- const filteredAssets = chainId
- ? assets.filter((asset) => asset.chainId === chainId)
- : assets;
+ const filteredAssets = chainId ? assets.filter(asset => asset.chainId === chainId) : assets
- return { data: filteredAssets, ...rest };
-};
+ return { data: filteredAssets, ...rest }
+}
const isNativeAsset = (assetId: string): boolean => {
- return assetId.includes("/slip44:") || assetId.endsWith("/native");
-};
+ return assetId.includes('/slip44:') || assetId.endsWith('/native')
+}
const scoreAsset = (asset: Asset, query: string): number => {
- const lowerQuery = query.toLowerCase();
- const symbolLower = asset.symbol.toLowerCase();
- const nameLower = asset.name.toLowerCase();
+ const lowerQuery = query.toLowerCase()
+ const symbolLower = asset.symbol.toLowerCase()
+ const nameLower = asset.name.toLowerCase()
- let score = 0;
+ let score = 0
- if (symbolLower === lowerQuery) score += 1000;
- else if (symbolLower.startsWith(lowerQuery)) score += 500;
- else if (symbolLower.includes(lowerQuery)) score += 100;
+ if (symbolLower === lowerQuery) score += 1000
+ else if (symbolLower.startsWith(lowerQuery)) score += 500
+ else if (symbolLower.includes(lowerQuery)) score += 100
- if (nameLower === lowerQuery) score += 800;
- else if (nameLower.startsWith(lowerQuery)) score += 300;
- else if (nameLower.includes(lowerQuery)) score += 50;
+ if (nameLower === lowerQuery) score += 800
+ else if (nameLower.startsWith(lowerQuery)) score += 300
+ else if (nameLower.includes(lowerQuery)) score += 50
- if (isNativeAsset(asset.assetId)) score += 200;
+ if (isNativeAsset(asset.assetId)) score += 200
- return score;
-};
+ return score
+}
export const useAssetSearch = (query: string, chainId?: ChainId) => {
- const { data: assets, ...rest } = useAssets();
+ const { data: assets, ...rest } = useAssets()
const searchResults = (() => {
- let filtered = chainId
- ? assets.filter((a) => a.chainId === chainId)
- : assets;
+ let filtered = chainId ? assets.filter(a => a.chainId === chainId) : assets
if (!query.trim()) {
- const natives = filtered.filter((a) => isNativeAsset(a.assetId));
- const others = filtered
- .filter((a) => !isNativeAsset(a.assetId))
- .slice(0, 50 - natives.length);
- return [...natives, ...others];
+ const natives = filtered.filter(a => isNativeAsset(a.assetId))
+ const others = filtered.filter(a => !isNativeAsset(a.assetId)).slice(0, 50 - natives.length)
+ return [...natives, ...others]
}
- const lowerQuery = query.toLowerCase();
+ const lowerQuery = query.toLowerCase()
const matched = filtered
- .filter((asset) => {
+ .filter(asset => {
return (
asset.symbol.toLowerCase().includes(lowerQuery) ||
asset.name.toLowerCase().includes(lowerQuery) ||
asset.assetId.toLowerCase().includes(lowerQuery)
- );
+ )
})
- .map((asset) => ({ asset, score: scoreAsset(asset, query) }))
+ .map(asset => ({ asset, score: scoreAsset(asset, query) }))
.sort((a, b) => b.score - a.score)
.slice(0, 100)
- .map((item) => item.asset);
+ .map(item => item.asset)
- return matched;
- })();
+ return matched
+ })()
- return { data: searchResults, ...rest };
-};
+ return { data: searchResults, ...rest }
+}
diff --git a/packages/swap-widget-poc/src/hooks/useBalances.ts b/packages/swap-widget-poc/src/hooks/useBalances.ts
index 0fb49c4e86f..9a5310d05a4 100644
--- a/packages/swap-widget-poc/src/hooks/useBalances.ts
+++ b/packages/swap-widget-poc/src/hooks/useBalances.ts
@@ -1,92 +1,61 @@
-import { useBalance } from "wagmi";
-import { useMemo } from "react";
-import { useQueries } from "@tanstack/react-query";
-import { getBalance, readContract } from "@wagmi/core";
-import { useConfig } from "wagmi";
-import type { AssetId } from "../types";
-import { formatAmount, getEvmChainIdNumber } from "../types";
-import { erc20Abi } from "viem";
-
-const CONCURRENCY_LIMIT = 5;
-const DELAY_BETWEEN_BATCHES_MS = 50;
-
-class ThrottledQueue {
- private running = 0;
- private queue: Array<() => Promise> = [];
-
- async add(fn: () => Promise): Promise {
- return new Promise((resolve, reject) => {
- const run = async () => {
- this.running++;
- try {
- const result = await fn();
- resolve(result);
- } catch (error) {
- reject(error);
- } finally {
- this.running--;
- await new Promise((r) => setTimeout(r, DELAY_BETWEEN_BATCHES_MS));
- this.processQueue();
- }
- };
+import { useQueries } from '@tanstack/react-query'
+import { getBalance, readContract } from '@wagmi/core'
+import PQueue from 'p-queue'
+import { useMemo } from 'react'
+import { erc20Abi } from 'viem'
+import { useBalance, useConfig } from 'wagmi'
- if (this.running < CONCURRENCY_LIMIT) {
- run();
- } else {
- this.queue.push(run);
- }
- });
- }
+import type { AssetId } from '../types'
+import { formatAmount, getEvmChainIdNumber } from '../types'
- private processQueue() {
- if (this.queue.length > 0 && this.running < CONCURRENCY_LIMIT) {
- const next = this.queue.shift();
- next?.();
- }
- }
-}
+const CONCURRENCY_LIMIT = 5
+const DELAY_BETWEEN_BATCHES_MS = 50
-const balanceQueue = new ThrottledQueue();
+const balanceQueue = new PQueue({
+ concurrency: CONCURRENCY_LIMIT,
+ interval: DELAY_BETWEEN_BATCHES_MS,
+ intervalCap: CONCURRENCY_LIMIT,
+})
type BalanceResult = {
- assetId: AssetId;
- balance: string;
- balanceFormatted: string;
-};
+ assetId: AssetId
+ balance: string
+ balanceFormatted: string
+}
-type BalancesMap = Record;
+type BalancesMap = Record
const parseAssetId = (
assetId: AssetId,
): { chainId: number; tokenAddress?: `0x${string}` } | null => {
- const [chainPart, assetPart] = assetId.split("/");
+ const [chainPart, assetPart] = assetId.split('/')
- if (!chainPart?.startsWith("eip155:")) return null;
+ if (!chainPart?.startsWith('eip155:')) return null
- const chainId = getEvmChainIdNumber(chainPart);
+ const chainId = getEvmChainIdNumber(chainPart)
- if (!assetPart) return { chainId };
+ if (!assetPart) return { chainId }
- if (assetPart.startsWith("erc20:")) {
- const tokenAddress = assetPart.replace("erc20:", "") as `0x${string}`;
- return { chainId, tokenAddress };
+ if (assetPart.startsWith('erc20:')) {
+ const tokenAddress = assetPart.replace('erc20:', '') as `0x${string}`
+ return { chainId, tokenAddress }
}
- if (assetPart.startsWith("slip44:")) {
- return { chainId };
+ if (assetPart.startsWith('slip44:')) {
+ return { chainId }
}
- return { chainId };
-};
+ return { chainId }
+}
export const useAssetBalance = (
address: string | undefined,
assetId: AssetId | undefined,
precision: number = 18,
) => {
- const parsed = assetId ? parseAssetId(assetId) : null;
- const isNative = parsed && !parsed.tokenAddress;
- const isErc20 = parsed && !!parsed.tokenAddress;
+ const parsed = assetId ? parseAssetId(assetId) : null
+ const isNative = parsed && !parsed.tokenAddress
+ const isErc20 = parsed && !!parsed.tokenAddress
const {
data: nativeBalance,
@@ -100,7 +69,7 @@ export const useAssetBalance = (
staleTime: 60_000,
refetchOnWindowFocus: false,
},
- });
+ })
const {
data: erc20Balance,
@@ -115,19 +84,15 @@ export const useAssetBalance = (
staleTime: 60_000,
refetchOnWindowFocus: false,
},
- });
+ })
- const balance = isNative ? nativeBalance : isErc20 ? erc20Balance : undefined;
- const isLoading = isNative
- ? isNativeLoading
- : isErc20
- ? isErc20Loading
- : false;
- const refetch = isNative ? refetchNative : isErc20 ? refetchErc20 : undefined;
+ const balance = isNative ? nativeBalance : isErc20 ? erc20Balance : undefined
+ const isLoading = isNative ? isNativeLoading : isErc20 ? isErc20Loading : false
+ const refetch = isNative ? refetchNative : isErc20 ? refetchErc20 : undefined
return useMemo(() => {
if (!balance || !assetId) {
- return { data: undefined, isLoading, refetch };
+ return { data: undefined, isLoading, refetch }
}
return {
@@ -138,152 +103,147 @@ export const useAssetBalance = (
},
isLoading,
refetch,
- };
- }, [balance, assetId, precision, isLoading, refetch]);
-};
+ }
+ }, [balance, assetId, precision, isLoading, refetch])
+}
export const useEvmBalances = (
address: string | undefined,
assetIds: AssetId[],
assetPrecisions: Record,
) => {
- const config = useConfig();
+ const config = useConfig()
const parsedAssets = useMemo(() => {
return assetIds
- .map((assetId) => {
- const parsed = parseAssetId(assetId);
- if (!parsed) return null;
+ .map(assetId => {
+ const parsed = parseAssetId(assetId)
+ if (!parsed) return null
return {
assetId,
chainId: parsed.chainId,
tokenAddress: parsed.tokenAddress,
precision: assetPrecisions[assetId] ?? 18,
isNative: !parsed.tokenAddress,
- };
+ }
})
- .filter(Boolean) as Array<{
- assetId: AssetId;
- chainId: number;
- tokenAddress?: `0x${string}`;
- precision: number;
- isNative: boolean;
- }>;
- }, [assetIds, assetPrecisions]);
-
- const nativeAssets = useMemo(
- () => parsedAssets.filter((a) => a.isNative),
- [parsedAssets],
- );
-
- const erc20Assets = useMemo(
- () => parsedAssets.filter((a) => !a.isNative),
- [parsedAssets],
- );
+ .filter(Boolean) as {
+ assetId: AssetId
+ chainId: number
+ tokenAddress?: `0x${string}`
+ precision: number
+ isNative: boolean
+ }[]
+ }, [assetIds, assetPrecisions])
+
+ const nativeAssets = useMemo(() => parsedAssets.filter(a => a.isNative), [parsedAssets])
+
+ const erc20Assets = useMemo(() => parsedAssets.filter(a => !a.isNative), [parsedAssets])
const nativeQueries = useQueries({
- queries: nativeAssets.map((asset) => ({
- queryKey: ["nativeBalance", address, asset.chainId],
- queryFn: async () => {
- if (!address) return null;
+ queries: nativeAssets.map(asset => ({
+ queryKey: ['nativeBalance', address, asset.chainId],
+ queryFn: () => {
+ if (!address) return Promise.resolve(null)
return balanceQueue.add(async () => {
try {
const result = await getBalance(config, {
address: address as `0x${string}`,
chainId: asset.chainId,
- });
+ })
return {
assetId: asset.assetId,
balance: result.value.toString(),
precision: asset.precision,
- };
+ }
} catch {
- return null;
+ return null
}
- });
+ })
},
enabled: !!address,
staleTime: 60_000,
refetchOnWindowFocus: false,
})),
- });
+ })
const erc20Queries = useQueries({
- queries: erc20Assets.map((asset) => ({
- queryKey: ["erc20Balance", address, asset.chainId, asset.tokenAddress],
- queryFn: async () => {
- if (!address) return null;
+ queries: erc20Assets.map(asset => ({
+ queryKey: ['erc20Balance', address, asset.chainId, asset.tokenAddress],
+ queryFn: () => {
+ if (!address) return Promise.resolve(null)
return balanceQueue.add(async () => {
try {
+ if (!asset.tokenAddress) return null
const result = await readContract(config, {
- address: asset.tokenAddress!,
+ address: asset.tokenAddress,
abi: erc20Abi,
- functionName: "balanceOf",
+ functionName: 'balanceOf',
args: [address as `0x${string}`],
chainId: asset.chainId,
- });
+ })
return {
assetId: asset.assetId,
balance: (result as bigint).toString(),
precision: asset.precision,
- };
+ }
} catch {
- return null;
+ return null
}
- });
+ })
},
enabled: !!address,
staleTime: 60_000,
refetchOnWindowFocus: false,
})),
- });
+ })
- const isErc20Loading = erc20Queries.some((q) => q.isLoading);
+ const isErc20Loading = erc20Queries.some(q => q.isLoading)
const balances = useMemo((): BalancesMap => {
- const result: BalancesMap = {};
+ const result: BalancesMap = {}
- nativeQueries.forEach((query) => {
+ nativeQueries.forEach(query => {
if (query.data) {
- const { assetId, balance, precision } = query.data;
+ const { assetId, balance, precision } = query.data
result[assetId] = {
assetId,
balance,
balanceFormatted: formatAmount(balance, precision),
- };
+ }
}
- });
+ })
- erc20Queries.forEach((query) => {
+ erc20Queries.forEach(query => {
if (query.data) {
- const { assetId, balance, precision } = query.data;
+ const { assetId, balance, precision } = query.data
result[assetId] = {
assetId,
balance,
balanceFormatted: formatAmount(balance, precision),
- };
+ }
}
- });
+ })
- return result;
- }, [nativeQueries, erc20Queries]);
+ return result
+ }, [nativeQueries, erc20Queries])
- const isLoading = nativeQueries.some((q) => q.isLoading) || isErc20Loading;
+ const isLoading = nativeQueries.some(q => q.isLoading) || isErc20Loading
const loadingAssetIds = useMemo(() => {
- const loading = new Set();
+ const loading = new Set()
nativeQueries.forEach((query, index) => {
if (query.isLoading) {
- loading.add(nativeAssets[index].assetId);
+ loading.add(nativeAssets[index].assetId)
}
- });
+ })
erc20Queries.forEach((query, index) => {
if (query.isLoading) {
- loading.add(erc20Assets[index].assetId);
+ loading.add(erc20Assets[index].assetId)
}
- });
- return loading;
- }, [nativeQueries, nativeAssets, erc20Queries, erc20Assets]);
+ })
+ return loading
+ }, [nativeQueries, nativeAssets, erc20Queries, erc20Assets])
- return { data: balances, isLoading, loadingAssetIds };
-};
+ return { data: balances, isLoading, loadingAssetIds }
+}
diff --git a/packages/swap-widget-poc/src/hooks/useMarketData.ts b/packages/swap-widget-poc/src/hooks/useMarketData.ts
index 7680525957e..753de9fcb87 100644
--- a/packages/swap-widget-poc/src/hooks/useMarketData.ts
+++ b/packages/swap-widget-poc/src/hooks/useMarketData.ts
@@ -1,165 +1,162 @@
-import { useQuery } from "@tanstack/react-query";
-import { adapters } from "@shapeshiftoss/caip";
-import type { AssetId } from "../types";
+import { adapters } from '@shapeshiftoss/caip'
+import { useQuery } from '@tanstack/react-query'
-const MARKET_DATA_STALE_TIME = 10 * 60 * 1000;
-const MARKET_DATA_GC_TIME = 60 * 60 * 1000;
+import type { AssetId } from '../types'
+
+const MARKET_DATA_STALE_TIME = 10 * 60 * 1000
+const MARKET_DATA_GC_TIME = 60 * 60 * 1000
type MarketData = {
- price: string;
- marketCap: string;
- volume: string;
- changePercent24Hr: number;
-};
+ price: string
+ marketCap: string
+ volume: string
+ changePercent24Hr: number
+}
-export type MarketDataById = Record;
+export type MarketDataById = Record
type CoinGeckoMarketCap = {
- id: string;
- symbol: string;
- current_price: number;
- market_cap: number;
- total_volume: number;
- price_change_percentage_24h: number;
-};
-
-const COINGECKO_PROXY_URL = "https://api.proxy.shapeshift.com/api/v1/markets";
-const COINGECKO_DIRECT_URL = "https://api.coingecko.com/api/v3";
-
-const fetchWithRetry = async (
- url: string,
- retries = 2,
- delay = 1000,
-): Promise => {
+ id: string
+ symbol: string
+ current_price: number
+ market_cap: number
+ total_volume: number
+ price_change_percentage_24h: number
+}
+
+const COINGECKO_PROXY_URL = 'https://api.proxy.shapeshift.com/api/v1/markets'
+const COINGECKO_DIRECT_URL = 'https://api.coingecko.com/api/v3'
+
+const fetchWithRetry = async (url: string, retries = 2, delay = 1000): Promise => {
for (let i = 0; i <= retries; i++) {
try {
- const response = await fetch(url);
- if (response.ok) return response;
+ const response = await fetch(url)
+ if (response.ok) return response
if (response.status === 429 && i < retries) {
- await new Promise((r) => setTimeout(r, delay * (i + 1)));
- continue;
+ await new Promise(r => setTimeout(r, delay * (i + 1)))
+ continue
}
- return response;
+ return response
} catch (error) {
- if (i === retries) throw error;
- await new Promise((r) => setTimeout(r, delay * (i + 1)));
+ if (i === retries) throw error
+ await new Promise(r => setTimeout(r, delay * (i + 1)))
}
}
- throw new Error("Failed after retries");
-};
+ throw new Error('Failed after retries')
+}
const fetchMarketsPage = async (
baseUrl: string,
page: number,
perPage: number,
): Promise => {
- const url = `${baseUrl}/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=${perPage}&page=${page}&sparkline=false`;
- const response = await fetchWithRetry(url);
- if (!response.ok) return [];
- return response.json();
-};
+ const url = `${baseUrl}/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=${perPage}&page=${page}&sparkline=false`
+ const response = await fetchWithRetry(url)
+ if (!response.ok) return []
+ return response.json()
+}
const fetchAllMarketData = async (): Promise => {
- const result: MarketDataById = {};
- const perPage = 250;
- const totalPages = 2;
+ const result: MarketDataById = {}
+ const perPage = 250
+ const totalPages = 2
- let baseUrl = COINGECKO_PROXY_URL;
+ let baseUrl = COINGECKO_PROXY_URL
const testResponse = await fetch(
`${COINGECKO_PROXY_URL}/coins/markets?vs_currency=usd&per_page=1&page=1`,
- ).catch(() => null);
+ ).catch(() => null)
if (!testResponse?.ok) {
- baseUrl = COINGECKO_DIRECT_URL;
+ baseUrl = COINGECKO_DIRECT_URL
}
try {
- const allData: CoinGeckoMarketCap[] = [];
+ const allData: CoinGeckoMarketCap[] = []
for (let page = 1; page <= totalPages; page++) {
- const pageData = await fetchMarketsPage(baseUrl, page, perPage);
- allData.push(...pageData);
+ const pageData = await fetchMarketsPage(baseUrl, page, perPage)
+ allData.push(...pageData)
if (page < totalPages) {
- await new Promise((r) => setTimeout(r, 500));
+ await new Promise(r => setTimeout(r, 500))
}
}
for (const asset of allData) {
- const assetIds = adapters.coingeckoToAssetIds(asset.id);
- if (!assetIds?.length) continue;
+ const assetIds = adapters.coingeckoToAssetIds(asset.id)
+ if (!assetIds?.length) continue
const marketData: MarketData = {
- price: asset.current_price?.toString() ?? "0",
- marketCap: asset.market_cap?.toString() ?? "0",
- volume: asset.total_volume?.toString() ?? "0",
+ price: asset.current_price?.toString() ?? '0',
+ marketCap: asset.market_cap?.toString() ?? '0',
+ volume: asset.total_volume?.toString() ?? '0',
changePercent24Hr: asset.price_change_percentage_24h ?? 0,
- };
+ }
for (const assetId of assetIds) {
- result[assetId] = marketData;
+ result[assetId] = marketData
}
}
} catch (error) {
- console.error("Failed to fetch market data:", error);
+ console.error('Failed to fetch market data:', error)
}
- return result;
-};
+ return result
+}
export const useAllMarketData = () => {
return useQuery({
- queryKey: ["allMarketData"],
+ queryKey: ['allMarketData'],
queryFn: fetchAllMarketData,
staleTime: MARKET_DATA_STALE_TIME,
gcTime: MARKET_DATA_GC_TIME,
retry: 1,
retryDelay: 5000,
- });
-};
+ })
+}
export const useMarketData = (assetIds: AssetId[]) => {
- const { data: allMarketData, ...rest } = useAllMarketData();
+ const { data: allMarketData, ...rest } = useAllMarketData()
const filteredData = (() => {
- if (!allMarketData) return {};
+ if (!allMarketData) return {}
- const result: MarketDataById = {};
+ const result: MarketDataById = {}
for (const assetId of assetIds) {
if (allMarketData[assetId]) {
- result[assetId] = allMarketData[assetId];
+ result[assetId] = allMarketData[assetId]
}
}
- return result;
- })();
+ return result
+ })()
- return { data: filteredData, ...rest };
-};
+ return { data: filteredData, ...rest }
+}
export const useAssetPrice = (assetId: AssetId | undefined) => {
- const { data: allMarketData, ...rest } = useAllMarketData();
+ const { data: allMarketData, ...rest } = useAllMarketData()
return {
data: assetId ? allMarketData?.[assetId]?.price : undefined,
...rest,
- };
-};
+ }
+}
export const formatUsdValue = (
cryptoAmount: string,
precision: number,
usdPrice: string | undefined,
): string => {
- if (!usdPrice || usdPrice === "0") return "$0.00";
+ if (!usdPrice || usdPrice === '0') return '$0.00'
- const amount = Number(cryptoAmount) / Math.pow(10, precision);
- const usdValue = amount * Number(usdPrice);
+ const amount = Number(cryptoAmount) / Math.pow(10, precision)
+ const usdValue = amount * Number(usdPrice)
- if (usdValue < 0.01) return "< $0.01";
- if (usdValue < 1000) return `$${usdValue.toFixed(2)}`;
- if (usdValue < 1000000) return `$${(usdValue / 1000).toFixed(2)}K`;
- return `$${(usdValue / 1000000).toFixed(2)}M`;
-};
+ if (usdValue < 0.01) return '< $0.01'
+ if (usdValue < 1000) return `$${usdValue.toFixed(2)}`
+ if (usdValue < 1000000) return `$${(usdValue / 1000).toFixed(2)}K`
+ return `$${(usdValue / 1000000).toFixed(2)}M`
+}
-export type { MarketData };
+export type { MarketData }
diff --git a/packages/swap-widget-poc/src/hooks/useSwapQuote.ts b/packages/swap-widget-poc/src/hooks/useSwapQuote.ts
index dd82ab2c2d9..ae180ac6ed5 100644
--- a/packages/swap-widget-poc/src/hooks/useSwapQuote.ts
+++ b/packages/swap-widget-poc/src/hooks/useSwapQuote.ts
@@ -1,22 +1,20 @@
-import { useQuery } from "@tanstack/react-query";
-import type { ApiClient } from "../api/client";
-import type { AssetId, QuoteResponse, SwapperName } from "../types";
+import { useQuery } from '@tanstack/react-query'
+
+import type { ApiClient } from '../api/client'
+import type { AssetId, QuoteResponse, SwapperName } from '../types'
export type UseSwapQuoteParams = {
- sellAssetId: AssetId | undefined;
- buyAssetId: AssetId | undefined;
- sellAmountCryptoBaseUnit: string | undefined;
- sendAddress: string | undefined;
- receiveAddress: string | undefined;
- swapperName: SwapperName | undefined;
- slippageTolerancePercentageDecimal?: string;
- enabled?: boolean;
-};
+ sellAssetId: AssetId | undefined
+ buyAssetId: AssetId | undefined
+ sellAmountCryptoBaseUnit: string | undefined
+ sendAddress: string | undefined
+ receiveAddress: string | undefined
+ swapperName: SwapperName | undefined
+ slippageTolerancePercentageDecimal?: string
+ enabled?: boolean
+}
-export const useSwapQuote = (
- apiClient: ApiClient,
- params: UseSwapQuoteParams,
-) => {
+export const useSwapQuote = (apiClient: ApiClient, params: UseSwapQuoteParams) => {
const {
sellAssetId,
buyAssetId,
@@ -26,11 +24,11 @@ export const useSwapQuote = (
swapperName,
slippageTolerancePercentageDecimal,
enabled = true,
- } = params;
+ } = params
return useQuery({
queryKey: [
- "swapQuote",
+ 'swapQuote',
sellAssetId,
buyAssetId,
sellAmountCryptoBaseUnit,
@@ -38,7 +36,7 @@ export const useSwapQuote = (
receiveAddress,
swapperName,
],
- queryFn: async (): Promise => {
+ queryFn: (): Promise => {
if (
!sellAssetId ||
!buyAssetId ||
@@ -47,7 +45,7 @@ export const useSwapQuote = (
!receiveAddress ||
!swapperName
) {
- return null;
+ return Promise.resolve(null)
}
return apiClient.getQuote({
sellAssetId,
@@ -57,7 +55,7 @@ export const useSwapQuote = (
receiveAddress,
swapperName,
slippageTolerancePercentageDecimal,
- });
+ })
},
enabled:
enabled &&
@@ -68,5 +66,5 @@ export const useSwapQuote = (
!!receiveAddress &&
!!swapperName,
staleTime: 30_000,
- });
-};
+ })
+}
diff --git a/packages/swap-widget-poc/src/hooks/useSwapRates.ts b/packages/swap-widget-poc/src/hooks/useSwapRates.ts
index e39b3459233..36944fb2551 100644
--- a/packages/swap-widget-poc/src/hooks/useSwapRates.ts
+++ b/packages/swap-widget-poc/src/hooks/useSwapRates.ts
@@ -1,52 +1,44 @@
-import { useQuery } from "@tanstack/react-query";
-import type { ApiClient } from "../api/client";
-import type { AssetId, TradeRate } from "../types";
+import { useQuery } from '@tanstack/react-query'
+
+import type { ApiClient } from '../api/client'
+import type { AssetId, TradeRate } from '../types'
export type UseSwapRatesParams = {
- sellAssetId: AssetId | undefined;
- buyAssetId: AssetId | undefined;
- sellAmountCryptoBaseUnit: string | undefined;
- enabled?: boolean;
-};
+ sellAssetId: AssetId | undefined
+ buyAssetId: AssetId | undefined
+ sellAmountCryptoBaseUnit: string | undefined
+ enabled?: boolean
+}
-export const useSwapRates = (
- apiClient: ApiClient,
- params: UseSwapRatesParams,
-) => {
- const {
- sellAssetId,
- buyAssetId,
- sellAmountCryptoBaseUnit,
- enabled = true,
- } = params;
+export const useSwapRates = (apiClient: ApiClient, params: UseSwapRatesParams) => {
+ const { sellAssetId, buyAssetId, sellAmountCryptoBaseUnit, enabled = true } = params
return useQuery({
- queryKey: ["swapRates", sellAssetId, buyAssetId, sellAmountCryptoBaseUnit],
+ queryKey: ['swapRates', sellAssetId, buyAssetId, sellAmountCryptoBaseUnit],
queryFn: async (): Promise => {
if (!sellAssetId || !buyAssetId || !sellAmountCryptoBaseUnit) {
- return [];
+ return []
}
const response = await apiClient.getRates({
sellAssetId,
buyAssetId,
sellAmountCryptoBaseUnit,
- });
+ })
return response.rates
- .filter((rate) => !rate.error && rate.buyAmountCryptoBaseUnit !== "0")
+ .filter(rate => !rate.error && rate.buyAmountCryptoBaseUnit !== '0')
.map((rate, index) => ({
...rate,
id: rate.id ?? `${rate.swapperName}-${index}`,
}))
.sort((a, b) => {
- const aAmount = parseFloat(a.buyAmountCryptoBaseUnit);
- const bAmount = parseFloat(b.buyAmountCryptoBaseUnit);
- return bAmount - aAmount;
- });
+ const aAmount = parseFloat(a.buyAmountCryptoBaseUnit)
+ const bAmount = parseFloat(b.buyAmountCryptoBaseUnit)
+ return bAmount - aAmount
+ })
},
- enabled:
- enabled && !!sellAssetId && !!buyAssetId && !!sellAmountCryptoBaseUnit,
+ enabled: enabled && !!sellAssetId && !!buyAssetId && !!sellAmountCryptoBaseUnit,
staleTime: 10_000,
refetchInterval: 15_000,
- });
-};
+ })
+}
diff --git a/packages/swap-widget-poc/src/index.ts b/packages/swap-widget-poc/src/index.ts
index 4e673a53aa9..6f69d89c707 100644
--- a/packages/swap-widget-poc/src/index.ts
+++ b/packages/swap-widget-poc/src/index.ts
@@ -1,4 +1,4 @@
-export { SwapWidget } from "./components/SwapWidget";
+export { SwapWidget } from './components/SwapWidget'
export type {
Asset,
@@ -11,7 +11,7 @@ export type {
SwapWidgetProps,
ThemeMode,
ThemeConfig,
-} from "./types";
+} from './types'
export {
isEvmChainId,
@@ -24,7 +24,7 @@ export {
UTXO_CHAIN_IDS,
COSMOS_CHAIN_IDS,
OTHER_CHAIN_IDS,
-} from "./types";
+} from './types'
export {
CHAIN_METADATA,
@@ -32,7 +32,7 @@ export {
getChainName,
getChainIcon,
getChainColor,
-} from "./constants/chains";
+} from './constants/chains'
export {
useAssets,
@@ -40,4 +40,4 @@ export {
useChains,
useAssetsByChainId,
useAssetSearch,
-} from "./hooks/useAssets";
+} from './hooks/useAssets'
diff --git a/packages/swap-widget-poc/src/types/index.ts b/packages/swap-widget-poc/src/types/index.ts
index 14a1fe24211..a25197154e6 100644
--- a/packages/swap-widget-poc/src/types/index.ts
+++ b/packages/swap-widget-poc/src/types/index.ts
@@ -1,230 +1,227 @@
-export type ChainId = string;
-export type AssetId = string;
+export type ChainId = string
+export type AssetId = string
export type Chain = {
- chainId: ChainId;
- name: string;
- icon: string;
- color: string;
- nativeAssetId: AssetId;
-};
+ chainId: ChainId
+ name: string
+ icon: string
+ color: string
+ nativeAssetId: AssetId
+}
export type Asset = {
- assetId: AssetId;
- chainId: ChainId;
- symbol: string;
- name: string;
- precision: number;
- icon?: string;
- color?: string;
- networkName?: string;
- networkIcon?: string;
- explorer?: string;
- explorerTxLink?: string;
- explorerAddressLink?: string;
- relatedAssetKey?: AssetId | null;
-};
+ assetId: AssetId
+ chainId: ChainId
+ symbol: string
+ name: string
+ precision: number
+ icon?: string
+ color?: string
+ networkName?: string
+ networkIcon?: string
+ explorer?: string
+ explorerTxLink?: string
+ explorerAddressLink?: string
+ relatedAssetKey?: AssetId | null
+}
export type SwapperName =
- | "THORChain"
- | "MAYAChain"
- | "CoW Swap"
- | "0x"
- | "Portals"
- | "Chainflip"
- | "Relay"
- | "Bebop"
- | "Jupiter"
- | "1inch"
- | "ButterSwap"
- | "ArbitrumBridge";
+ | 'THORChain'
+ | 'MAYAChain'
+ | 'CoW Swap'
+ | '0x'
+ | 'Portals'
+ | 'Chainflip'
+ | 'Relay'
+ | 'Bebop'
+ | 'Jupiter'
+ | '1inch'
+ | 'ButterSwap'
+ | 'ArbitrumBridge'
export type TradeQuoteStep = {
- sellAsset: Asset;
- buyAsset: Asset;
- sellAmountCryptoBaseUnit: string;
- buyAmountCryptoBaseUnit: string;
- rate: string;
- source: SwapperName;
+ sellAsset: Asset
+ buyAsset: Asset
+ sellAmountCryptoBaseUnit: string
+ buyAmountCryptoBaseUnit: string
+ rate: string
+ source: SwapperName
feeData: {
- networkFeeCryptoBaseUnit: string;
- protocolFees?: Record;
- };
- allowanceContract?: string;
- estimatedExecutionTimeMs?: number;
-};
+ networkFeeCryptoBaseUnit: string
+ protocolFees?: Record
+ }
+ allowanceContract?: string
+ estimatedExecutionTimeMs?: number
+}
export type TradeQuote = {
- id: string;
- rate: string;
- swapperName: SwapperName;
- steps: TradeQuoteStep[];
- receiveAddress: string;
- affiliateBps: string;
- slippageTolerancePercentageDecimal?: string;
- isStreaming?: boolean;
-};
+ id: string
+ rate: string
+ swapperName: SwapperName
+ steps: TradeQuoteStep[]
+ receiveAddress: string
+ affiliateBps: string
+ slippageTolerancePercentageDecimal?: string
+ isStreaming?: boolean
+}
export type TradeRate = {
- swapperName: SwapperName;
- rate: string;
- buyAmountCryptoBaseUnit: string;
- sellAmountCryptoBaseUnit: string;
- steps: number;
- estimatedExecutionTimeMs?: number;
- affiliateBps: string;
- networkFeeCryptoBaseUnit?: string;
+ swapperName: SwapperName
+ rate: string
+ buyAmountCryptoBaseUnit: string
+ sellAmountCryptoBaseUnit: string
+ steps: number
+ estimatedExecutionTimeMs?: number
+ affiliateBps: string
+ networkFeeCryptoBaseUnit?: string
error?: {
- code: string;
- message: string;
- };
- id?: string;
-};
+ code: string
+ message: string
+ }
+ id?: string
+}
-export type ThemeMode = "light" | "dark";
+export type ThemeMode = 'light' | 'dark'
export type ThemeConfig = {
- mode: ThemeMode;
- accentColor?: string;
- backgroundColor?: string;
- cardColor?: string;
- textColor?: string;
- borderRadius?: string;
- fontFamily?: string;
-};
+ mode: ThemeMode
+ accentColor?: string
+ backgroundColor?: string
+ cardColor?: string
+ textColor?: string
+ borderRadius?: string
+ fontFamily?: string
+}
export type SwapWidgetProps = {
- apiKey?: string;
- apiBaseUrl?: string;
- defaultSellAsset?: Asset;
- defaultBuyAsset?: Asset;
- disabledChainIds?: ChainId[];
- disabledAssetIds?: AssetId[];
- allowedChainIds?: ChainId[];
- allowedAssetIds?: AssetId[];
- walletClient?: unknown;
- onConnectWallet?: () => void;
- onSwapSuccess?: (txHash: string) => void;
- onSwapError?: (error: Error) => void;
- onAssetSelect?: (type: "sell" | "buy", asset: Asset) => void;
- theme?: ThemeMode | ThemeConfig;
- defaultSlippage?: string;
- showPoweredBy?: boolean;
-};
+ apiKey?: string
+ apiBaseUrl?: string
+ defaultSellAsset?: Asset
+ defaultBuyAsset?: Asset
+ disabledChainIds?: ChainId[]
+ disabledAssetIds?: AssetId[]
+ allowedChainIds?: ChainId[]
+ allowedAssetIds?: AssetId[]
+ walletClient?: unknown
+ onConnectWallet?: () => void
+ onSwapSuccess?: (txHash: string) => void
+ onSwapError?: (error: Error) => void
+ onAssetSelect?: (type: 'sell' | 'buy', asset: Asset) => void
+ theme?: ThemeMode | ThemeConfig
+ defaultSlippage?: string
+ showPoweredBy?: boolean
+ enableWalletConnection?: boolean
+ walletConnectProjectId?: string
+ defaultReceiveAddress?: string
+}
export type RatesResponse = {
- rates: TradeRate[];
-};
+ rates: TradeRate[]
+}
type TransactionData = {
- to: string;
- data: string;
- value?: string;
- gasLimit?: string;
- chainId?: number;
- relayId?: string;
-};
+ to: string
+ data: string
+ value?: string
+ gasLimit?: string
+ chainId?: number
+ relayId?: string
+}
type QuoteStep = {
- transactionData?: TransactionData;
- relayTransactionMetadata?: TransactionData;
- butterSwapTransactionMetadata?: TransactionData;
-};
+ transactionData?: TransactionData
+ relayTransactionMetadata?: TransactionData
+ butterSwapTransactionMetadata?: TransactionData
+}
export type QuoteResponse = {
quote?: {
- steps?: QuoteStep[];
- };
- transactionData?: TransactionData;
- steps?: QuoteStep[];
+ steps?: QuoteStep[]
+ }
+ transactionData?: TransactionData
+ steps?: QuoteStep[]
approval?: {
- isRequired: boolean;
- spender: string;
- };
-};
+ isRequired: boolean
+ spender: string
+ }
+}
export const ERC20_APPROVE_ABI = [
{
- name: "approve",
- type: "function",
+ name: 'approve',
+ type: 'function',
inputs: [
- { name: "spender", type: "address" },
- { name: "amount", type: "uint256" },
+ { name: 'spender', type: 'address' },
+ { name: 'amount', type: 'uint256' },
],
- outputs: [{ name: "", type: "bool" }],
+ outputs: [{ name: '', type: 'bool' }],
},
-] as const;
+] as const
export type AssetsResponse = {
- byId: Record;
- ids: AssetId[];
-};
+ byId: Record
+ ids: AssetId[]
+}
export const EVM_CHAIN_IDS = {
- ethereum: "eip155:1",
- arbitrum: "eip155:42161",
- optimism: "eip155:10",
- polygon: "eip155:137",
- base: "eip155:8453",
- avalanche: "eip155:43114",
- bsc: "eip155:56",
- gnosis: "eip155:100",
- arbitrumNova: "eip155:42170",
-} as const;
+ ethereum: 'eip155:1',
+ arbitrum: 'eip155:42161',
+ optimism: 'eip155:10',
+ polygon: 'eip155:137',
+ base: 'eip155:8453',
+ avalanche: 'eip155:43114',
+ bsc: 'eip155:56',
+ gnosis: 'eip155:100',
+ arbitrumNova: 'eip155:42170',
+} as const
export const UTXO_CHAIN_IDS = {
- bitcoin: "bip122:000000000019d6689c085ae165831e93",
- bitcoinCash: "bip122:000000000000000000651ef99cb9fcbe",
- dogecoin: "bip122:00000000001a91e3dace36e2be3bf030",
- litecoin: "bip122:12a765e31ffd4059bada1e25190f6e98",
-} as const;
+ bitcoin: 'bip122:000000000019d6689c085ae165831e93',
+ bitcoinCash: 'bip122:000000000000000000651ef99cb9fcbe',
+ dogecoin: 'bip122:00000000001a91e3dace36e2be3bf030',
+ litecoin: 'bip122:12a765e31ffd4059bada1e25190f6e98',
+} as const
export const COSMOS_CHAIN_IDS = {
- cosmos: "cosmos:cosmoshub-4",
- thorchain: "cosmos:thorchain-1",
- mayachain: "cosmos:mayachain-mainnet-v1",
-} as const;
+ cosmos: 'cosmos:cosmoshub-4',
+ thorchain: 'cosmos:thorchain-1',
+ mayachain: 'cosmos:mayachain-mainnet-v1',
+} as const
export const OTHER_CHAIN_IDS = {
- solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
-} as const;
+ solana: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
+} as const
export const isEvmChainId = (chainId: string): boolean => {
- return chainId.startsWith("eip155:");
-};
+ return chainId.startsWith('eip155:')
+}
export const getEvmChainIdNumber = (chainId: string): number => {
- const parts = chainId.split(":");
- return parseInt(parts[1] ?? "1", 10);
-};
-
-export const getChainType = (
- chainId: string,
-): "evm" | "utxo" | "cosmos" | "solana" | "other" => {
- if (chainId.startsWith("eip155:")) return "evm";
- if (chainId.startsWith("bip122:")) return "utxo";
- if (chainId.startsWith("cosmos:")) return "cosmos";
- if (chainId.startsWith("solana:")) return "solana";
- return "other";
-};
-
-export const formatAmount = (
- amount: string,
- decimals: number,
- maxDecimals = 6,
-): string => {
- const num = Number(amount) / Math.pow(10, decimals);
- if (num === 0) return "0";
- if (num < 0.0001) return "< 0.0001";
- return num.toLocaleString(undefined, { maximumFractionDigits: maxDecimals });
-};
+ const parts = chainId.split(':')
+ return parseInt(parts[1] ?? '1', 10)
+}
+
+export const getChainType = (chainId: string): 'evm' | 'utxo' | 'cosmos' | 'solana' | 'other' => {
+ if (chainId.startsWith('eip155:')) return 'evm'
+ if (chainId.startsWith('bip122:')) return 'utxo'
+ if (chainId.startsWith('cosmos:')) return 'cosmos'
+ if (chainId.startsWith('solana:')) return 'solana'
+ return 'other'
+}
+
+export const formatAmount = (amount: string, decimals: number, maxDecimals = 6): string => {
+ const num = Number(amount) / Math.pow(10, decimals)
+ if (num === 0) return '0'
+ if (num < 0.0001) return '< 0.0001'
+ return num.toLocaleString(undefined, { maximumFractionDigits: maxDecimals })
+}
export const parseAmount = (amount: string, decimals: number): string => {
- const num = parseFloat(amount) || 0;
- return Math.floor(num * Math.pow(10, decimals)).toString();
-};
+ const num = parseFloat(amount) || 0
+ return Math.floor(num * Math.pow(10, decimals)).toString()
+}
export const truncateAddress = (address: string, chars = 4): string => {
- if (address.length <= chars * 2 + 2) return address;
- return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
-};
+ if (address.length <= chars * 2 + 2) return address
+ return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`
+}
diff --git a/packages/swap-widget-poc/src/utils/addressValidation.ts b/packages/swap-widget-poc/src/utils/addressValidation.ts
index 70db8c36b1f..942df359b64 100644
--- a/packages/swap-widget-poc/src/utils/addressValidation.ts
+++ b/packages/swap-widget-poc/src/utils/addressValidation.ts
@@ -1,110 +1,104 @@
-import { isAddress } from "viem";
-import type { ChainId } from "../types";
-import { getChainType, COSMOS_CHAIN_IDS } from "../types";
+import { isAddress } from 'viem'
+
+import type { ChainId } from '../types'
+import { COSMOS_CHAIN_IDS, getChainType } from '../types'
/**
* Validates an EVM address using viem
*/
export const isValidEvmAddress = (address: string): boolean => {
- return isAddress(address, { strict: false });
-};
+ return isAddress(address, { strict: false })
+}
/**
* Validates a Bitcoin address (Legacy, SegWit, Native SegWit, Taproot)
*/
export const isValidBitcoinAddress = (address: string): boolean => {
// Legacy (P2PKH) - starts with 1
- const legacyRegex = /^1[a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ const legacyRegex = /^1[a-km-zA-HJ-NP-Z1-9]{25,34}$/
// Legacy (P2SH) - starts with 3
- const p2shRegex = /^3[a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ const p2shRegex = /^3[a-km-zA-HJ-NP-Z1-9]{25,34}$/
// Native SegWit (Bech32) - starts with bc1q
- const nativeSegwitRegex = /^bc1q[a-z0-9]{38,58}$/i;
+ const nativeSegwitRegex = /^bc1q[a-z0-9]{38,58}$/i
// Taproot (Bech32m) - starts with bc1p
- const taprootRegex = /^bc1p[a-z0-9]{58}$/i;
+ const taprootRegex = /^bc1p[a-z0-9]{58}$/i
return (
legacyRegex.test(address) ||
p2shRegex.test(address) ||
nativeSegwitRegex.test(address) ||
taprootRegex.test(address)
- );
-};
+ )
+}
/**
* Validates a Bitcoin Cash address
*/
export const isValidBitcoinCashAddress = (address: string): boolean => {
// CashAddr format - starts with bitcoincash: or just q/p
- const cashAddrRegex = /^(bitcoincash:)?[qp][a-z0-9]{41}$/i;
+ const cashAddrRegex = /^(bitcoincash:)?[qp][a-z0-9]{41}$/i
// Legacy format (same as Bitcoin)
- const legacyRegex = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ const legacyRegex = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/
- return cashAddrRegex.test(address) || legacyRegex.test(address);
-};
+ return cashAddrRegex.test(address) || legacyRegex.test(address)
+}
/**
* Validates a Litecoin address
*/
export const isValidLitecoinAddress = (address: string): boolean => {
// Legacy (P2PKH) - starts with L
- const legacyRegex = /^L[a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ const legacyRegex = /^L[a-km-zA-HJ-NP-Z1-9]{25,34}$/
// Legacy (P2SH) - starts with M or 3
- const p2shRegex = /^[M3][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
+ const p2shRegex = /^[M3][a-km-zA-HJ-NP-Z1-9]{25,34}$/
// Native SegWit (Bech32) - starts with ltc1
- const nativeSegwitRegex = /^ltc1[a-z0-9]{38,58}$/i;
+ const nativeSegwitRegex = /^ltc1[a-z0-9]{38,58}$/i
- return (
- legacyRegex.test(address) ||
- p2shRegex.test(address) ||
- nativeSegwitRegex.test(address)
- );
-};
+ return legacyRegex.test(address) || p2shRegex.test(address) || nativeSegwitRegex.test(address)
+}
/**
* Validates a Dogecoin address
*/
export const isValidDogecoinAddress = (address: string): boolean => {
// P2PKH - starts with D
- const p2pkhRegex = /^D[5-9A-HJ-NP-U][a-km-zA-HJ-NP-Z1-9]{32}$/;
+ const p2pkhRegex = /^D[5-9A-HJ-NP-U][a-km-zA-HJ-NP-Z1-9]{32}$/
// P2SH - starts with 9 or A
- const p2shRegex = /^[9A][a-km-zA-HJ-NP-Z1-9]{33}$/;
+ const p2shRegex = /^[9A][a-km-zA-HJ-NP-Z1-9]{33}$/
- return p2pkhRegex.test(address) || p2shRegex.test(address);
-};
+ return p2pkhRegex.test(address) || p2shRegex.test(address)
+}
/**
* Validates a Cosmos SDK address (bech32 format)
*/
-export const isValidCosmosAddress = (
- address: string,
- expectedPrefix?: string,
-): boolean => {
+export const isValidCosmosAddress = (address: string, expectedPrefix?: string): boolean => {
// Basic bech32 validation - prefix + 1 + base32 characters
- const bech32Regex = /^[a-z]{1,83}1[a-z0-9]{38,58}$/i;
+ const bech32Regex = /^[a-z]{1,83}1[a-z0-9]{38,58}$/i
if (!bech32Regex.test(address)) {
- return false;
+ return false
}
// If prefix is specified, validate it
if (expectedPrefix) {
- return address.toLowerCase().startsWith(expectedPrefix.toLowerCase());
+ return address.toLowerCase().startsWith(expectedPrefix.toLowerCase())
}
- return true;
-};
+ return true
+}
/**
* Gets the expected bech32 prefix for a Cosmos chain
*/
const getCosmosPrefix = (chainId: ChainId): string | undefined => {
const prefixMap: Record = {
- [COSMOS_CHAIN_IDS.cosmos]: "cosmos",
- [COSMOS_CHAIN_IDS.thorchain]: "thor",
- [COSMOS_CHAIN_IDS.mayachain]: "maya",
- };
- return prefixMap[chainId];
-};
+ [COSMOS_CHAIN_IDS.cosmos]: 'cosmos',
+ [COSMOS_CHAIN_IDS.thorchain]: 'thor',
+ [COSMOS_CHAIN_IDS.mayachain]: 'maya',
+ }
+ return prefixMap[chainId]
+}
/**
* Validates a Solana address (base58, 32-44 chars)
@@ -112,9 +106,9 @@ const getCosmosPrefix = (chainId: ChainId): string | undefined => {
export const isValidSolanaAddress = (address: string): boolean => {
// Solana addresses are base58 encoded, typically 32-44 characters
// Base58 alphabet excludes 0, O, I, l
- const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
- return base58Regex.test(address);
-};
+ const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/
+ return base58Regex.test(address)
+}
/**
* Validates an address for a specific chain
@@ -123,96 +117,96 @@ export const validateAddress = (
address: string,
chainId: ChainId,
): { valid: boolean; error?: string } => {
- if (!address || address.trim() === "") {
- return { valid: false, error: "Address is required" };
+ if (!address || address.trim() === '') {
+ return { valid: false, error: 'Address is required' }
}
- const trimmedAddress = address.trim();
- const chainType = getChainType(chainId);
+ const trimmedAddress = address.trim()
+ const chainType = getChainType(chainId)
switch (chainType) {
- case "evm":
+ case 'evm':
if (!isValidEvmAddress(trimmedAddress)) {
- return { valid: false, error: "Invalid EVM address" };
+ return { valid: false, error: 'Invalid EVM address' }
}
- break;
+ break
- case "utxo":
+ case 'utxo':
// Determine specific UTXO chain
- if (chainId.includes("000000000019d6689c085ae165831e93")) {
+ if (chainId.includes('000000000019d6689c085ae165831e93')) {
// Bitcoin
if (!isValidBitcoinAddress(trimmedAddress)) {
- return { valid: false, error: "Invalid Bitcoin address" };
+ return { valid: false, error: 'Invalid Bitcoin address' }
}
- } else if (chainId.includes("000000000000000000651ef99cb9fcbe")) {
+ } else if (chainId.includes('000000000000000000651ef99cb9fcbe')) {
// Bitcoin Cash
if (!isValidBitcoinCashAddress(trimmedAddress)) {
- return { valid: false, error: "Invalid Bitcoin Cash address" };
+ return { valid: false, error: 'Invalid Bitcoin Cash address' }
}
- } else if (chainId.includes("12a765e31ffd4059bada1e25190f6e98")) {
+ } else if (chainId.includes('12a765e31ffd4059bada1e25190f6e98')) {
// Litecoin
if (!isValidLitecoinAddress(trimmedAddress)) {
- return { valid: false, error: "Invalid Litecoin address" };
+ return { valid: false, error: 'Invalid Litecoin address' }
}
- } else if (chainId.includes("00000000001a91e3dace36e2be3bf030")) {
+ } else if (chainId.includes('00000000001a91e3dace36e2be3bf030')) {
// Dogecoin
if (!isValidDogecoinAddress(trimmedAddress)) {
- return { valid: false, error: "Invalid Dogecoin address" };
+ return { valid: false, error: 'Invalid Dogecoin address' }
}
} else {
- return { valid: false, error: "Unsupported UTXO chain" };
+ return { valid: false, error: 'Unsupported UTXO chain' }
}
- break;
+ break
- case "cosmos":
- const expectedPrefix = getCosmosPrefix(chainId);
+ case 'cosmos':
+ const expectedPrefix = getCosmosPrefix(chainId)
if (!isValidCosmosAddress(trimmedAddress, expectedPrefix)) {
const chainName = expectedPrefix
? expectedPrefix.charAt(0).toUpperCase() + expectedPrefix.slice(1)
- : "Cosmos";
- return { valid: false, error: `Invalid ${chainName} address` };
+ : 'Cosmos'
+ return { valid: false, error: `Invalid ${chainName} address` }
}
- break;
+ break
- case "solana":
+ case 'solana':
if (!isValidSolanaAddress(trimmedAddress)) {
- return { valid: false, error: "Invalid Solana address" };
+ return { valid: false, error: 'Invalid Solana address' }
}
- break;
+ break
default:
- return { valid: false, error: "Unsupported chain type" };
+ return { valid: false, error: 'Unsupported chain type' }
}
- return { valid: true };
-};
+ return { valid: true }
+}
/**
* Gets the expected address format hint for a chain
*/
export const getAddressFormatHint = (chainId: ChainId): string => {
- const chainType = getChainType(chainId);
+ const chainType = getChainType(chainId)
switch (chainType) {
- case "evm":
- return "0x...";
- case "utxo":
- if (chainId.includes("000000000019d6689c085ae165831e93")) {
- return "bc1... or 1... or 3...";
- } else if (chainId.includes("000000000000000000651ef99cb9fcbe")) {
- return "bitcoincash:q... or 1...";
- } else if (chainId.includes("12a765e31ffd4059bada1e25190f6e98")) {
- return "ltc1... or L... or M...";
- } else if (chainId.includes("00000000001a91e3dace36e2be3bf030")) {
- return "D...";
+ case 'evm':
+ return '0x...'
+ case 'utxo':
+ if (chainId.includes('000000000019d6689c085ae165831e93')) {
+ return 'bc1... or 1... or 3...'
+ } else if (chainId.includes('000000000000000000651ef99cb9fcbe')) {
+ return 'bitcoincash:q... or 1...'
+ } else if (chainId.includes('12a765e31ffd4059bada1e25190f6e98')) {
+ return 'ltc1... or L... or M...'
+ } else if (chainId.includes('00000000001a91e3dace36e2be3bf030')) {
+ return 'D...'
}
- return "Enter address";
- case "cosmos":
- const prefix = getCosmosPrefix(chainId);
- return prefix ? `${prefix}1...` : "Enter address";
- case "solana":
- return "Base58 address";
+ return 'Enter address'
+ case 'cosmos':
+ const prefix = getCosmosPrefix(chainId)
+ return prefix ? `${prefix}1...` : 'Enter address'
+ case 'solana':
+ return 'Base58 address'
default:
- return "Enter address";
+ return 'Enter address'
}
-};
+}
diff --git a/packages/swap-widget-poc/src/utils/redirect.ts b/packages/swap-widget-poc/src/utils/redirect.ts
index 27d6e46184c..a24a03f5894 100644
--- a/packages/swap-widget-poc/src/utils/redirect.ts
+++ b/packages/swap-widget-poc/src/utils/redirect.ts
@@ -1,48 +1,45 @@
-import type { AssetId, Asset } from "../types";
-import { isEvmChainId } from "../types";
+import type { Asset, AssetId } from '../types'
+import { isEvmChainId } from '../types'
-const SHAPESHIFT_APP_URL = "https://app.shapeshift.com";
+const SHAPESHIFT_APP_URL = 'https://app.shapeshift.com'
export type RedirectParams = {
- sellAssetId: AssetId;
- buyAssetId: AssetId;
- sellAmount?: string;
-};
+ sellAssetId: AssetId
+ buyAssetId: AssetId
+ sellAmount?: string
+}
export const buildShapeShiftTradeUrl = (params: RedirectParams): string => {
- const url = new URL(`${SHAPESHIFT_APP_URL}/trade`);
- url.searchParams.set("sellAssetId", params.sellAssetId);
- url.searchParams.set("buyAssetId", params.buyAssetId);
+ const url = new URL(`${SHAPESHIFT_APP_URL}/trade`)
+ url.searchParams.set('sellAssetId', params.sellAssetId)
+ url.searchParams.set('buyAssetId', params.buyAssetId)
if (params.sellAmount) {
- url.searchParams.set("sellAmount", params.sellAmount);
+ url.searchParams.set('sellAmount', params.sellAmount)
}
- return url.toString();
-};
+ return url.toString()
+}
export const redirectToShapeShift = (params: RedirectParams): void => {
- const url = buildShapeShiftTradeUrl(params);
- window.open(url, "_blank", "noopener,noreferrer");
-};
+ const url = buildShapeShiftTradeUrl(params)
+ window.open(url, '_blank', 'noopener,noreferrer')
+}
-export type ChainType = "evm" | "utxo" | "cosmos" | "solana" | "other";
+export type ChainType = 'evm' | 'utxo' | 'cosmos' | 'solana' | 'other'
export const getChainTypeFromAsset = (asset: Asset): ChainType => {
- const chainId = asset.chainId;
+ const chainId = asset.chainId
- if (isEvmChainId(chainId)) return "evm";
- if (chainId.startsWith("bip122:")) return "utxo";
- if (chainId.startsWith("cosmos:")) return "cosmos";
- if (chainId.startsWith("solana:")) return "solana";
+ if (isEvmChainId(chainId)) return 'evm'
+ if (chainId.startsWith('bip122:')) return 'utxo'
+ if (chainId.startsWith('cosmos:')) return 'cosmos'
+ if (chainId.startsWith('solana:')) return 'solana'
- return "other";
-};
+ return 'other'
+}
-export const canExecuteInWidget = (
- sellAsset: Asset,
- buyAsset: Asset,
-): boolean => {
- const sellChainType = getChainTypeFromAsset(sellAsset);
- const buyChainType = getChainTypeFromAsset(buyAsset);
+export const canExecuteInWidget = (sellAsset: Asset, buyAsset: Asset): boolean => {
+ const sellChainType = getChainTypeFromAsset(sellAsset)
+ const buyChainType = getChainTypeFromAsset(buyAsset)
- return sellChainType === "evm" && buyChainType === "evm";
-};
+ return sellChainType === 'evm' && buyChainType === 'evm'
+}
diff --git a/packages/swap-widget-poc/src/vite-env.d.ts b/packages/swap-widget-poc/src/vite-env.d.ts
index b60eb85e682..2b2ebad0ae3 100644
--- a/packages/swap-widget-poc/src/vite-env.d.ts
+++ b/packages/swap-widget-poc/src/vite-env.d.ts
@@ -1,10 +1,10 @@
///
interface EthereumProvider {
- request: (args: { method: string; params?: unknown[] }) => Promise;
- on: (event: string, callback: (accounts: string[]) => void) => void;
+ request: (args: { method: string; params?: unknown[] }) => Promise
+ on: (event: string, callback: (accounts: string[]) => void) => void
}
interface Window {
- ethereum?: EthereumProvider;
+ ethereum?: EthereumProvider
}
From 0cdec6616d2434bed44d19ea06cef9e99c82a2a3 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 13 Jan 2026 11:52:23 +0100
Subject: [PATCH 18/41] chore: rename swap-widget-poc to swap-widget, fix
yarn.lock
---
package.json | 2 +-
.../Dockerfile | 2 +-
.../README.md | 32 +++++++++----------
.../index.html | 0
.../package.json | 4 +--
.../src/api/client.ts | 0
.../src/components/AddressInputModal.css | 0
.../src/components/AddressInputModal.tsx | 0
.../src/components/QuoteSelector.css | 0
.../src/components/QuoteSelector.tsx | 0
.../src/components/QuotesModal.css | 0
.../src/components/QuotesModal.tsx | 0
.../src/components/SettingsModal.css | 0
.../src/components/SettingsModal.tsx | 0
.../src/components/SwapWidget.css | 0
.../src/components/SwapWidget.tsx | 0
.../src/components/TokenSelectModal.css | 0
.../src/components/TokenSelectModal.tsx | 0
.../src/components/WalletProvider.tsx | 0
.../src/constants/chains.ts | 0
.../src/constants/swappers.ts | 0
.../src/demo/App.css | 0
.../src/demo/App.tsx | 0
.../src/demo/main.tsx | 0
.../src/hooks/useAssets.ts | 0
.../src/hooks/useBalances.ts | 0
.../src/hooks/useMarketData.ts | 0
.../src/hooks/useSwapQuote.ts | 0
.../src/hooks/useSwapRates.ts | 0
.../src/index.ts | 0
.../src/types/index.ts | 0
.../src/utils/addressValidation.ts | 0
.../src/utils/redirect.ts | 0
.../src/vite-env.d.ts | 0
.../tsconfig.json | 0
.../tsconfig.node.json | 0
.../vite.config.ts | 0
railway.json | 2 +-
yarn.lock | 5 +--
39 files changed, 24 insertions(+), 23 deletions(-)
rename packages/{swap-widget-poc => swap-widget}/Dockerfile (88%)
rename packages/{swap-widget-poc => swap-widget}/README.md (95%)
rename packages/{swap-widget-poc => swap-widget}/index.html (100%)
rename packages/{swap-widget-poc => swap-widget}/package.json (88%)
rename packages/{swap-widget-poc => swap-widget}/src/api/client.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/AddressInputModal.css (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/AddressInputModal.tsx (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/QuoteSelector.css (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/QuoteSelector.tsx (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/QuotesModal.css (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/QuotesModal.tsx (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/SettingsModal.css (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/SettingsModal.tsx (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/SwapWidget.css (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/SwapWidget.tsx (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/TokenSelectModal.css (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/TokenSelectModal.tsx (100%)
rename packages/{swap-widget-poc => swap-widget}/src/components/WalletProvider.tsx (100%)
rename packages/{swap-widget-poc => swap-widget}/src/constants/chains.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/constants/swappers.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/demo/App.css (100%)
rename packages/{swap-widget-poc => swap-widget}/src/demo/App.tsx (100%)
rename packages/{swap-widget-poc => swap-widget}/src/demo/main.tsx (100%)
rename packages/{swap-widget-poc => swap-widget}/src/hooks/useAssets.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/hooks/useBalances.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/hooks/useMarketData.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/hooks/useSwapQuote.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/hooks/useSwapRates.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/index.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/types/index.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/utils/addressValidation.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/utils/redirect.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/src/vite-env.d.ts (100%)
rename packages/{swap-widget-poc => swap-widget}/tsconfig.json (100%)
rename packages/{swap-widget-poc => swap-widget}/tsconfig.node.json (100%)
rename packages/{swap-widget-poc => swap-widget}/vite.config.ts (100%)
diff --git a/package.json b/package.json
index 1f4344bf342..73fcd3079e8 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"dev:web": "vite",
"dev:web:linked": "GENERATE_SOURCEMAP=false BROWSER=none yarn dev:web",
"dev:packages": "yarn tsc --build --watch --preserveWatchOutput tsconfig.packages.json",
- "dev:swap-widget": "yarn workspace @shapeshiftoss/swap-widget-poc dev",
+ "dev:swap-widget": "yarn workspace @shapeshiftoss/swap-widget dev",
"generate:all": "yarn generate:caip-adapters && yarn run generate:color-map && yarn run generate:asset-data && yarn run generate:tradable-asset-map && yarn run generate:thor-longtail-tokens",
"generate:caip-adapters": "yarn workspace @shapeshiftoss/caip generate",
"generate:asset-data": "yarn tsx ./scripts/generateAssetData/generateAssetData.ts && yarn run codemod:clear-assets-migration",
diff --git a/packages/swap-widget-poc/Dockerfile b/packages/swap-widget/Dockerfile
similarity index 88%
rename from packages/swap-widget-poc/Dockerfile
rename to packages/swap-widget/Dockerfile
index 421409c1ad5..e2dd3ff8bc2 100644
--- a/packages/swap-widget-poc/Dockerfile
+++ b/packages/swap-widget/Dockerfile
@@ -2,7 +2,7 @@ FROM node:20-slim AS builder
WORKDIR /app
-COPY packages/swap-widget-poc ./
+COPY packages/swap-widget ./
RUN npm install --legacy-peer-deps
RUN npm run build
diff --git a/packages/swap-widget-poc/README.md b/packages/swap-widget/README.md
similarity index 95%
rename from packages/swap-widget-poc/README.md
rename to packages/swap-widget/README.md
index bbe7778ee31..1b1dd620e28 100644
--- a/packages/swap-widget-poc/README.md
+++ b/packages/swap-widget/README.md
@@ -1,4 +1,4 @@
-# @shapeshiftoss/swap-widget-poc
+# @shapeshiftoss/swap-widget
An embeddable React widget that enables multi-chain token swaps using ShapeShift's aggregation API. Integrate swaps into your application with minimal configuration.
@@ -18,9 +18,9 @@ An embeddable React widget that enables multi-chain token swaps using ShapeShift
## Installation
```bash
-yarn add @shapeshiftoss/swap-widget-poc
+yarn add @shapeshiftoss/swap-widget
# or
-npm install @shapeshiftoss/swap-widget-poc
+npm install @shapeshiftoss/swap-widget
```
### Peer Dependencies
@@ -39,7 +39,7 @@ This package requires React 18 or later:
## Quick Start
```tsx
-import { SwapWidget } from "@shapeshiftoss/swap-widget-poc";
+import { SwapWidget } from "@shapeshiftoss/swap-widget";
function App() {
return (
@@ -94,8 +94,8 @@ The widget supports both simple theme modes and full customization.
### Custom Theme Configuration
```tsx
-import { SwapWidget } from "@shapeshiftoss/swap-widget-poc";
-import type { ThemeConfig } from "@shapeshiftoss/swap-widget-poc";
+import { SwapWidget } from "@shapeshiftoss/swap-widget";
+import type { ThemeConfig } from "@shapeshiftoss/swap-widget";
const customTheme: ThemeConfig = {
mode: "dark",
@@ -129,7 +129,7 @@ function App() {
### Basic Usage
```tsx
-import { SwapWidget } from "@shapeshiftoss/swap-widget-poc";
+import { SwapWidget } from "@shapeshiftoss/swap-widget";
function App() {
return