From c3005d5fe2bf8247cb2f17ecc9a5b7902f14997f Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:01:51 +0100 Subject: [PATCH 01/41] feat: widget poc --- package.json | 1 + packages/swap-widget-poc/index.html | 28 + packages/swap-widget-poc/package.json | 34 + packages/swap-widget-poc/src/api/client.ts | 79 + .../src/components/QuoteSelector.css | 146 + .../src/components/QuoteSelector.tsx | 150 + .../src/components/QuotesModal.css | 244 ++ .../src/components/QuotesModal.tsx | 158 ++ .../src/components/SettingsModal.css | 133 + .../src/components/SettingsModal.tsx | 168 ++ .../src/components/SwapWidget.css | 336 +++ .../src/components/SwapWidget.tsx | 569 ++++ .../src/components/TokenSelectModal.css | 365 +++ .../src/components/TokenSelectModal.tsx | 353 +++ .../swap-widget-poc/src/constants/chains.ts | 147 + .../swap-widget-poc/src/constants/swappers.ts | 52 + packages/swap-widget-poc/src/demo/App.css | 454 +++ packages/swap-widget-poc/src/demo/App.tsx | 466 ++++ packages/swap-widget-poc/src/demo/main.tsx | 21 + .../swap-widget-poc/src/hooks/useAssets.ts | 194 ++ .../swap-widget-poc/src/hooks/useBalances.ts | 216 ++ .../src/hooks/useMarketData.ts | 118 + .../swap-widget-poc/src/hooks/useSwapQuote.ts | 66 + .../swap-widget-poc/src/hooks/useSwapRates.ts | 52 + packages/swap-widget-poc/src/index.ts | 43 + packages/swap-widget-poc/src/types/index.ts | 201 ++ .../swap-widget-poc/src/utils/redirect.ts | 48 + packages/swap-widget-poc/src/vite-env.d.ts | 10 + packages/swap-widget-poc/tsconfig.json | 23 + packages/swap-widget-poc/tsconfig.node.json | 10 + packages/swap-widget-poc/vite.config.ts | 29 + yarn.lock | 2459 ++++++++++++++++- 32 files changed, 7341 insertions(+), 32 deletions(-) create mode 100644 packages/swap-widget-poc/index.html create mode 100644 packages/swap-widget-poc/package.json create mode 100644 packages/swap-widget-poc/src/api/client.ts create mode 100644 packages/swap-widget-poc/src/components/QuoteSelector.css create mode 100644 packages/swap-widget-poc/src/components/QuoteSelector.tsx create mode 100644 packages/swap-widget-poc/src/components/QuotesModal.css create mode 100644 packages/swap-widget-poc/src/components/QuotesModal.tsx create mode 100644 packages/swap-widget-poc/src/components/SettingsModal.css create mode 100644 packages/swap-widget-poc/src/components/SettingsModal.tsx create mode 100644 packages/swap-widget-poc/src/components/SwapWidget.css create mode 100644 packages/swap-widget-poc/src/components/SwapWidget.tsx create mode 100644 packages/swap-widget-poc/src/components/TokenSelectModal.css create mode 100644 packages/swap-widget-poc/src/components/TokenSelectModal.tsx create mode 100644 packages/swap-widget-poc/src/constants/chains.ts create mode 100644 packages/swap-widget-poc/src/constants/swappers.ts create mode 100644 packages/swap-widget-poc/src/demo/App.css create mode 100644 packages/swap-widget-poc/src/demo/App.tsx create mode 100644 packages/swap-widget-poc/src/demo/main.tsx create mode 100644 packages/swap-widget-poc/src/hooks/useAssets.ts create mode 100644 packages/swap-widget-poc/src/hooks/useBalances.ts create mode 100644 packages/swap-widget-poc/src/hooks/useMarketData.ts create mode 100644 packages/swap-widget-poc/src/hooks/useSwapQuote.ts create mode 100644 packages/swap-widget-poc/src/hooks/useSwapRates.ts create mode 100644 packages/swap-widget-poc/src/index.ts create mode 100644 packages/swap-widget-poc/src/types/index.ts create mode 100644 packages/swap-widget-poc/src/utils/redirect.ts create mode 100644 packages/swap-widget-poc/src/vite-env.d.ts create mode 100644 packages/swap-widget-poc/tsconfig.json create mode 100644 packages/swap-widget-poc/tsconfig.node.json create mode 100644 packages/swap-widget-poc/vite.config.ts diff --git a/package.json b/package.json index 52c29c0c385..1f4344bf342 100644 --- a/package.json +++ b/package.json @@ -29,6 +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", "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/index.html b/packages/swap-widget-poc/index.html new file mode 100644 index 00000000000..aea8eaadc3a --- /dev/null +++ b/packages/swap-widget-poc/index.html @@ -0,0 +1,28 @@ + + + + + + ShapeShift Swap Widget POC + + + +
+ + + diff --git a/packages/swap-widget-poc/package.json b/packages/swap-widget-poc/package.json new file mode 100644 index 00000000000..92cc4e06ec2 --- /dev/null +++ b/packages/swap-widget-poc/package.json @@ -0,0 +1,34 @@ +{ + "name": "@shapeshiftoss/swap-widget-poc", + "version": "0.0.1", + "private": true, + "description": "POC: Embeddable swap widget using ShapeShift API", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@rainbow-me/rainbowkit": "^2.2.3", + "@shapeshiftoss/caip": "workspace:*", + "@tanstack/react-query": "^5.60.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "viem": "^2.21.0", + "wagmi": "^2.14.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "typescript": "^5.2.2", + "vite": "^5.0.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } +} diff --git a/packages/swap-widget-poc/src/api/client.ts b/packages/swap-widget-poc/src/api/client.ts new file mode 100644 index 00000000000..b75a4a0e8dc --- /dev/null +++ b/packages/swap-widget-poc/src/api/client.ts @@ -0,0 +1,79 @@ +import type { + RatesResponse, + QuoteResponse, + AssetsResponse, + AssetId, +} from "../types"; + +const DEFAULT_API_BASE_URL = "https://api.shapeshift.com"; + +export type ApiClientConfig = { + baseUrl?: string; + apiKey?: string; +}; + +export const createApiClient = (config: ApiClientConfig = {}) => { + const baseUrl = config.baseUrl ?? DEFAULT_API_BASE_URL; + + const fetchWithConfig = async ( + endpoint: string, + params?: Record, + ): Promise => { + const url = new URL(`${baseUrl}${endpoint}`); + if (params) { + Object.entries(params).forEach(([key, value]) => { + url.searchParams.append(key, value); + }); + } + + const headers: Record = { + "Content-Type": "application/json", + }; + if (config.apiKey) { + headers["x-api-key"] = config.apiKey; + } + + const response = await fetch(url.toString(), { headers }); + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`); + } + return response.json() as Promise; + }; + + return { + getAssets: () => fetchWithConfig("/v1/assets"), + + getRates: (params: { + sellAssetId: AssetId; + buyAssetId: AssetId; + sellAmountCryptoBaseUnit: string; + }) => + fetchWithConfig("/v1/swap/rates", { + sellAssetId: params.sellAssetId, + buyAssetId: params.buyAssetId, + sellAmountCryptoBaseUnit: params.sellAmountCryptoBaseUnit, + }), + + getQuote: (params: { + sellAssetId: AssetId; + buyAssetId: AssetId; + sellAmountCryptoBaseUnit: string; + receiveAddress: string; + swapperName: string; + slippageTolerancePercentageDecimal?: string; + }) => + fetchWithConfig("/v1/swap/quote", { + sellAssetId: params.sellAssetId, + buyAssetId: params.buyAssetId, + sellAmountCryptoBaseUnit: params.sellAmountCryptoBaseUnit, + receiveAddress: params.receiveAddress, + swapperName: params.swapperName, + ...(params.slippageTolerancePercentageDecimal && { + slippageTolerancePercentageDecimal: + params.slippageTolerancePercentageDecimal, + }), + }), + }; +}; + +export type ApiClient = ReturnType; diff --git a/packages/swap-widget-poc/src/components/QuoteSelector.css b/packages/swap-widget-poc/src/components/QuoteSelector.css new file mode 100644 index 00000000000..aed09d7c917 --- /dev/null +++ b/packages/swap-widget-poc/src/components/QuoteSelector.css @@ -0,0 +1,146 @@ +.ssw-quote-selector { + width: 100%; + padding: 14px 16px; + border-radius: 14px; + background: var(--ssw-bg-tertiary); + border: 1px solid var(--ssw-border); + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.ssw-quote-selector:hover { + border-color: var(--ssw-border-hover); + background: var(--ssw-bg-hover); +} + +.ssw-quote-selector.ssw-loading { + cursor: default; + justify-content: center; +} + +.ssw-quote-selector.ssw-loading:hover { + border-color: var(--ssw-border); + background: var(--ssw-bg-tertiary); +} + +.ssw-quote-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + color: var(--ssw-text-secondary); + font-size: 14px; +} + +.ssw-spinner-small { + width: 16px; + height: 16px; + border: 2px solid var(--ssw-border); + border-top-color: var(--ssw-accent); + border-radius: 50%; + animation: ssw-spin 0.8s linear infinite; +} + +@keyframes ssw-spin { + to { + transform: rotate(360deg); + } +} + +.ssw-quote-left { + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-start; +} + +.ssw-quote-provider { + display: flex; + align-items: center; + gap: 10px; +} + +.ssw-quote-provider-icon { + width: 28px; + height: 28px; + border-radius: 8px; + object-fit: contain; + background: var(--ssw-bg-secondary); +} + +.ssw-quote-provider-icon-placeholder { + width: 28px; + height: 28px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; + color: white; +} + +.ssw-quote-provider-name { + font-size: 14px; + font-weight: 600; + color: var(--ssw-text-primary); +} + +.ssw-quote-best-tag { + padding: 3px 7px; + border-radius: 5px; + background: var(--ssw-accent); + color: white; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.ssw-quote-usd { + font-size: 13px; + color: var(--ssw-text-secondary); + padding-left: 38px; +} + +.ssw-quote-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; +} + +.ssw-quote-amount-row { + display: flex; + align-items: baseline; + gap: 5px; +} + +.ssw-quote-amount { + font-size: 17px; + font-weight: 600; + color: var(--ssw-text-primary); +} + +.ssw-quote-symbol { + font-size: 13px; + color: var(--ssw-text-secondary); + font-weight: 500; +} + +.ssw-quote-more { + display: flex; + align-items: center; + gap: 3px; + font-size: 12px; + color: var(--ssw-accent); + font-weight: 500; +} + +.ssw-quote-more svg { + opacity: 0.9; +} diff --git a/packages/swap-widget-poc/src/components/QuoteSelector.tsx b/packages/swap-widget-poc/src/components/QuoteSelector.tsx new file mode 100644 index 00000000000..cd3292e646a --- /dev/null +++ b/packages/swap-widget-poc/src/components/QuoteSelector.tsx @@ -0,0 +1,150 @@ +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"; + +type QuoteSelectorProps = { + rates: TradeRate[]; + selectedRate: TradeRate | null; + onSelectRate: (rate: TradeRate) => void; + buyAsset: Asset; + sellAsset: Asset; + sellAmountBaseUnit: string; + isLoading: boolean; + buyAssetUsdPrice?: string; +}; + +export const QuoteSelector = ({ + rates, + selectedRate, + onSelectRate, + buyAsset, + sellAsset, + sellAmountBaseUnit, + isLoading, + buyAssetUsdPrice, +}: QuoteSelectorProps) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + 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); + } + }, [rates.length]); + + const handleCloseModal = useCallback(() => { + setIsModalOpen(false); + }, []); + + const handleSelectRate = useCallback( + (rate: TradeRate) => { + onSelectRate(rate); + }, + [onSelectRate], + ); + + if (isLoading) { + return ( +
+
+
+ Finding best rates... +
+
+ ); + } + + if (!bestRate) { + 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, + ); + + return ( + <> + + + + + ); +}; diff --git a/packages/swap-widget-poc/src/components/QuotesModal.css b/packages/swap-widget-poc/src/components/QuotesModal.css new file mode 100644 index 00000000000..d985647df69 --- /dev/null +++ b/packages/swap-widget-poc/src/components/QuotesModal.css @@ -0,0 +1,244 @@ +.ssw-quotes-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; + animation: ssw-fade-in 0.15s ease; +} + +@keyframes ssw-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.ssw-quotes-modal { + width: 100%; + max-width: 400px; + max-height: 70vh; + background: var(--ssw-bg-secondary, #12121c); + border-radius: 20px; + border: 1px solid var(--ssw-border, rgba(255, 255, 255, 0.08)); + display: flex; + flex-direction: column; + overflow: hidden; + animation: ssw-slide-up 0.2s ease; +} + +@keyframes ssw-slide-up { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.ssw-quotes-modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 20px 20px 16px; + flex-shrink: 0; +} + +.ssw-quotes-header-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.ssw-quotes-modal-title { + font-size: 18px; + font-weight: 600; + color: var(--ssw-text-primary, #ffffff); + margin: 0; +} + +.ssw-quotes-modal-subtitle { + font-size: 13px; + color: var(--ssw-text-secondary, #a0a0b0); +} + +.ssw-quotes-modal-close { + padding: 6px; + border: none; + background: none; + border-radius: 8px; + color: var(--ssw-text-muted, #6b6b80); + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + margin: -2px -6px 0 0; +} + +.ssw-quotes-modal-close:hover { + color: var(--ssw-text-primary, #ffffff); + background: var(--ssw-bg-hover, rgba(255, 255, 255, 0.05)); +} + +.ssw-quotes-modal-list { + flex: 1; + overflow-y: auto; + padding: 0 12px 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.ssw-quotes-modal-list::-webkit-scrollbar { + width: 4px; +} + +.ssw-quotes-modal-list::-webkit-scrollbar-track { + background: transparent; +} + +.ssw-quotes-modal-list::-webkit-scrollbar-thumb { + background: var(--ssw-border, rgba(255, 255, 255, 0.08)); + border-radius: 2px; +} + +.ssw-quote-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border: 1px solid transparent; + border-radius: 14px; + background: var(--ssw-bg-tertiary, #1a1a2e); + cursor: pointer; + transition: all 0.15s ease; + text-align: left; + width: 100%; + gap: 12px; +} + +.ssw-quote-row:hover { + background: var(--ssw-bg-hover, rgba(255, 255, 255, 0.05)); +} + +.ssw-quote-row.ssw-best { + border-color: var(--ssw-accent, #3861fb); + background: rgba(56, 97, 251, 0.06); +} + +.ssw-quote-row.ssw-best:hover { + background: rgba(56, 97, 251, 0.1); +} + +.ssw-quote-row.ssw-selected { + border-color: var(--ssw-success, #00d395); + background: rgba(0, 211, 149, 0.06); +} + +.ssw-quote-row-left { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.ssw-quote-row-icon { + width: 36px; + height: 36px; + border-radius: 10px; + object-fit: contain; + background: var(--ssw-bg-secondary, #12121c); + flex-shrink: 0; +} + +.ssw-quote-row-icon-placeholder { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + font-weight: 600; + color: white; + flex-shrink: 0; +} + +.ssw-quote-row-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.ssw-quote-row-name-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.ssw-quote-row-name { + font-size: 14px; + font-weight: 600; + color: var(--ssw-text-primary, #ffffff); +} + +.ssw-quote-row-best { + padding: 2px 6px; + border-radius: 4px; + background: var(--ssw-accent, #3861fb); + color: white; + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.ssw-quote-row-diff { + padding: 2px 6px; + border-radius: 4px; + background: rgba(239, 68, 68, 0.12); + color: #ef4444; + font-size: 10px; + font-weight: 600; +} + +.ssw-quote-row-time { + font-size: 12px; + color: var(--ssw-text-muted, #6b6b80); +} + +.ssw-quote-row-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + flex-shrink: 0; +} + +.ssw-quote-row-amount { + font-size: 15px; + font-weight: 600; + color: var(--ssw-text-primary, #ffffff); +} + +.ssw-quote-row-symbol { + font-size: 13px; + font-weight: 500; + color: var(--ssw-text-secondary, #a0a0b0); +} + +.ssw-quote-row-usd { + font-size: 12px; + color: var(--ssw-text-muted, #6b6b80); +} diff --git a/packages/swap-widget-poc/src/components/QuotesModal.tsx b/packages/swap-widget-poc/src/components/QuotesModal.tsx new file mode 100644 index 00000000000..131ba479498 --- /dev/null +++ b/packages/swap-widget-poc/src/components/QuotesModal.tsx @@ -0,0 +1,158 @@ +import './QuotesModal.css' + +import { useCallback, useMemo } from 'react' + +import { getSwapperColor, getSwapperIcon } from '../constants/swappers' +import { formatUsdValue } from '../hooks/useMarketData' +import type { Asset, TradeRate } from '../types' +import { formatAmount } from '../types' + +type QuotesModalProps = { + 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 +} + +export const QuotesModal = ({ + isOpen, + onClose, + rates, + selectedRate, + onSelectRate, + buyAsset, + sellAsset, + sellAmountBaseUnit, + buyAssetUsdPrice, +}: QuotesModalProps) => { + const handleBackdropClick = useCallback( + (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose() + } + }, + [onClose], + ) + + const handleSelectRate = useCallback( + (rate: TradeRate) => { + onSelectRate(rate) + onClose() + }, + [onSelectRate, onClose], + ) + + const sortedRates = useMemo(() => { + return [...rates] + .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 bestRate = useMemo(() => sortedRates[0], [sortedRates]) + const bestBuyAmount = bestRate?.buyAmountCryptoBaseUnit ?? '0' + + if (!isOpen) return null + + return ( +
+
+
+
+

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 + + return ( + + ) + })} +
+
+
+ ) +} diff --git a/packages/swap-widget-poc/src/components/SettingsModal.css b/packages/swap-widget-poc/src/components/SettingsModal.css new file mode 100644 index 00000000000..2b059a14f93 --- /dev/null +++ b/packages/swap-widget-poc/src/components/SettingsModal.css @@ -0,0 +1,133 @@ +.ssw-settings-modal { + background: var(--ssw-bg-secondary); + border-radius: 16px; + width: 100%; + max-width: 400px; + overflow: hidden; + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2); +} + +.ssw-settings-content { + padding: 20px 24px 24px; +} + +.ssw-settings-section { + display: flex; + flex-direction: column; + gap: 12px; +} + +.ssw-settings-label { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 500; + color: var(--ssw-text-secondary); +} + +.ssw-info-btn { + background: none; + border: none; + padding: 2px; + cursor: pointer; + color: var(--ssw-text-muted); + border-radius: 50%; + transition: color 0.15s ease; +} + +.ssw-info-btn:hover { + color: var(--ssw-text-secondary); +} + +.ssw-slippage-options { + display: flex; + gap: 8px; +} + +.ssw-slippage-btn { + flex: 1; + padding: 10px 12px; + border: 1px solid var(--ssw-border); + border-radius: 10px; + background: var(--ssw-bg-tertiary); + color: var(--ssw-text-primary); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.ssw-slippage-btn:hover { + border-color: var(--ssw-accent); +} + +.ssw-slippage-btn.ssw-selected { + background: var(--ssw-accent-light); + border-color: var(--ssw-accent); + color: var(--ssw-accent); +} + +.ssw-slippage-custom { + flex: 1.2; + position: relative; + display: flex; + align-items: center; +} + +.ssw-slippage-custom input { + width: 100%; + padding: 10px 28px 10px 12px; + border: 1px solid var(--ssw-border); + border-radius: 10px; + background: var(--ssw-bg-tertiary); + color: var(--ssw-text-primary); + font-size: 14px; + font-weight: 500; + outline: none; + transition: border-color 0.15s ease; +} + +.ssw-slippage-custom input:focus { + border-color: var(--ssw-accent); +} + +.ssw-slippage-custom input::placeholder { + color: var(--ssw-text-muted); + font-weight: 400; +} + +.ssw-slippage-custom.ssw-selected input { + background: var(--ssw-accent-light); + border-color: var(--ssw-accent); +} + +.ssw-slippage-suffix { + position: absolute; + right: 12px; + color: var(--ssw-text-secondary); + font-size: 14px; + pointer-events: none; +} + +.ssw-slippage-warning { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 10px 12px; + border-radius: 10px; + background: rgba(255, 193, 7, 0.1); + color: #ffc107; + font-size: 13px; + line-height: 1.4; +} + +.ssw-slippage-warning svg { + flex-shrink: 0; + margin-top: 1px; +} + +.ssw-slippage-warning.ssw-error { + background: rgba(244, 67, 54, 0.1); + color: #f44336; +} diff --git a/packages/swap-widget-poc/src/components/SettingsModal.tsx b/packages/swap-widget-poc/src/components/SettingsModal.tsx new file mode 100644 index 00000000000..7c781839f0e --- /dev/null +++ b/packages/swap-widget-poc/src/components/SettingsModal.tsx @@ -0,0 +1,168 @@ +import { useState, useCallback } from "react"; +import "./SettingsModal.css"; + +const SLIPPAGE_PRESETS = ["0.1", "0.5", "1.0"]; + +type SettingsModalProps = { + isOpen: boolean; + onClose: () => void; + slippage: string; + onSlippageChange: (slippage: string) => void; +}; + +export const SettingsModal = ({ + isOpen, + onClose, + slippage, + onSlippageChange, +}: SettingsModalProps) => { + const [customSlippage, setCustomSlippage] = useState(""); + const [isCustom, setIsCustom] = useState( + !SLIPPAGE_PRESETS.includes(slippage), + ); + + const handlePresetClick = useCallback( + (preset: string) => { + 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; + + setCustomSlippage(formatted); + setIsCustom(true); + + const numValue = parseFloat(formatted); + if (!isNaN(numValue) && numValue > 0 && numValue <= 50) { + onSlippageChange(formatted); + } + }, + [onSlippageChange], + ); + + const handleBackdropClick = useCallback( + (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }, + [onClose], + ); + + if (!isOpen) return null; + + const currentSlippageNum = parseFloat(slippage); + const isHighSlippage = currentSlippageNum > 1; + const isVeryHighSlippage = currentSlippageNum > 5; + + return ( +
+
+
+

Settings

+ +
+ +
+
+
+ Slippage Tolerance + +
+ +
+ {SLIPPAGE_PRESETS.map((preset) => ( + + ))} +
+ handleCustomChange(e.target.value)} + onFocus={() => { + setIsCustom(true); + if (!customSlippage) setCustomSlippage(slippage); + }} + /> + % +
+
+ + {isHighSlippage && ( +
+ + + + + {isVeryHighSlippage + ? "Very high slippage. Your transaction may be frontrun." + : "High slippage may result in unfavorable rates."} + +
+ )} +
+
+
+
+ ); +}; diff --git a/packages/swap-widget-poc/src/components/SwapWidget.css b/packages/swap-widget-poc/src/components/SwapWidget.css new file mode 100644 index 00000000000..ad63d5cf01e --- /dev/null +++ b/packages/swap-widget-poc/src/components/SwapWidget.css @@ -0,0 +1,336 @@ +.ssw-widget { + --ssw-accent: #3861fb; + --ssw-accent-light: rgba(56, 97, 251, 0.1); + --ssw-success: #00d395; + --ssw-error: #f44336; + --ssw-warning: #ffc107; + + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + width: 100%; + max-width: 420px; + border-radius: 20px; + overflow: hidden; +} + +.ssw-widget *, +.ssw-widget *::before, +.ssw-widget *::after { + box-sizing: border-box; +} + +.ssw-widget.ssw-dark { + --ssw-bg-primary: #0a0a14; + --ssw-bg-secondary: #12121c; + --ssw-bg-tertiary: #1a1a2e; + --ssw-bg-input: #0d0d16; + --ssw-bg-hover: rgba(255, 255, 255, 0.05); + --ssw-border: rgba(255, 255, 255, 0.08); + --ssw-border-hover: rgba(255, 255, 255, 0.15); + --ssw-text-primary: #ffffff; + --ssw-text-secondary: #a0a0b0; + --ssw-text-muted: #6b6b80; + + background: var(--ssw-bg-secondary); + color: var(--ssw-text-primary); +} + +.ssw-widget.ssw-light { + --ssw-bg-primary: #ffffff; + --ssw-bg-secondary: #f8f9fc; + --ssw-bg-tertiary: #ffffff; + --ssw-bg-input: #f0f2f5; + --ssw-bg-hover: rgba(0, 0, 0, 0.04); + --ssw-border: rgba(0, 0, 0, 0.08); + --ssw-border-hover: rgba(0, 0, 0, 0.15); + --ssw-text-primary: #1a1a2e; + --ssw-text-secondary: #5c5c70; + --ssw-text-muted: #9090a0; + + background: var(--ssw-bg-secondary); + color: var(--ssw-text-primary); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); +} + +.ssw-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--ssw-border); +} + +.ssw-header-title { + font-size: 16px; + font-weight: 600; + color: var(--ssw-text-primary); +} + +.ssw-settings-btn { + padding: 8px; + border: none; + background: none; + border-radius: 10px; + color: var(--ssw-text-secondary); + cursor: pointer; + transition: all 0.15s ease; +} + +.ssw-settings-btn:hover { + color: var(--ssw-text-primary); + background: var(--ssw-bg-hover); +} + +.ssw-swap-container { + padding: 16px; + display: flex; + flex-direction: column; + gap: 4px; + position: relative; +} + +.ssw-token-section { + background: var(--ssw-bg-tertiary); + border-radius: 16px; + padding: 16px; + border: 1px solid var(--ssw-border); + transition: border-color 0.15s ease; +} + +.ssw-token-section:focus-within { + border-color: var(--ssw-accent); +} + +.ssw-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.ssw-section-label { + font-size: 13px; + font-weight: 500; + color: var(--ssw-text-secondary); +} + +.ssw-wallet-badge { + font-size: 12px; + color: var(--ssw-accent); + padding: 2px 8px; + background: var(--ssw-accent-light); + border-radius: 6px; +} + +.ssw-input-row { + display: flex; + align-items: center; + gap: 12px; +} + +.ssw-amount-input { + flex: 1; + background: none; + border: none; + font-size: 32px; + font-weight: 500; + color: var(--ssw-text-primary); + outline: none; + min-width: 0; +} + +.ssw-amount-input::placeholder { + color: var(--ssw-text-muted); +} + +.ssw-token-btn { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border: none; + background: var(--ssw-bg-secondary); + border-radius: 12px; + cursor: pointer; + transition: all 0.15s ease; + flex-shrink: 0; +} + +.ssw-token-btn:hover { + background: var(--ssw-bg-hover); +} + +.ssw-token-icon { + width: 32px; + height: 32px; + border-radius: 50%; + object-fit: contain; +} + +.ssw-token-icon-placeholder { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--ssw-accent); + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 600; + color: white; +} + +.ssw-token-info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; +} + +.ssw-token-symbol { + font-size: 15px; + font-weight: 600; + color: var(--ssw-text-primary); +} + +.ssw-token-chain { + font-size: 12px; + color: var(--ssw-text-secondary); +} + +.ssw-token-btn svg { + color: var(--ssw-text-muted); +} + +.ssw-section-footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 8px; +} + +.ssw-balance { + font-size: 12px; + color: var(--ssw-text-muted); +} + +.ssw-balance-skeleton { + width: 80px; + height: 14px; + border-radius: 4px; + background: linear-gradient( + 90deg, + var(--ssw-bg-hover) 25%, + var(--ssw-border) 50%, + var(--ssw-bg-hover) 75% + ); + background-size: 200% 100%; + animation: ssw-skeleton-shimmer 1.5s infinite; +} + +@keyframes ssw-skeleton-shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.ssw-usd-value { + font-size: 13px; + color: var(--ssw-text-muted); +} + +.ssw-swap-divider { + display: flex; + justify-content: center; + margin: -12px 0; + position: relative; + z-index: 10; +} + +.ssw-swap-btn { + width: 36px; + height: 36px; + border: 4px solid var(--ssw-bg-secondary); + background: var(--ssw-bg-tertiary); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: var(--ssw-text-secondary); + transition: all 0.15s ease; +} + +.ssw-swap-btn:hover { + color: var(--ssw-text-primary); + background: var(--ssw-bg-hover); +} + +.ssw-quotes { + padding: 0 16px 16px; +} + +.ssw-action-btn { + width: calc(100% - 32px); + margin: 0 16px 16px; + padding: 16px; + border: none; + border-radius: 14px; + background: var(--ssw-accent); + color: white; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; +} + +.ssw-action-btn:hover:not(:disabled) { + filter: brightness(1.1); +} + +.ssw-action-btn:disabled { + background: var(--ssw-bg-tertiary); + color: var(--ssw-text-muted); + cursor: not-allowed; +} + +.ssw-action-btn.ssw-secondary { + background: var(--ssw-bg-tertiary); + color: var(--ssw-text-primary); + border: 1px solid var(--ssw-border); +} + +.ssw-action-btn.ssw-secondary:hover:not(:disabled) { + background: var(--ssw-bg-hover); + filter: none; +} + +.ssw-powered-by { + padding: 12px 16px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + font-size: 12px; + color: var(--ssw-text-muted); + border-top: 1px solid var(--ssw-border); +} + +.ssw-powered-by a { + color: var(--ssw-accent); + text-decoration: none; + font-weight: 500; +} + +.ssw-powered-by-link { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.ssw-powered-by a:hover { + opacity: 0.8; +} diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget-poc/src/components/SwapWidget.tsx new file mode 100644 index 00000000000..cc246df317d --- /dev/null +++ b/packages/swap-widget-poc/src/components/SwapWidget.tsx @@ -0,0 +1,569 @@ +import { useState, useMemo, useCallback } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +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 { + getEvmChainIdNumber, + parseAmount, + formatAmount, + getChainType, + truncateAddress, +} from "../types"; +import { TokenSelectModal } from "./TokenSelectModal"; +import { SettingsModal } from "./SettingsModal"; +import { QuoteSelector } from "./QuoteSelector"; +import "./SwapWidget.css"; + +const DEFAULT_SELL_ASSET: Asset = { + 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", +}; + +const DEFAULT_BUY_ASSET: Asset = { + 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", +}; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + staleTime: 30_000, + }, + }, +}); + +type SwapWidgetInnerProps = SwapWidgetProps & { + apiClient: ReturnType; +}; + +const SwapWidgetInner = ({ + defaultSellAsset = DEFAULT_SELL_ASSET, + defaultBuyAsset = DEFAULT_BUY_ASSET, + disabledChainIds = [], + disabledAssetIds = [], + walletClient, + onConnectWallet, + onSwapSuccess, + onSwapError, + onAssetSelect, + theme = "dark", + defaultSlippage = "0.5", + showPoweredBy = true, + 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); + + const [tokenModalType, setTokenModalType] = useState<"sell" | "buy" | null>( + null, + ); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + + 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, sellAsset.precision], + ); + + const { + data: rates, + isLoading: isLoadingRates, + error: ratesError, + } = useSwapRates(apiClient, { + sellAssetId: sellAsset.assetId, + buyAssetId: buyAsset.assetId, + sellAmountCryptoBaseUnit: sellAmountBaseUnit, + enabled: !!sellAmountBaseUnit && sellAmountBaseUnit !== "0", + }); + + const walletAddress = useMemo(() => { + if (!walletClient) return undefined; + return (walletClient as WalletClient).account?.address; + }, [walletClient]); + + const isSellAssetEvm = getChainType(sellAsset.chainId) === "evm"; + const isBuyAssetEvm = getChainType(buyAsset.chainId) === "evm"; + const canExecuteDirectly = isSellAssetEvm && isBuyAssetEvm; + + const handleSwapTokens = useCallback(() => { + const tempSell = sellAsset; + setSellAsset(buyAsset); + setBuyAsset(tempSell); + setSellAmount(""); + setSelectedRate(null); + }, [sellAsset, buyAsset]); + + const handleSellAssetSelect = useCallback( + (asset: Asset) => { + setSellAsset(asset); + setSelectedRate(null); + onAssetSelect?.("sell", asset); + }, + [onAssetSelect], + ); + + const handleBuyAssetSelect = useCallback( + (asset: Asset) => { + setBuyAsset(asset); + setSelectedRate(null); + onAssetSelect?.("buy", asset); + }, + [onAssetSelect], + ); + + const handleExecuteSwap = useCallback(async () => { + 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; + } + + setIsExecuting(true); + + try { + const slippageDecimal = (parseFloat(slippage) / 100).toString(); + const quoteResponse = await apiClient.getQuote({ + sellAssetId: sellAsset.assetId, + buyAssetId: buyAsset.assetId, + sellAmountCryptoBaseUnit: sellAmountBaseUnit!, + receiveAddress: walletAddress, + swapperName: rateToUse.swapperName, + slippageTolerancePercentageDecimal: slippageDecimal, + }); + + if (!quoteResponse.transactionData) { + throw new Error("No transaction data returned"); + } + + const { to, data, value, gasLimit } = quoteResponse.transactionData; + + const txHash = await (walletClient as WalletClient).sendTransaction({ + to: to as `0x${string}`, + data: data as `0x${string}`, + value: BigInt(value), + gas: gasLimit ? BigInt(gasLimit) : undefined, + chain: { + id: getEvmChainIdNumber(sellAsset.chainId), + name: "Chain", + nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: [] } }, + }, + account: walletAddress as `0x${string}`, + }); + + onSwapSuccess?.(txHash); + } catch (error) { + onSwapError?.(error as Error); + } finally { + setIsExecuting(false); + } + }, [ + selectedRate, + rates, + walletClient, + walletAddress, + canExecuteDirectly, + sellAsset, + buyAsset, + sellAmount, + sellAmountBaseUnit, + slippage, + apiClient, + onSwapSuccess, + onSwapError, + ]); + + const handleButtonClick = useCallback(() => { + if (!walletClient && canExecuteDirectly && onConnectWallet) { + onConnectWallet(); + return; + } + handleExecuteSwap(); + }, [walletClient, canExecuteDirectly, onConnectWallet, handleExecuteSwap]); + + const buttonText = useMemo(() => { + if (!walletClient && canExecuteDirectly) return "Connect Wallet"; + if (!sellAmount) return "Enter an amount"; + 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, + sellAmount, + isLoadingRates, + ratesError, + rates, + isExecuting, + ]); + + const isButtonDisabled = useMemo(() => { + if (!sellAmount) return true; + if (isLoadingRates) return true; + if (ratesError) return true; + if (!rates?.length) return true; + if (isExecuting) return true; + return false; + }, [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 { data: sellAssetBalance, isLoading: isSellBalanceLoading } = + useAssetBalance(walletAddress, sellAsset.assetId, sellAsset.precision); + const { data: buyAssetBalance, isLoading: isBuyBalanceLoading } = + useAssetBalance(walletAddress, buyAsset.assetId, buyAsset.precision); + + 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 sellUsdValue = useMemo(() => { + 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]); + + const widgetStyle = useMemo(() => { + if (!themeConfig) return undefined; + const style: Record = {}; + if (themeConfig.accentColor) { + 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; + } + if (themeConfig.cardColor) { + style["--ssw-bg-tertiary"] = themeConfig.cardColor; + style["--ssw-bg-input"] = themeConfig.cardColor; + } + if (themeConfig.textColor) { + style["--ssw-text-primary"] = themeConfig.textColor; + } + if (themeConfig.borderRadius) { + style["--ssw-border-radius"] = themeConfig.borderRadius; + } + return Object.keys(style).length > 0 + ? (style as React.CSSProperties) + : undefined; + }, [themeConfig]); + + return ( +
+
+ Swap + +
+ +
+
+
+ Sell + {walletAddress && isSellAssetEvm && ( + + {truncateAddress(walletAddress)} + + )} +
+ +
+ { + setSellAmount(e.target.value.replace(/[^0-9.]/g, "")); + setSelectedRate(null); + }} + /> + +
+ +
+ {sellUsdValue} + {walletAddress && + (isSellBalanceLoading ? ( + + ) : sellAssetBalance ? ( + + Balance: {sellAssetBalance.balanceFormatted} + + ) : null)} +
+
+ +
+ +
+ +
+
+ Buy + {walletAddress && isBuyAssetEvm && ( + + {truncateAddress(walletAddress)} + + )} +
+ +
+ + +
+ +
+ {buyUsdValue} + {walletAddress && + (isBuyBalanceLoading ? ( + + ) : buyAssetBalance ? ( + + Balance: {buyAssetBalance.balanceFormatted} + + ) : null)} +
+
+
+ + {sellAmountBaseUnit && + sellAmountBaseUnit !== "0" && + (rates?.length || isLoadingRates) && ( +
+ +
+ )} + + + + {showPoweredBy && ( +
+ Powered by{" "} + + + + + ShapeShift + +
+ )} + + setTokenModalType(null)} + onSelect={ + tokenModalType === "sell" + ? handleSellAssetSelect + : handleBuyAssetSelect + } + disabledAssetIds={disabledAssetIds} + disabledChainIds={disabledChainIds} + walletAddress={walletAddress} + /> + + setIsSettingsOpen(false)} + slippage={slippage} + onSlippageChange={setSlippage} + /> +
+ ); +}; + +export const SwapWidget = (props: SwapWidgetProps) => { + const apiClient = useMemo( + () => createApiClient({ baseUrl: props.apiBaseUrl, apiKey: props.apiKey }), + [props.apiBaseUrl, props.apiKey], + ); + + return ( + + + + ); +}; diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.css b/packages/swap-widget-poc/src/components/TokenSelectModal.css new file mode 100644 index 00000000000..97d6a6decac --- /dev/null +++ b/packages/swap-widget-poc/src/components/TokenSelectModal.css @@ -0,0 +1,365 @@ +.ssw-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 16px; +} + +.ssw-modal { + background: var(--ssw-bg-secondary); + border-radius: 16px; + width: 100%; + max-width: 680px; + max-height: 80vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2); +} + +.ssw-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid var(--ssw-border); +} + +.ssw-modal-title { + font-size: 18px; + font-weight: 600; + color: var(--ssw-text-primary); + margin: 0; +} + +.ssw-modal-close { + background: none; + border: none; + padding: 8px; + cursor: pointer; + color: var(--ssw-text-secondary); + border-radius: 8px; + transition: all 0.15s ease; +} + +.ssw-modal-close:hover { + background: var(--ssw-bg-hover); + color: var(--ssw-text-primary); +} + +.ssw-modal-content { + display: flex; + flex: 1; + min-height: 0; +} + +.ssw-chain-sidebar { + width: 200px; + border-right: 1px solid var(--ssw-border); + display: flex; + flex-direction: column; + background: var(--ssw-bg-tertiary); +} + +.ssw-search-wrapper { + position: relative; + padding: 16px; +} + +.ssw-search-icon { + position: absolute; + left: 28px; + top: 50%; + transform: translateY(-50%); + color: var(--ssw-text-muted); + pointer-events: none; +} + +.ssw-chain-search, +.ssw-token-search { + width: 100%; + padding: 10px 12px 10px 36px; + border: 1px solid var(--ssw-border); + border-radius: 10px; + background: var(--ssw-bg-input); + color: var(--ssw-text-primary); + font-size: 14px; + outline: none; + transition: border-color 0.15s ease; +} + +.ssw-chain-search:focus, +.ssw-token-search:focus { + border-color: var(--ssw-accent); +} + +.ssw-chain-search::placeholder, +.ssw-token-search::placeholder { + color: var(--ssw-text-muted); +} + +.ssw-chain-list { + flex: 1; + overflow-y: auto; + padding: 0 8px 16px; +} + +.ssw-chain-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 12px; + border: none; + background: none; + border-radius: 10px; + cursor: pointer; + transition: background 0.15s ease; + text-align: left; +} + +.ssw-chain-item:hover { + background: var(--ssw-bg-hover); +} + +.ssw-chain-item.ssw-selected { + background: var(--ssw-accent-light); +} + +.ssw-chain-icon, +.ssw-chain-icon-placeholder, +.ssw-chain-icon-multi { + width: 28px; + height: 28px; + border-radius: 8px; + flex-shrink: 0; +} + +.ssw-chain-icon { + object-fit: contain; +} + +.ssw-chain-icon-placeholder { + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; + color: white; +} + +.ssw-chain-icon-multi { + display: flex; + align-items: center; + justify-content: center; + background: var(--ssw-bg-hover); + font-size: 14px; +} + +.ssw-chain-name { + font-size: 14px; + font-weight: 500; + color: var(--ssw-text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ssw-token-panel { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} + +.ssw-token-list { + flex: 1; + overflow-y: auto; + padding: 0 16px 16px; +} + +.ssw-token-item { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 12px; + border: none; + background: none; + border-radius: 12px; + cursor: pointer; + transition: background 0.15s ease; + text-align: left; +} + +.ssw-token-item:hover { + background: var(--ssw-bg-hover); +} + +.ssw-token-icon-wrapper { + position: relative; + width: 40px; + height: 40px; + flex-shrink: 0; +} + +.ssw-token-icon, +.ssw-token-icon-placeholder { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.ssw-token-icon { + object-fit: contain; +} + +.ssw-token-icon-placeholder { + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 600; + color: white; +} + +.ssw-token-chain-badge { + position: absolute; + bottom: -2px; + right: -2px; + width: 16px; + height: 16px; + border-radius: 4px; + border: 2px solid var(--ssw-bg-secondary); + background: var(--ssw-bg-secondary); +} + +.ssw-token-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.ssw-token-symbol { + font-size: 15px; + font-weight: 600; + color: var(--ssw-text-primary); +} + +.ssw-token-name { + font-size: 13px; + color: var(--ssw-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ssw-token-right { + margin-left: auto; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + flex-shrink: 0; +} + +.ssw-token-price { + font-size: 14px; + font-weight: 500; + color: var(--ssw-text-primary); +} + +.ssw-token-balance { + font-size: 13px; + font-weight: 400; + color: var(--ssw-text-secondary); +} + +.ssw-token-balance-skeleton { + width: 60px; + height: 14px; + border-radius: 4px; + background: linear-gradient( + 90deg, + var(--ssw-bg-hover) 25%, + var(--ssw-border) 50%, + var(--ssw-bg-hover) 75% + ); + background-size: 200% 100%; + animation: ssw-skeleton-shimmer 1.5s infinite; + flex-shrink: 0; +} + +@keyframes ssw-skeleton-shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.ssw-loading, +.ssw-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + padding: 48px 24px; + color: var(--ssw-text-secondary); + font-size: 14px; +} + +.ssw-spinner { + width: 24px; + height: 24px; + border: 2px solid var(--ssw-border); + border-top-color: var(--ssw-accent); + border-radius: 50%; + animation: ssw-spin 0.8s linear infinite; +} + +@keyframes ssw-spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 600px) { + .ssw-modal { + max-height: 90vh; + } + + .ssw-modal-content { + flex-direction: column; + } + + .ssw-chain-sidebar { + width: 100%; + border-right: none; + border-bottom: 1px solid var(--ssw-border); + max-height: 200px; + } + + .ssw-chain-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 0 16px 16px; + } + + .ssw-chain-item { + padding: 8px 12px; + } + + .ssw-chain-name { + font-size: 13px; + } +} diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx new file mode 100644 index 00000000000..a55aa85e10a --- /dev/null +++ b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx @@ -0,0 +1,353 @@ +import { useState, useMemo, useCallback } from "react"; +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"; + +type TokenSelectModalProps = { + isOpen: boolean; + onClose: () => void; + onSelect: (asset: Asset) => void; + disabledAssetIds?: string[]; + disabledChainIds?: ChainId[]; + walletAddress?: string; +}; + +const isNativeAsset = (assetId: string): boolean => { + 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(); + + let score = 0; + + 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 (isNativeAsset(asset.assetId)) score += 200; + + return score; +}; + +export const TokenSelectModal = ({ + isOpen, + onClose, + onSelect, + disabledAssetIds = [], + disabledChainIds = [], + walletAddress, +}: TokenSelectModalProps) => { + const [searchQuery, setSearchQuery] = useState(""); + const [chainSearchQuery, setChainSearchQuery] = useState(""); + const [selectedChainId, setSelectedChainId] = useState(null); + + const { data: allAssets, isLoading: isLoadingAssets } = useAssets(); + const { data: chains, isLoading: isLoadingChains } = useChains(); + + const chainInfoMap = useMemo(() => { + const map = new Map(); + for (const chain of chains) { + map.set(chain.chainId, chain); + } + return map; + }, [chains]); + + const filteredChains = useMemo(() => { + const enabledChains = chains.filter( + (chain) => !disabledChainIds.includes(chain.chainId), + ); + + if (!chainSearchQuery.trim()) return enabledChains; + + const lowerQuery = chainSearchQuery.toLowerCase(); + return enabledChains.filter((chain) => + chain.name.toLowerCase().includes(lowerQuery), + ); + }, [chains, chainSearchQuery, disabledChainIds]); + + const filteredAssets = useMemo(() => { + let assets = allAssets.filter( + (asset) => + !disabledAssetIds.includes(asset.assetId) && + !disabledChainIds.includes(asset.chainId), + ); + + if (selectedChainId) { + assets = assets.filter((asset) => asset.chainId === selectedChainId); + } + + if (!searchQuery.trim()) { + 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]; + } + + const lowerQuery = searchQuery.toLowerCase(); + return assets + .filter( + (asset) => + asset.symbol.toLowerCase().includes(lowerQuery) || + asset.name.toLowerCase().includes(lowerQuery) || + asset.assetId.toLowerCase().includes(lowerQuery), + ) + .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, + ]); + + const assetPrecisions = useMemo(() => { + const precisions: Record = {}; + for (const asset of filteredAssets) { + precisions[asset.assetId] = asset.precision; + } + return precisions; + }, [filteredAssets]); + + const assetIds = useMemo( + () => filteredAssets.map((a) => a.assetId), + [filteredAssets], + ); + + const { data: balances, loadingAssetIds } = useEvmBalances( + walletAddress, + assetIds, + assetPrecisions, + ); + + const { data: marketData } = useAllMarketData(); + + const handleAssetSelect = useCallback( + (asset: Asset) => { + onSelect(asset); + onClose(); + setSearchQuery(""); + setSelectedChainId(null); + }, + [onSelect, onClose], + ); + + const handleChainSelect = useCallback((chainId: ChainId | null) => { + setSelectedChainId(chainId); + }, []); + + const handleBackdropClick = useCallback( + (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }, + [onClose], + ); + + if (!isOpen) return null; + + const isLoading = isLoadingAssets || isLoadingChains; + + return ( +
+
+
+

Select Token

+ +
+ +
+
+
+ + + + + setChainSearchQuery(e.target.value)} + /> +
+ +
+ + + {filteredChains.map((chain) => ( + + ))} +
+
+ +
+
+ + + + + setSearchQuery(e.target.value)} + autoFocus + /> +
+ +
+ {isLoading ? ( +
+
+ Loading assets... +
+ ) : filteredAssets.length === 0 ? ( +
No tokens found
+ ) : ( + filteredAssets.map((asset) => { + const chainInfo = chainInfoMap.get(asset.chainId); + const balance = balances?.[asset.assetId]; + return ( + + ); + }) + )} +
+
+
+
+
+ ); +}; diff --git a/packages/swap-widget-poc/src/constants/chains.ts b/packages/swap-widget-poc/src/constants/chains.ts new file mode 100644 index 00000000000..45114fe21d6 --- /dev/null +++ b/packages/swap-widget-poc/src/constants/chains.ts @@ -0,0 +1,147 @@ +import type { ChainId } from '../types' + +export type ChainMeta = { + chainId: ChainId + name: string + shortName: string + color: string + icon: string +} + +export const CHAIN_METADATA: Record = { + 'eip155:1': { + chainId: 'eip155:1', + name: 'Ethereum', + shortName: 'ETH', + color: '#627EEA', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/ethereum/info/logo.png', + }, + 'eip155:42161': { + chainId: 'eip155:42161', + name: 'Arbitrum One', + shortName: 'ARB', + color: '#2D374B', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/arbitrum/info/logo.png', + }, + 'eip155:42170': { + chainId: 'eip155:42170', + name: 'Arbitrum Nova', + shortName: 'NOVA', + color: '#E57310', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrumnova/info/logo.png', + }, + 'eip155:10': { + chainId: 'eip155:10', + name: 'Optimism', + shortName: 'OP', + color: '#FF0420', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/optimism/info/logo.png', + }, + 'eip155:137': { + chainId: 'eip155:137', + name: 'Polygon', + shortName: 'MATIC', + color: '#8247E5', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/polygon/info/logo.png', + }, + 'eip155:8453': { + chainId: 'eip155:8453', + name: 'Base', + shortName: 'BASE', + color: '#0052FF', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/base/info/logo.png', + }, + 'eip155:43114': { + chainId: 'eip155:43114', + name: 'Avalanche', + shortName: 'AVAX', + color: '#E84142', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/avalanchec/info/logo.png', + }, + 'eip155:56': { + chainId: 'eip155:56', + name: 'BNB Smart Chain', + shortName: 'BNB', + color: '#F0B90B', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/binance/info/logo.png', + }, + 'eip155:100': { + chainId: 'eip155:100', + name: 'Gnosis', + shortName: 'GNO', + color: '#04795B', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/xdai/info/logo.png', + }, + 'bip122:000000000019d6689c085ae165831e93': { + chainId: 'bip122:000000000019d6689c085ae165831e93', + name: 'Bitcoin', + shortName: 'BTC', + color: '#FF9800', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/bitcoin/info/logo.png', + }, + 'bip122:000000000000000000651ef99cb9fcbe': { + chainId: 'bip122:000000000000000000651ef99cb9fcbe', + name: 'Bitcoin Cash', + shortName: 'BCH', + color: '#8BC34A', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/bitcoincash/info/logo.png', + }, + 'bip122:00000000001a91e3dace36e2be3bf030': { + chainId: 'bip122:00000000001a91e3dace36e2be3bf030', + name: 'Dogecoin', + shortName: 'DOGE', + color: '#FFC107', + icon: 'https://assets.coingecko.com/coins/images/5/large/dogecoin.png', + }, + 'bip122:12a765e31ffd4059bada1e25190f6e98': { + chainId: 'bip122:12a765e31ffd4059bada1e25190f6e98', + name: 'Litecoin', + shortName: 'LTC', + color: '#B8B8B8', + icon: 'https://rawcdn.githack.com/trustwallet/assets/32e51d582a890b3dd3135fe3ee7c20c2fd699a6d/blockchains/litecoin/info/logo.png', + }, + 'cosmos:cosmoshub-4': { + chainId: 'cosmos:cosmoshub-4', + name: 'Cosmos Hub', + shortName: 'ATOM', + color: '#303F9F', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/cosmos/info/logo.png', + }, + 'cosmos:thorchain-1': { + chainId: 'cosmos:thorchain-1', + name: 'THORChain', + shortName: 'RUNE', + color: '#33FF99', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/thorchain/info/logo.png', + }, + 'cosmos:mayachain-mainnet-v1': { + chainId: 'cosmos:mayachain-mainnet-v1', + name: 'MAYAChain', + shortName: 'CACAO', + color: '#63FDD9', + icon: 'https://raw.githubusercontent.com/shapeshift/web/develop/src/assets/mayachain.png', + }, + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'Solana', + shortName: 'SOL', + color: '#9945FF', + icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/solana/info/logo.png', + }, +} + +export const getChainMeta = (chainId: ChainId): ChainMeta | undefined => { + return CHAIN_METADATA[chainId] +} + +export const getChainName = (chainId: ChainId): string => { + return CHAIN_METADATA[chainId]?.name ?? chainId +} + +export const getChainIcon = (chainId: ChainId): string | undefined => { + return CHAIN_METADATA[chainId]?.icon +} + +export const getChainColor = (chainId: ChainId): string => { + return CHAIN_METADATA[chainId]?.color ?? '#888888' +} diff --git a/packages/swap-widget-poc/src/constants/swappers.ts b/packages/swap-widget-poc/src/constants/swappers.ts new file mode 100644 index 00000000000..4515876b336 --- /dev/null +++ b/packages/swap-widget-poc/src/constants/swappers.ts @@ -0,0 +1,52 @@ +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", + 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", + Portals: + "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", + Relay: + "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", + 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", + ButterSwap: + "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", +}; + +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", +}; + +export const getSwapperIcon = ( + swapperName: SwapperName, +): string | undefined => { + return SWAPPER_ICONS[swapperName]; +}; + +export const getSwapperColor = (swapperName: SwapperName): string => { + return SWAPPER_COLORS[swapperName] ?? "#6366F1"; +}; diff --git a/packages/swap-widget-poc/src/demo/App.css b/packages/swap-widget-poc/src/demo/App.css new file mode 100644 index 00000000000..b9dfb20f70e --- /dev/null +++ b/packages/swap-widget-poc/src/demo/App.css @@ -0,0 +1,454 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body, +#root { + width: 100%; + min-height: 100vh; +} + +.demo-app { + width: 100%; + min-height: 100vh; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + display: flex; + flex-direction: column; +} + +.demo-app.dark { + --demo-bg: var(--demo-page-bg, #09090f); + --demo-bg-secondary: color-mix( + in srgb, + var(--demo-page-bg, #09090f), + white 5% + ); + --demo-text: #ffffff; + --demo-text-secondary: #a0a0b0; + --demo-text-muted: #6b6b80; + --demo-border: rgba(255, 255, 255, 0.08); + --demo-accent: var(--demo-page-accent, #3861fb); + + background: var(--demo-bg); + background-image: radial-gradient( + ellipse 80% 50% at 50% -20%, + color-mix(in srgb, var(--demo-page-accent, #3861fb), transparent 88%), + transparent + ), + radial-gradient( + ellipse 60% 40% at 80% 100%, + color-mix(in srgb, var(--demo-page-accent, #6366f1), transparent 92%), + transparent + ); + color: var(--demo-text); +} + +.demo-app.light { + --demo-bg: #f5f5f7; + --demo-bg-secondary: #ffffff; + --demo-text: #1a1a2e; + --demo-text-secondary: #5c5c70; + --demo-text-muted: #9090a0; + --demo-border: rgba(0, 0, 0, 0.08); + --demo-accent: #3861fb; + + background: var(--demo-bg); + color: var(--demo-text); +} + +.demo-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + border-bottom: 1px solid var(--demo-border); + background: var(--demo-bg-secondary); + position: sticky; + top: 0; + z-index: 100; +} + +.demo-logo { + display: flex; + align-items: center; + gap: 10px; + color: var(--demo-text); + text-decoration: none; + transition: opacity 0.15s ease; +} + +.demo-logo:hover { + opacity: 0.8; +} + +.demo-logo-text { + font-size: 17px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.demo-header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.demo-customize-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 14px; + border: 1px solid var(--demo-border); + border-radius: 12px; + background: transparent; + color: var(--demo-text-secondary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.demo-customize-btn:hover { + border-color: var(--demo-accent); + color: var(--demo-text); +} + +.demo-main { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 48px 24px; +} + +.demo-content { + width: 100%; + max-width: 1100px; + display: flex; + flex-direction: column; + align-items: center; + gap: 40px; +} + +.demo-hero { + text-align: center; + max-width: 500px; +} + +.demo-title { + font-size: 42px; + font-weight: 700; + letter-spacing: -0.03em; + margin-bottom: 12px; + background: linear-gradient(135deg, var(--demo-text) 30%, var(--demo-accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.demo-subtitle { + font-size: 16px; + color: var(--demo-text-secondary); + line-height: 1.5; +} + +.demo-layout { + display: flex; + gap: 24px; + align-items: flex-start; + justify-content: center; + width: 100%; +} + +.demo-customizer { + width: 280px; + padding: 20px; + background: var(--demo-bg-secondary); + border: 1px solid var(--demo-border); + border-radius: 16px; + flex-shrink: 0; +} + +.demo-customizer-title { + font-size: 15px; + font-weight: 600; + color: var(--demo-text); + margin-bottom: 20px; +} + +.demo-customizer-section { + margin-bottom: 20px; +} + +.demo-customizer-section:last-child { + margin-bottom: 0; +} + +.demo-customizer-label { + display: block; + font-size: 12px; + font-weight: 500; + color: var(--demo-text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 10px; +} + +.demo-theme-toggle { + display: flex; + gap: 8px; +} + +.demo-theme-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 10px; + border: 1px solid var(--demo-border); + border-radius: 10px; + background: transparent; + color: var(--demo-text-secondary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.demo-theme-btn:hover { + border-color: var(--demo-text-muted); +} + +.demo-theme-btn.active { + border-color: var(--demo-accent); + background: color-mix(in srgb, var(--demo-accent), transparent 90%); + color: var(--demo-accent); +} + +.demo-color-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 8px; +} + +.demo-color-btn { + width: 100%; + aspect-ratio: 1; + border: 2px solid transparent; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease; +} + +.demo-color-btn:hover { + transform: scale(1.1); +} + +.demo-color-btn.active { + border-color: var(--demo-text); + box-shadow: 0 0 0 2px var(--demo-bg); +} + +.demo-preset-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; +} + +.demo-preset-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 8px; + border: 1px solid var(--demo-border); + border-radius: 10px; + background: transparent; + cursor: pointer; + transition: all 0.15s ease; +} + +.demo-preset-btn:hover { + border-color: var(--demo-accent); + transform: translateY(-1px); +} + +.demo-preset-preview { + width: 100%; + aspect-ratio: 1.2; + border-radius: 6px; + position: relative; + overflow: hidden; +} + +.demo-preset-card { + position: absolute; + inset: 20%; + border-radius: 4px; +} + +.demo-preset-accent { + position: absolute; + bottom: 15%; + left: 25%; + right: 25%; + height: 12%; + border-radius: 2px; +} + +.demo-preset-name { + font-size: 10px; + font-weight: 500; + color: var(--demo-text-secondary); +} + +.demo-color-input-row { + display: flex; + gap: 8px; + align-items: center; +} + +.demo-color-picker { + width: 40px; + height: 40px; + padding: 0; + border: 1px solid var(--demo-border); + border-radius: 8px; + cursor: pointer; + background: transparent; +} + +.demo-color-picker::-webkit-color-swatch-wrapper { + padding: 2px; +} + +.demo-color-picker::-webkit-color-swatch { + border-radius: 6px; + border: none; +} + +.demo-color-text { + flex: 1; + padding: 10px 12px; + border: 1px solid var(--demo-border); + border-radius: 8px; + background: var(--demo-bg); + color: var(--demo-text); + font-size: 13px; + font-family: monospace; +} + +.demo-color-text:focus { + outline: none; + border-color: var(--demo-accent); +} + +.demo-copy-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + padding: 12px 16px; + border: 1px solid var(--demo-accent); + border-radius: 10px; + background: color-mix(in srgb, var(--demo-accent), transparent 90%); + color: var(--demo-accent); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; +} + +.demo-copy-btn:hover { + background: color-mix(in srgb, var(--demo-accent), transparent 80%); +} + +.demo-copy-btn:active { + transform: scale(0.98); +} + +.demo-connection-info { + display: flex; + flex-direction: column; + gap: 6px; +} + +.demo-connected-badge { + display: inline-flex; + align-items: center; + padding: 4px 8px; + background: rgba(16, 185, 129, 0.1); + color: #10b981; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + width: fit-content; +} + +.demo-address { + font-size: 13px; + color: var(--demo-text-secondary); + font-family: monospace; +} + +.demo-disconnected { + font-size: 13px; + color: var(--demo-text-muted); +} + +.demo-widget-container { + display: flex; + justify-content: center; +} + +.demo-footer { + padding: 20px; + text-align: center; + border-top: 1px solid var(--demo-border); + color: var(--demo-text-muted); + font-size: 13px; +} + +@media (max-width: 768px) { + .demo-header { + padding: 12px 16px; + flex-wrap: wrap; + gap: 12px; + } + + .demo-customize-btn span { + display: none; + } + + .demo-customize-btn { + padding: 10px; + } + + .demo-title { + font-size: 32px; + } + + .demo-subtitle { + font-size: 14px; + } + + .demo-layout { + flex-direction: column; + align-items: center; + } + + .demo-customizer { + width: 100%; + max-width: 420px; + } + + .demo-main { + padding: 32px 16px; + } +} diff --git a/packages/swap-widget-poc/src/demo/App.tsx b/packages/swap-widget-poc/src/demo/App.tsx new file mode 100644 index 00000000000..7b2fa30cc16 --- /dev/null +++ b/packages/swap-widget-poc/src/demo/App.tsx @@ -0,0 +1,466 @@ +import { useState, useMemo, useCallback } from "react"; +import { + RainbowKitProvider, + ConnectButton, + getDefaultConfig, + darkTheme, + lightTheme, +} from "@rainbow-me/rainbowkit"; +import { WagmiProvider, useAccount, useWalletClient } from "wagmi"; +import { mainnet, polygon, arbitrum, optimism, base } from "wagmi/chains"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { SwapWidget } from "../components/SwapWidget"; +import type { ThemeConfig } from "../types"; +import "@rainbow-me/rainbowkit/styles.css"; +import "./App.css"; + +const config = getDefaultConfig({ + appName: "ShapeShift Swap Widget", + projectId: "demo-project-id", + chains: [mainnet, polygon, arbitrum, optimism, base], + ssr: false, +}); + +const queryClient = new QueryClient(); + +type ThemeColors = { + bg: string; + card: string; + accent: string; +}; + +const THEME_PRESETS: Array<{ + name: string; + dark: ThemeColors; + light: ThemeColors; +}> = [ + { + name: "Blue", + dark: { bg: "#0a0a14", card: "#12121c", accent: "#3861fb" }, + light: { bg: "#f8f9fc", card: "#ffffff", accent: "#3861fb" }, + }, + { + name: "Rose", + dark: { bg: "#140a0f", card: "#1c1218", accent: "#f43f5e" }, + light: { bg: "#fef2f4", card: "#ffffff", accent: "#f43f5e" }, + }, + { + name: "Purple", + dark: { bg: "#0e0a14", card: "#1a1424", accent: "#a855f7" }, + light: { bg: "#faf5ff", card: "#ffffff", accent: "#a855f7" }, + }, + { + name: "Cyan", + dark: { bg: "#0a1214", card: "#141d20", accent: "#06b6d4" }, + light: { bg: "#f0fdff", card: "#ffffff", accent: "#06b6d4" }, + }, + { + name: "Green", + dark: { bg: "#0a140e", card: "#141c18", accent: "#10b981" }, + light: { bg: "#f0fdf6", card: "#ffffff", accent: "#10b981" }, + }, + { + name: "Orange", + dark: { bg: "#14100a", card: "#1c1814", accent: "#f97316" }, + light: { bg: "#fff8f3", card: "#ffffff", accent: "#f97316" }, + }, +]; + +const DemoContent = () => { + const { address, isConnected } = useAccount(); + const { data: walletClient } = useWalletClient(); + const [theme, setTheme] = useState<"light" | "dark">("dark"); + const [showCustomizer, setShowCustomizer] = useState(true); + + const [darkColors, setDarkColors] = useState({ + bg: "#0a0a14", + card: "#12121c", + accent: "#3861fb", + }); + + const [lightColors, setLightColors] = useState({ + bg: "#f8f9fc", + card: "#ffffff", + accent: "#3861fb", + }); + + const currentColors = theme === "dark" ? darkColors : lightColors; + const setCurrentColors = theme === "dark" ? setDarkColors : setLightColors; + + const themeConfig: ThemeConfig = useMemo( + () => ({ + mode: theme, + accentColor: currentColors.accent, + backgroundColor: currentColors.bg, + cardColor: currentColors.card, + }), + [theme, currentColors], + ); + + const applyPreset = (preset: (typeof THEME_PRESETS)[0]) => { + setDarkColors(preset.dark); + setLightColors(preset.light); + }; + + const [copied, setCopied] = useState(false); + + const copyConfig = useCallback(() => { + const code = `const themeConfig = { + dark: { + mode: "dark", + backgroundColor: "${darkColors.bg}", + cardColor: "${darkColors.card}", + accentColor: "${darkColors.accent}", + }, + light: { + mode: "light", + backgroundColor: "${lightColors.bg}", + cardColor: "${lightColors.card}", + accentColor: "${lightColors.accent}", + }, +}; + +// Usage: + +// or +`; + + navigator.clipboard.writeText(code).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, [darkColors, lightColors]); + + const handleSwapSuccess = (txHash: string) => { + console.log("Swap successful:", txHash); + alert(`Swap successful! TxHash: ${txHash}`); + }; + + const handleSwapError = (error: Error) => { + console.error("Swap failed:", error); + alert(`Swap failed: ${error.message}`); + }; + + const demoStyle = useMemo( + () => + ({ + "--demo-page-bg": currentColors.bg, + "--demo-page-accent": currentColors.accent, + }) as React.CSSProperties, + [currentColors], + ); + + return ( +
+
+ + + + + ShapeShift + + +
+ + +
+
+ +
+
+
+

Swap Widget

+

+ Embeddable multi-chain swap widget powered by ShapeShift +

+
+ +
+ {showCustomizer && ( +
+

Customize Widget

+ +
+ +
+ {THEME_PRESETS.map((preset) => { + const previewColors = + theme === "dark" ? preset.dark : preset.light; + return ( + + ); + })} +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + setCurrentColors((c) => ({ ...c, bg: e.target.value })) + } + className="demo-color-picker" + /> + + setCurrentColors((c) => ({ ...c, bg: e.target.value })) + } + className="demo-color-text" + /> +
+
+ +
+ +
+ + setCurrentColors((c) => ({ + ...c, + card: e.target.value, + })) + } + className="demo-color-picker" + /> + + setCurrentColors((c) => ({ + ...c, + card: e.target.value, + })) + } + className="demo-color-text" + /> +
+
+ +
+ +
+ + setCurrentColors((c) => ({ + ...c, + accent: e.target.value, + })) + } + className="demo-color-picker" + /> + + setCurrentColors((c) => ({ + ...c, + accent: e.target.value, + })) + } + className="demo-color-text" + /> +
+
+ +
+ +
+ {isConnected ? ( + <> + Connected + + {address?.slice(0, 6)}...{address?.slice(-4)} + + + ) : ( + Not connected + )} +
+
+ +
+ +
+
+ )} + +
+ +
+
+
+
+ +
+

Built with ❤️ by ShapeShift DAO

+
+
+ ); +}; + +export const App = () => { + return ( + + + + + + + + ); +}; diff --git a/packages/swap-widget-poc/src/demo/main.tsx b/packages/swap-widget-poc/src/demo/main.tsx new file mode 100644 index 00000000000..a49a4804fb1 --- /dev/null +++ b/packages/swap-widget-poc/src/demo/main.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { App } from "./App"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + refetchOnWindowFocus: false, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/packages/swap-widget-poc/src/hooks/useAssets.ts b/packages/swap-widget-poc/src/hooks/useAssets.ts new file mode 100644 index 00000000000..2682ca3d3d1 --- /dev/null +++ b/packages/swap-widget-poc/src/hooks/useAssets.ts @@ -0,0 +1,194 @@ +import { useQuery } from "@tanstack/react-query"; +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; +}; + +type RawAssetData = { + byId: Record; + ids: AssetId[]; +}; + +const fetchAssetManifest = async (): Promise => { + const response = await fetch( + `${SHAPESHIFT_ASSET_CDN}/generated/asset-manifest.json`, + ); + if (!response.ok) { + return { assetData: Date.now().toString(), relatedAssetIndex: "" }; + } + return response.json(); +}; + +const fetchAssetData = async (): Promise => { + 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"); + } + + return response.json(); +}; + +export const useAssetData = () => { + return useQuery({ + queryKey: ["assetData"], + queryFn: fetchAssetData, + staleTime: ASSET_QUERY_STALE_TIME, + gcTime: 30 * 60 * 1000, + }); +}; + +export const useAssets = () => { + const { data, ...rest } = useAssetData(); + + const assets = data + ? data.ids.map((id) => data.byId[id]).filter(Boolean) + : []; + + return { data: assets, ...rest }; +}; + +export const useAssetsById = () => { + const { data, ...rest } = useAssetData(); + return { data: data?.byId ?? {}, ...rest }; +}; + +export const useAssetById = (assetId: AssetId | undefined) => { + 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; +}; + +export const useChains = () => { + const { data: assets, ...rest } = useAssets(); + + const chains = (() => { + if (!assets.length) return []; + + const chainMap = new Map(); + + for (const asset of assets) { + if (chainMap.has(asset.chainId)) continue; + + 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, + nativeAsset: asset, + }); + } + } + + return Array.from(chainMap.values()).sort((a, b) => + a.name.localeCompare(b.name), + ); + })(); + + 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 }; +}; + +export const useAssetsByChainId = (chainId: ChainId | undefined) => { + const { data: assets, ...rest } = useAssets(); + + const filteredAssets = chainId + ? assets.filter((asset) => asset.chainId === chainId) + : assets; + + return { data: filteredAssets, ...rest }; +}; + +const isNativeAsset = (assetId: string): boolean => { + 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(); + + let score = 0; + + 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 (isNativeAsset(asset.assetId)) score += 200; + + return score; +}; + +export const useAssetSearch = (query: string, chainId?: ChainId) => { + const { data: assets, ...rest } = useAssets(); + + const searchResults = (() => { + 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 lowerQuery = query.toLowerCase(); + + const matched = filtered + .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) })) + .sort((a, b) => b.score - a.score) + .slice(0, 100) + .map((item) => item.asset); + + return matched; + })(); + + return { data: searchResults, ...rest }; +}; diff --git a/packages/swap-widget-poc/src/hooks/useBalances.ts b/packages/swap-widget-poc/src/hooks/useBalances.ts new file mode 100644 index 00000000000..77c9de48cb2 --- /dev/null +++ b/packages/swap-widget-poc/src/hooks/useBalances.ts @@ -0,0 +1,216 @@ +import { useBalance, useReadContracts } from "wagmi"; +import { useMemo } from "react"; +import { useQueries } from "@tanstack/react-query"; +import { getBalance } from "@wagmi/core"; +import { useConfig } from "wagmi"; +import type { AssetId } from "../types"; +import { formatAmount, getEvmChainIdNumber } from "../types"; +import { erc20Abi } from "viem"; + +type BalanceResult = { + assetId: AssetId; + balance: string; + balanceFormatted: string; +}; + +type BalancesMap = Record; + +const parseAssetId = ( + assetId: AssetId, +): { chainId: number; tokenAddress?: `0x${string}` } | null => { + const [chainPart, assetPart] = assetId.split("/"); + + if (!chainPart?.startsWith("eip155:")) return null; + + const chainId = getEvmChainIdNumber(chainPart); + + if (!assetPart) return { chainId }; + + if (assetPart.startsWith("erc20:")) { + const tokenAddress = assetPart.replace("erc20:", "") as `0x${string}`; + return { chainId, tokenAddress }; + } + + if (assetPart.startsWith("slip44:")) { + 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 { data: nativeBalance } = useBalance({ + address: address as `0x${string}` | undefined, + chainId: isNative ? parsed.chainId : undefined, + query: { + enabled: !!address && !!isNative, + }, + }); + + const { data: erc20Balance } = useBalance({ + address: address as `0x${string}` | undefined, + chainId: isErc20 ? parsed.chainId : undefined, + token: isErc20 ? parsed.tokenAddress : undefined, + query: { + enabled: !!address && !!isErc20, + }, + }); + + const balance = isNative ? nativeBalance : isErc20 ? erc20Balance : undefined; + + return useMemo(() => { + if (!balance || !assetId) { + return { data: undefined, isLoading: false }; + } + + return { + data: { + assetId, + balance: balance.value.toString(), + balanceFormatted: formatAmount(balance.value.toString(), precision), + }, + isLoading: false, + }; + }, [balance, assetId, precision]); +}; + +export const useEvmBalances = ( + address: string | undefined, + assetIds: AssetId[], + assetPrecisions: Record, +) => { + const config = useConfig(); + + const parsedAssets = useMemo(() => { + return assetIds + .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], + ); + + const nativeQueries = useQueries({ + queries: nativeAssets.map((asset) => ({ + 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; + } + }, + enabled: !!address, + staleTime: 30_000, + })), + }); + + 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 balances = useMemo((): BalancesMap => { + const result: BalancesMap = {}; + + nativeQueries.forEach((query) => { + if (query.data) { + const { assetId, balance, precision } = query.data; + result[assetId] = { + assetId, + balance, + balanceFormatted: formatAmount(balance, precision), + }; + } + }); + + 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), + }; + } + }); + } + + return result; + }, [nativeQueries, erc20Results, erc20Assets]); + + const isLoading = nativeQueries.some((q) => q.isLoading) || isErc20Loading; + + const loadingAssetIds = useMemo(() => { + const loading = new Set(); + nativeQueries.forEach((query, index) => { + if (query.isLoading) { + loading.add(nativeAssets[index].assetId); + } + }); + if (isErc20Loading) { + erc20Assets.forEach((asset) => { + loading.add(asset.assetId); + }); + } + return loading; + }, [nativeQueries, nativeAssets, isErc20Loading, erc20Assets]); + + 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 new file mode 100644 index 00000000000..da87ffc1aee --- /dev/null +++ b/packages/swap-widget-poc/src/hooks/useMarketData.ts @@ -0,0 +1,118 @@ +import { useQuery } from "@tanstack/react-query"; +import { adapters } from "@shapeshiftoss/caip"; +import type { AssetId } from "../types"; + +const MARKET_DATA_STALE_TIME = 5 * 60 * 1000; +const COINGECKO_API_URL = "https://api.coingecko.com/api/v3"; + +type MarketData = { + price: string; + marketCap: string; + volume: string; + changePercent24Hr: number; +}; + +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 fetchAllMarketData = async (): Promise => { + const result: MarketDataById = {}; + const maxPerPage = 250; + const totalPages = 4; + + try { + const allData = await Promise.all( + Array.from({ length: totalPages }, (_, i) => i + 1).map(async (page) => { + const response = await fetch( + `${COINGECKO_API_URL}/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=${maxPerPage}&page=${page}&sparkline=false`, + ); + if (!response.ok) return []; + return response.json() as Promise; + }), + ); + + const flatData = allData.flat(); + + for (const asset of flatData) { + 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", + changePercent24Hr: asset.price_change_percentage_24h ?? 0, + }; + + for (const assetId of assetIds) { + result[assetId] = marketData; + } + } + } catch (error) { + console.error("Failed to fetch market data:", error); + } + + return result; +}; + +export const useAllMarketData = () => { + return useQuery({ + queryKey: ["allMarketData"], + queryFn: fetchAllMarketData, + staleTime: MARKET_DATA_STALE_TIME, + gcTime: 30 * 60 * 1000, + }); +}; + +export const useMarketData = (assetIds: AssetId[]) => { + const { data: allMarketData, ...rest } = useAllMarketData(); + + const filteredData = (() => { + if (!allMarketData) return {}; + + const result: MarketDataById = {}; + for (const assetId of assetIds) { + if (allMarketData[assetId]) { + result[assetId] = allMarketData[assetId]; + } + } + return result; + })(); + + return { data: filteredData, ...rest }; +}; + +export const useAssetPrice = (assetId: AssetId | undefined) => { + 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"; + + 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`; +}; + +export type { MarketData }; diff --git a/packages/swap-widget-poc/src/hooks/useSwapQuote.ts b/packages/swap-widget-poc/src/hooks/useSwapQuote.ts new file mode 100644 index 00000000000..fb5b0af1e32 --- /dev/null +++ b/packages/swap-widget-poc/src/hooks/useSwapQuote.ts @@ -0,0 +1,66 @@ +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; + receiveAddress: string | undefined; + swapperName: SwapperName | undefined; + slippageTolerancePercentageDecimal?: string; + enabled?: boolean; +}; + +export const useSwapQuote = ( + apiClient: ApiClient, + params: UseSwapQuoteParams, +) => { + const { + sellAssetId, + buyAssetId, + sellAmountCryptoBaseUnit, + receiveAddress, + swapperName, + slippageTolerancePercentageDecimal, + enabled = true, + } = params; + + return useQuery({ + queryKey: [ + "swapQuote", + sellAssetId, + buyAssetId, + sellAmountCryptoBaseUnit, + receiveAddress, + swapperName, + ], + queryFn: async (): Promise => { + if ( + !sellAssetId || + !buyAssetId || + !sellAmountCryptoBaseUnit || + !receiveAddress || + !swapperName + ) { + return null; + } + return apiClient.getQuote({ + sellAssetId, + buyAssetId, + sellAmountCryptoBaseUnit, + receiveAddress, + swapperName, + slippageTolerancePercentageDecimal, + }); + }, + enabled: + enabled && + !!sellAssetId && + !!buyAssetId && + !!sellAmountCryptoBaseUnit && + !!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 new file mode 100644 index 00000000000..e39b3459233 --- /dev/null +++ b/packages/swap-widget-poc/src/hooks/useSwapRates.ts @@ -0,0 +1,52 @@ +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; +}; + +export const useSwapRates = ( + apiClient: ApiClient, + params: UseSwapRatesParams, +) => { + const { + sellAssetId, + buyAssetId, + sellAmountCryptoBaseUnit, + enabled = true, + } = params; + + return useQuery({ + queryKey: ["swapRates", sellAssetId, buyAssetId, sellAmountCryptoBaseUnit], + queryFn: async (): Promise => { + if (!sellAssetId || !buyAssetId || !sellAmountCryptoBaseUnit) { + return []; + } + const response = await apiClient.getRates({ + sellAssetId, + buyAssetId, + sellAmountCryptoBaseUnit, + }); + + return response.rates + .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; + }); + }, + 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 new file mode 100644 index 00000000000..4e673a53aa9 --- /dev/null +++ b/packages/swap-widget-poc/src/index.ts @@ -0,0 +1,43 @@ +export { SwapWidget } from "./components/SwapWidget"; + +export type { + Asset, + AssetId, + ChainId, + Chain, + TradeRate, + TradeQuote, + SwapperName, + SwapWidgetProps, + ThemeMode, + ThemeConfig, +} from "./types"; + +export { + isEvmChainId, + getEvmChainIdNumber, + getChainType, + formatAmount, + parseAmount, + truncateAddress, + EVM_CHAIN_IDS, + UTXO_CHAIN_IDS, + COSMOS_CHAIN_IDS, + OTHER_CHAIN_IDS, +} from "./types"; + +export { + CHAIN_METADATA, + getChainMeta, + getChainName, + getChainIcon, + getChainColor, +} from "./constants/chains"; + +export { + useAssets, + useAssetById, + useChains, + useAssetsByChainId, + useAssetSearch, +} from "./hooks/useAssets"; diff --git a/packages/swap-widget-poc/src/types/index.ts b/packages/swap-widget-poc/src/types/index.ts new file mode 100644 index 00000000000..2430872c227 --- /dev/null +++ b/packages/swap-widget-poc/src/types/index.ts @@ -0,0 +1,201 @@ +export type ChainId = string; +export type AssetId = string; + +export type Chain = { + 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; +}; + +export type SwapperName = + | "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; + feeData: { + 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; +}; + +export type TradeRate = { + swapperName: SwapperName; + rate: string; + buyAmountCryptoBaseUnit: string; + sellAmountCryptoBaseUnit: string; + steps: number; + estimatedExecutionTimeMs?: number; + affiliateBps: string; + networkFeeCryptoBaseUnit?: string; + error?: { + code: string; + message: string; + }; + id?: string; +}; + +export type ThemeMode = "light" | "dark"; + +export type ThemeConfig = { + 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; +}; + +export type RatesResponse = { + rates: TradeRate[]; +}; + +export type QuoteResponse = { + quote: TradeQuote; + transactionData?: { + to: string; + data: string; + value: string; + gasLimit?: string; + }; +}; + +export type AssetsResponse = { + 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; + +export const UTXO_CHAIN_IDS = { + 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; + +export const OTHER_CHAIN_IDS = { + solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", +} as const; + +export const isEvmChainId = (chainId: string): boolean => { + 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 }); +}; + +export const parseAmount = (amount: string, decimals: number): string => { + 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)}`; +}; diff --git a/packages/swap-widget-poc/src/utils/redirect.ts b/packages/swap-widget-poc/src/utils/redirect.ts new file mode 100644 index 00000000000..27d6e46184c --- /dev/null +++ b/packages/swap-widget-poc/src/utils/redirect.ts @@ -0,0 +1,48 @@ +import type { AssetId, Asset } from "../types"; +import { isEvmChainId } from "../types"; + +const SHAPESHIFT_APP_URL = "https://app.shapeshift.com"; + +export type RedirectParams = { + 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); + if (params.sellAmount) { + url.searchParams.set("sellAmount", params.sellAmount); + } + return url.toString(); +}; + +export const redirectToShapeShift = (params: RedirectParams): void => { + const url = buildShapeShiftTradeUrl(params); + window.open(url, "_blank", "noopener,noreferrer"); +}; + +export type ChainType = "evm" | "utxo" | "cosmos" | "solana" | "other"; + +export const getChainTypeFromAsset = (asset: Asset): ChainType => { + 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"; + + return "other"; +}; + +export const canExecuteInWidget = ( + sellAsset: Asset, + buyAsset: Asset, +): boolean => { + const sellChainType = getChainTypeFromAsset(sellAsset); + const buyChainType = getChainTypeFromAsset(buyAsset); + + 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 new file mode 100644 index 00000000000..b60eb85e682 --- /dev/null +++ b/packages/swap-widget-poc/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface EthereumProvider { + request: (args: { method: string; params?: unknown[] }) => Promise; + on: (event: string, callback: (accounts: string[]) => void) => void; +} + +interface Window { + ethereum?: EthereumProvider; +} diff --git a/packages/swap-widget-poc/tsconfig.json b/packages/swap-widget-poc/tsconfig.json new file mode 100644 index 00000000000..f50b75c5f0c --- /dev/null +++ b/packages/swap-widget-poc/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/swap-widget-poc/tsconfig.node.json b/packages/swap-widget-poc/tsconfig.node.json new file mode 100644 index 00000000000..42872c59f5b --- /dev/null +++ b/packages/swap-widget-poc/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/swap-widget-poc/vite.config.ts b/packages/swap-widget-poc/vite.config.ts new file mode 100644 index 00000000000..074a898debe --- /dev/null +++ b/packages/swap-widget-poc/vite.config.ts @@ -0,0 +1,29 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + define: { + "process.env": {}, + }, + server: { + port: 3001, + open: true, + }, + build: { + lib: { + entry: "src/index.ts", + name: "SwapWidget", + fileName: "index", + }, + rollupOptions: { + external: ["react", "react-dom"], + output: { + globals: { + react: "React", + "react-dom": "ReactDOM", + }, + }, + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 14052f7063d..d2e0c642f48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -324,7 +324,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.28.4": +"@babel/core@npm:^7.28.0, @babel/core@npm:^7.28.4": version: 7.28.5 resolution: "@babel/core@npm:7.28.5" dependencies: @@ -3842,6 +3842,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.26.0": + version: 7.28.4 + resolution: "@babel/runtime@npm:7.28.4" + checksum: 934b0a0460f7d06637d93fcd1a44ac49adc33518d17253b5a0b55ff4cb90a45d8fe78bf034b448911dbec7aff2a90b918697559f78d21c99ff8dbadae9565b55 + languageName: node + linkType: hard + "@babel/template@npm:^7.18.10": version: 7.18.10 resolution: "@babel/template@npm:7.18.10" @@ -4095,6 +4102,23 @@ __metadata: languageName: node linkType: hard +"@base-org/account@npm:2.4.0": + version: 2.4.0 + resolution: "@base-org/account@npm:2.4.0" + dependencies: + "@coinbase/cdp-sdk": ^1.0.0 + "@noble/hashes": 1.4.0 + clsx: 1.2.1 + eventemitter3: 5.0.1 + idb-keyval: 6.2.1 + ox: 0.6.9 + preact: 10.24.2 + viem: ^2.31.7 + zustand: 5.0.3 + checksum: 6e824da6108756a3c3efab6c660186ecc06c355b0053d5721c0578111c058f01ae8d967b2a6490857498421eea610d6e4195d9bcd5496aa4a268420b4da52451 + languageName: node + linkType: hard + "@bitcoinerlab/secp256k1@npm:^1.1.1": version: 1.1.1 resolution: "@bitcoinerlab/secp256k1@npm:1.1.1" @@ -4485,6 +4509,26 @@ __metadata: languageName: node linkType: hard +"@coinbase/cdp-sdk@npm:^1.0.0": + version: 1.43.0 + resolution: "@coinbase/cdp-sdk@npm:1.43.0" + dependencies: + "@solana-program/system": ^0.10.0 + "@solana-program/token": ^0.9.0 + "@solana/kit": ^5.1.0 + "@solana/web3.js": ^1.98.1 + abitype: 1.0.6 + axios: ^1.12.2 + axios-retry: ^4.5.0 + jose: ^6.0.8 + md5: ^2.3.0 + uncrypto: ^0.1.3 + viem: ^2.21.26 + zod: ^3.24.4 + checksum: 9131adbfd1c3cc8f2fddd73aa9ecc15b97bb7f95c1f39ffe161eb8bec163684b51b1cfed37fd38aaf780d561d7b470044a48454d945a1ce8522cbd4e441c5494 + languageName: node + linkType: hard + "@coinbase/wallet-sdk@npm:4.0.0": version: 4.0.0 resolution: "@coinbase/wallet-sdk@npm:4.0.0" @@ -4499,6 +4543,22 @@ __metadata: languageName: node linkType: hard +"@coinbase/wallet-sdk@npm:4.3.6": + version: 4.3.6 + resolution: "@coinbase/wallet-sdk@npm:4.3.6" + dependencies: + "@noble/hashes": 1.4.0 + clsx: 1.2.1 + eventemitter3: 5.0.1 + idb-keyval: 6.2.1 + ox: 0.6.9 + preact: 10.24.2 + viem: ^2.27.2 + zustand: 5.0.3 + checksum: 9c193bcc715c709245893ceae02ebb127e7a25723887f8aee59d9cd1bf8e2a9204ff2e2636d06a434825785b675f57368fde351d09ed295c12b040f43a4b845a + languageName: node + linkType: hard + "@coinbase/wallet-sdk@npm:^3.6.6": version: 3.7.1 resolution: "@coinbase/wallet-sdk@npm:3.7.1" @@ -5108,6 +5168,15 @@ __metadata: languageName: node linkType: hard +"@ecies/ciphers@npm:^0.2.4": + version: 0.2.5 + resolution: "@ecies/ciphers@npm:0.2.5" + peerDependencies: + "@noble/ciphers": ^1.0.0 + checksum: 33aa89b8633e66cf5f0fbc18e461e4b152261bd7d30658a213ebb86beb7ae657d64defa7c4483e1c5c82f5935241958210d44d39efab7e4f09bbfa4e76323ad7 + languageName: node + linkType: hard + "@eivifj/dot@npm:^1.0.1": version: 1.0.3 resolution: "@eivifj/dot@npm:1.0.3" @@ -5147,7 +5216,7 @@ __metadata: languageName: node linkType: hard -"@emotion/hash@npm:^0.9.2": +"@emotion/hash@npm:^0.9.0, @emotion/hash@npm:^0.9.2": version: 0.9.2 resolution: "@emotion/hash@npm:0.9.2" checksum: 379bde2830ccb0328c2617ec009642321c0e009a46aa383dfbe75b679c6aea977ca698c832d225a893901f29d7b3eef0e38cf341f560f6b2b56f1ff23c172387 @@ -5346,6 +5415,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/aix-ppc64@npm:0.24.2" @@ -5367,6 +5443,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/android-arm64@npm:0.24.2" @@ -5388,6 +5471,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/android-arm@npm:0.24.2" @@ -5409,6 +5499,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/android-x64@npm:0.24.2" @@ -5430,6 +5527,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/darwin-arm64@npm:0.24.2" @@ -5451,6 +5555,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/darwin-x64@npm:0.24.2" @@ -5472,6 +5583,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/freebsd-arm64@npm:0.24.2" @@ -5493,6 +5611,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/freebsd-x64@npm:0.24.2" @@ -5514,6 +5639,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-arm64@npm:0.24.2" @@ -5535,6 +5667,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-arm@npm:0.24.2" @@ -5556,6 +5695,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-ia32@npm:0.24.2" @@ -5577,6 +5723,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-loong64@npm:0.24.2" @@ -5598,6 +5751,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-mips64el@npm:0.24.2" @@ -5619,6 +5779,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-ppc64@npm:0.24.2" @@ -5640,6 +5807,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-riscv64@npm:0.24.2" @@ -5661,6 +5835,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-s390x@npm:0.24.2" @@ -5682,6 +5863,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-x64@npm:0.24.2" @@ -5724,6 +5912,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/netbsd-x64@npm:0.24.2" @@ -5766,6 +5961,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/openbsd-x64@npm:0.24.2" @@ -5787,6 +5989,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/sunos-x64@npm:0.24.2" @@ -5808,6 +6017,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/win32-arm64@npm:0.24.2" @@ -5829,6 +6045,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/win32-ia32@npm:0.24.2" @@ -5850,6 +6073,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/win32-x64@npm:0.24.2" @@ -6875,6 +7105,18 @@ __metadata: languageName: node linkType: hard +"@gemini-wallet/core@npm:0.3.2": + version: 0.3.2 + resolution: "@gemini-wallet/core@npm:0.3.2" + dependencies: + "@metamask/rpc-errors": 7.0.2 + eventemitter3: 5.0.1 + peerDependencies: + viem: ">=2.0.0" + checksum: a74a60548a7e35ae6b3c1a0a4a1289baa7b646be5846508e432cc87735691994de806c7dca4961b5d77ee04652fa0cc18e3a67376da886aeebb5d4a5409c96be + languageName: node + linkType: hard + "@gql.tada/cli-utils@npm:1.7.2": version: 1.7.2 resolution: "@gql.tada/cli-utils@npm:1.7.2" @@ -7745,6 +7987,13 @@ __metadata: languageName: node linkType: hard +"@lit-labs/ssr-dom-shim@npm:^1.5.0": + version: 1.5.1 + resolution: "@lit-labs/ssr-dom-shim@npm:1.5.1" + checksum: 22c404c4813bceccaba2b6ee533251b58a2f4e9669507fcbd021120c66fbc2ae1daea4d1e31a76e450fa6a16942471579c4d1e256aa65ec1172d5aac0c0197e1 + languageName: node + linkType: hard + "@lit/reactive-element@npm:^1.3.0, @lit/reactive-element@npm:^1.6.0": version: 1.6.3 resolution: "@lit/reactive-element@npm:1.6.3" @@ -7931,6 +8180,17 @@ __metadata: languageName: node linkType: hard +"@metamask/json-rpc-engine@npm:^8.0.1, @metamask/json-rpc-engine@npm:^8.0.2": + version: 8.0.2 + resolution: "@metamask/json-rpc-engine@npm:8.0.2" + dependencies: + "@metamask/rpc-errors": ^6.2.1 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 + checksum: c240d298ad503d93922a94a62cf59f0344b6d6644a523bc8ea3c0f321bea7172b89f2747a5618e2861b2e8152ae5086b76f391a10e4566529faa50b8850c051d + languageName: node + linkType: hard + "@metamask/json-rpc-middleware-stream@npm:^6.0.2": version: 6.0.2 resolution: "@metamask/json-rpc-middleware-stream@npm:6.0.2" @@ -7943,6 +8203,18 @@ __metadata: languageName: node linkType: hard +"@metamask/json-rpc-middleware-stream@npm:^7.0.1": + version: 7.0.2 + resolution: "@metamask/json-rpc-middleware-stream@npm:7.0.2" + dependencies: + "@metamask/json-rpc-engine": ^8.0.2 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 + readable-stream: ^3.6.2 + checksum: ff11ad3ff0ec27530efc53c4e6543661648f437dacdd58797449307e20dbc428b479cd8d1e9767797268b98d0445bd6f1986820a8c855faeef01d5c03b55323b + languageName: node + linkType: hard + "@metamask/key-tree@npm:^7.1.1": version: 7.1.1 resolution: "@metamask/key-tree@npm:7.1.1" @@ -8021,6 +8293,26 @@ __metadata: languageName: node linkType: hard +"@metamask/providers@npm:16.1.0": + version: 16.1.0 + resolution: "@metamask/providers@npm:16.1.0" + dependencies: + "@metamask/json-rpc-engine": ^8.0.1 + "@metamask/json-rpc-middleware-stream": ^7.0.1 + "@metamask/object-multiplex": ^2.0.0 + "@metamask/rpc-errors": ^6.2.1 + "@metamask/safe-event-emitter": ^3.1.1 + "@metamask/utils": ^8.3.0 + detect-browser: ^5.2.0 + extension-port-stream: ^3.0.0 + fast-deep-equal: ^3.1.3 + is-stream: ^2.0.0 + readable-stream: ^3.6.2 + webextension-polyfill: ^0.10.0 + checksum: 85e40140f342a38112c3d7cee436751a2be4c575cc4f815ab48a73b549abc2d756bf4a10e4b983e91dbd38076601f992531edb6d8d674aebceae32ef7e299275 + languageName: node + linkType: hard + "@metamask/providers@npm:^10.2.0, @metamask/providers@npm:^10.2.1": version: 10.2.1 resolution: "@metamask/providers@npm:10.2.1" @@ -8061,6 +8353,16 @@ __metadata: languageName: node linkType: hard +"@metamask/rpc-errors@npm:7.0.2": + version: 7.0.2 + resolution: "@metamask/rpc-errors@npm:7.0.2" + dependencies: + "@metamask/utils": ^11.0.1 + fast-safe-stringify: ^2.0.6 + checksum: 262a1ab57121e277eb979325d8e4335b9f4194c5acd0138ee0032db35b4e20ea0423badb5dad4bdf6abb85d22b476377f17911a54f82b3b1a2bdffc36654d028 + languageName: node + linkType: hard + "@metamask/rpc-errors@npm:^6.2.1": version: 6.2.1 resolution: "@metamask/rpc-errors@npm:6.2.1" @@ -8104,6 +8406,13 @@ __metadata: languageName: node linkType: hard +"@metamask/safe-event-emitter@npm:^3.1.1": + version: 3.1.2 + resolution: "@metamask/safe-event-emitter@npm:3.1.2" + checksum: 8ef7579f9317eb5c94ecf3e6abb8d13b119af274b678805eac76abe4c0667bfdf539f385e552bb973e96333b71b77aa7c787cb3fce9cd5fb4b00f1dbbabf880d + languageName: node + linkType: hard + "@metamask/scure-bip39@npm:^2.1.0": version: 2.1.0 resolution: "@metamask/scure-bip39@npm:2.1.0" @@ -8114,6 +8423,15 @@ __metadata: languageName: node linkType: hard +"@metamask/sdk-analytics@npm:0.0.5": + version: 0.0.5 + resolution: "@metamask/sdk-analytics@npm:0.0.5" + dependencies: + openapi-fetch: ^0.13.5 + checksum: dcbc07fa4ce7e487f7f37164739513d6a39018c5fc2b53efbc7b880ed4c2fd24e3d4e92ef9f171561179f2434301e31c8919fb13a645f2943698060795ead967 + languageName: node + linkType: hard + "@metamask/sdk-communication-layer@npm:0.20.2": version: 0.20.2 resolution: "@metamask/sdk-communication-layer@npm:0.20.2" @@ -8133,6 +8451,26 @@ __metadata: languageName: node linkType: hard +"@metamask/sdk-communication-layer@npm:0.33.1": + version: 0.33.1 + resolution: "@metamask/sdk-communication-layer@npm:0.33.1" + dependencies: + "@metamask/sdk-analytics": 0.0.5 + bufferutil: ^4.0.8 + date-fns: ^2.29.3 + debug: 4.3.4 + utf-8-validate: ^5.0.2 + uuid: ^8.3.2 + peerDependencies: + cross-fetch: ^4.0.0 + eciesjs: "*" + eventemitter2: ^6.4.9 + readable-stream: ^3.6.2 + socket.io-client: ^4.5.1 + checksum: cc79345be52fe933d4f693a9b0ef37dd9a1db3550ddcaaec7d2e51cc130c33403af968a78a6d8dd51255c59b37f933977736f8a4eeee67a4f5554c569607b213 + languageName: node + linkType: hard + "@metamask/sdk-install-modal-web@npm:0.20.2": version: 0.20.2 resolution: "@metamask/sdk-install-modal-web@npm:0.20.2" @@ -8155,6 +8493,15 @@ __metadata: languageName: node linkType: hard +"@metamask/sdk-install-modal-web@npm:0.32.1": + version: 0.32.1 + resolution: "@metamask/sdk-install-modal-web@npm:0.32.1" + dependencies: + "@paulmillr/qr": ^0.2.1 + checksum: f8cbaa7f22d097cf5ea39132cd09c7ce9b314bec1ea3d01547ee5205fcbebb68f896b0a0194f5d84614dafff21cdfcf017070fd1ae3b888d1e1c986a0dafb289 + languageName: node + linkType: hard + "@metamask/sdk@npm:0.20.3": version: 0.20.3 resolution: "@metamask/sdk@npm:0.20.3" @@ -8193,6 +8540,34 @@ __metadata: languageName: node linkType: hard +"@metamask/sdk@npm:0.33.1": + version: 0.33.1 + resolution: "@metamask/sdk@npm:0.33.1" + dependencies: + "@babel/runtime": ^7.26.0 + "@metamask/onboarding": ^1.0.1 + "@metamask/providers": 16.1.0 + "@metamask/sdk-analytics": 0.0.5 + "@metamask/sdk-communication-layer": 0.33.1 + "@metamask/sdk-install-modal-web": 0.32.1 + "@paulmillr/qr": ^0.2.1 + bowser: ^2.9.0 + cross-fetch: ^4.0.0 + debug: 4.3.4 + eciesjs: ^0.4.11 + eth-rpc-errors: ^4.0.3 + eventemitter2: ^6.4.9 + obj-multiplex: ^1.0.0 + pump: ^3.0.0 + readable-stream: ^3.6.2 + socket.io-client: ^4.5.1 + tslib: ^2.6.0 + util: ^0.12.4 + uuid: ^8.3.2 + checksum: 40dca7ff0331e80008e94e56fe009a590ab8b96fc68ea48ceb8fb7ba1e8bfc492e0e66033e80be932d27acae7034cd55c07e54ff051586d1c3f37d5a0b7edda7 + languageName: node + linkType: hard + "@metamask/snaps-registry@npm:^1.2.1": version: 1.2.2 resolution: "@metamask/snaps-registry@npm:1.2.2" @@ -9500,7 +9875,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.9.7, @noble/curves@npm:^1.0.0": +"@noble/curves@npm:1.9.7, @noble/curves@npm:^1.0.0, @noble/curves@npm:^1.9.7": version: 1.9.7 resolution: "@noble/curves@npm:1.9.7" dependencies: @@ -9916,6 +10291,13 @@ __metadata: languageName: node linkType: hard +"@paulmillr/qr@npm:^0.2.1": + version: 0.2.1 + resolution: "@paulmillr/qr@npm:0.2.1" + checksum: 8a7b882f74f472759b0e5911c9c902a29c5232609373af4c5775625d9aad4ebda635d84c25be27e694144ba73d8e4204e72c3b9b59e9a375ec1d19f034a2d2ad + languageName: node + linkType: hard + "@peculiar/asn1-schema@npm:^2.1.0": version: 2.1.0 resolution: "@peculiar/asn1-schema@npm:2.1.0" @@ -10434,6 +10816,27 @@ __metadata: languageName: node linkType: hard +"@rainbow-me/rainbowkit@npm:^2.2.3": + version: 2.2.10 + resolution: "@rainbow-me/rainbowkit@npm:2.2.10" + dependencies: + "@vanilla-extract/css": 1.17.3 + "@vanilla-extract/dynamic": 2.1.4 + "@vanilla-extract/sprinkles": 1.6.4 + clsx: 2.1.1 + cuer: 0.0.3 + react-remove-scroll: 2.6.2 + ua-parser-js: ^1.0.37 + peerDependencies: + "@tanstack/react-query": ">=5.0.0" + react: ">=18" + react-dom: ">=18" + viem: 2.x + wagmi: ^2.9.0 + checksum: 3f8842f93791fe8bc9d3d11f43b4ab0c2ca7fed0978dacd47df082f70c044af609f93d2addd4e77b0c35bbd892f525a14c8415822c08402ee1a2490512034c61 + languageName: node + linkType: hard + "@randlabs/communication-bridge@npm:1.0.1": version: 1.0.1 resolution: "@randlabs/communication-bridge@npm:1.0.1" @@ -10554,6 +10957,17 @@ __metadata: languageName: node linkType: hard +"@reown/appkit-common@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-common@npm:1.7.8" + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: ">=2.29.0" + checksum: 6d643d1e93b0709b90dddc66a3e0391d577e3cd84cd0e7d6da895e982058e5e0ea96f518f8d103d74d037589082523fd339057689a99cd5cd98cbb37b61a29ea + languageName: node + linkType: hard + "@reown/appkit-controllers@npm:1.7.3": version: 1.7.3 resolution: "@reown/appkit-controllers@npm:1.7.3" @@ -10567,6 +10981,33 @@ __metadata: languageName: node linkType: hard +"@reown/appkit-controllers@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-controllers@npm:1.7.8" + dependencies: + "@reown/appkit-common": 1.7.8 + "@reown/appkit-wallet": 1.7.8 + "@walletconnect/universal-provider": 2.21.0 + valtio: 1.13.2 + viem: ">=2.29.0" + checksum: 1da42acf43e16cd01bda6a431b9ef5a7aaf3b702d178766bba7eb1aa8e9d74258262ff94be3095fd04c25044ed072c323bcb7606a9038401239a0c31c1894212 + languageName: node + linkType: hard + +"@reown/appkit-pay@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-pay@npm:1.7.8" + dependencies: + "@reown/appkit-common": 1.7.8 + "@reown/appkit-controllers": 1.7.8 + "@reown/appkit-ui": 1.7.8 + "@reown/appkit-utils": 1.7.8 + lit: 3.3.0 + valtio: 1.13.2 + checksum: 5785088196b08b0067e48734804ed2256a31622d47fac240896f070bc096c080c76be9a4bb45fe05fc9ad1105ef4537b9cd1525aa6ce44dc9eab6dc697bab6cd + languageName: node + linkType: hard + "@reown/appkit-polyfills@npm:1.7.3": version: 1.7.3 resolution: "@reown/appkit-polyfills@npm:1.7.3" @@ -10576,6 +11017,15 @@ __metadata: languageName: node linkType: hard +"@reown/appkit-polyfills@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-polyfills@npm:1.7.8" + dependencies: + buffer: 6.0.3 + checksum: f47887c27d2a58e39c9344710a805c41fd4db7032a40bbfb628f5da2724576201c79c68e5030126c410cee5bb3c480d8670cceb4610dd39c5955e54ca4f453d3 + languageName: node + linkType: hard + "@reown/appkit-scaffold-ui@npm:1.7.3": version: 1.7.3 resolution: "@reown/appkit-scaffold-ui@npm:1.7.3" @@ -10590,6 +11040,20 @@ __metadata: languageName: node linkType: hard +"@reown/appkit-scaffold-ui@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-scaffold-ui@npm:1.7.8" + dependencies: + "@reown/appkit-common": 1.7.8 + "@reown/appkit-controllers": 1.7.8 + "@reown/appkit-ui": 1.7.8 + "@reown/appkit-utils": 1.7.8 + "@reown/appkit-wallet": 1.7.8 + lit: 3.3.0 + checksum: b835d4a8762d631cdeb41b4fdfc7aaff9b386d5808ca4f8f78c9733a0a9cbc7c1f05f527db42172330d439b7d22fa880b12e02a80cbe320ccfbe5cb91eab1ba7 + languageName: node + linkType: hard + "@reown/appkit-ui@npm:1.7.3": version: 1.7.3 resolution: "@reown/appkit-ui@npm:1.7.3" @@ -10603,6 +11067,19 @@ __metadata: languageName: node linkType: hard +"@reown/appkit-ui@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-ui@npm:1.7.8" + dependencies: + "@reown/appkit-common": 1.7.8 + "@reown/appkit-controllers": 1.7.8 + "@reown/appkit-wallet": 1.7.8 + lit: 3.3.0 + qrcode: 1.5.3 + checksum: 21464449cff886f952e68accb13c9b73e94bcf04974612f3afc48cd4f3c9e63ecef5d240d6ba1fd26e4ab29ad6bb2304c7b326528674acd8490b346b9449a39e + languageName: node + linkType: hard + "@reown/appkit-utils@npm:1.7.3": version: 1.7.3 resolution: "@reown/appkit-utils@npm:1.7.3" @@ -10621,6 +11098,24 @@ __metadata: languageName: node linkType: hard +"@reown/appkit-utils@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-utils@npm:1.7.8" + dependencies: + "@reown/appkit-common": 1.7.8 + "@reown/appkit-controllers": 1.7.8 + "@reown/appkit-polyfills": 1.7.8 + "@reown/appkit-wallet": 1.7.8 + "@walletconnect/logger": 2.1.2 + "@walletconnect/universal-provider": 2.21.0 + valtio: 1.13.2 + viem: ">=2.29.0" + peerDependencies: + valtio: 1.13.2 + checksum: 54ed191019815d20c4b817ecc81b49b888ac65ae0632ff724eb51033f3cd72fd40461568e69168a2003c858c80d1f4a555ca449cfef6c0cbbd7ae66c2bd8111e + languageName: node + linkType: hard + "@reown/appkit-wallet@npm:1.7.3": version: 1.7.3 resolution: "@reown/appkit-wallet@npm:1.7.3" @@ -10633,6 +11128,18 @@ __metadata: languageName: node linkType: hard +"@reown/appkit-wallet@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-wallet@npm:1.7.8" + dependencies: + "@reown/appkit-common": 1.7.8 + "@reown/appkit-polyfills": 1.7.8 + "@walletconnect/logger": 2.1.2 + zod: 3.22.4 + checksum: 4b1caaae2ca188f56298c1d835b3d8e89d00889c38a7964ac7e7c4e8b97cf06e5bcfb224a5076e8d9a0ac926d4fcda30733d7ca4a04f15c11b83c55a632964d4 + languageName: node + linkType: hard + "@reown/appkit@npm:1.7.3": version: 1.7.3 resolution: "@reown/appkit@npm:1.7.3" @@ -10653,6 +11160,27 @@ __metadata: languageName: node linkType: hard +"@reown/appkit@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit@npm:1.7.8" + dependencies: + "@reown/appkit-common": 1.7.8 + "@reown/appkit-controllers": 1.7.8 + "@reown/appkit-pay": 1.7.8 + "@reown/appkit-polyfills": 1.7.8 + "@reown/appkit-scaffold-ui": 1.7.8 + "@reown/appkit-ui": 1.7.8 + "@reown/appkit-utils": 1.7.8 + "@reown/appkit-wallet": 1.7.8 + "@walletconnect/types": 2.21.0 + "@walletconnect/universal-provider": 2.21.0 + bs58: 6.0.0 + valtio: 1.13.2 + viem: ">=2.29.0" + checksum: 1f74d86988fb8ad6b449588572dfeb64c186c3ae5ca3617b2794f3d566dcc66d2154d9f105fc2d9f560557e0fca62240bff49ec6fbeb2c012b456363e8d16600 + languageName: node + linkType: hard + "@reown/walletkit@npm:^1.2.6": version: 1.2.6 resolution: "@reown/walletkit@npm:1.2.6" @@ -10668,6 +11196,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-beta.27": + version: 1.0.0-beta.27 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.27" + checksum: b57d8de44534bdbb92e9dda70a29c5b3cb7a13cc3a2efaaf9d27923ca23e2526e9470939fe1d7c6a96623da20aaf96a1517502e1d9a6d4f98fbb544cf502600a + languageName: node + linkType: hard + "@rolldown/pluginutils@npm:1.0.0-beta.43": version: 1.0.0-beta.43 resolution: "@rolldown/pluginutils@npm:1.0.0-beta.43" @@ -10721,6 +11256,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.55.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-android-arm64@npm:4.34.8" @@ -10735,6 +11277,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-android-arm64@npm:4.55.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-darwin-arm64@npm:4.34.8" @@ -10749,6 +11298,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.55.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-darwin-x64@npm:4.34.8" @@ -10763,6 +11319,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.55.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-arm64@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-freebsd-arm64@npm:4.34.8" @@ -10777,6 +11340,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.55.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-freebsd-x64@npm:4.34.8" @@ -10791,6 +11361,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.55.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.34.8" @@ -10805,6 +11382,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.55.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.34.8" @@ -10819,6 +11403,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.55.1" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.34.8" @@ -10833,6 +11424,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.55.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.34.8" @@ -10847,6 +11445,27 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.55.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.55.1" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.55.1" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-loongarch64-gnu@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.34.8" @@ -10875,6 +11494,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-ppc64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.55.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.55.1" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.34.8" @@ -10889,6 +11522,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.55.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-musl@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.50.1" @@ -10896,6 +11536,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.55.1" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.34.8" @@ -10910,6 +11557,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.55.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.34.8" @@ -10924,6 +11578,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.55.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-linux-x64-musl@npm:4.34.8" @@ -10938,6 +11599,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.55.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-openbsd-x64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-openbsd-x64@npm:4.55.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-openharmony-arm64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-openharmony-arm64@npm:4.50.1" @@ -10945,6 +11620,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-openharmony-arm64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.55.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.34.8" @@ -10959,6 +11641,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.55.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.34.8" @@ -10973,6 +11662,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.55.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.55.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.34.8": version: 4.34.8 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.34.8" @@ -10987,6 +11690,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.55.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rrweb/types@npm:^2.0.0-alpha.18": version: 2.0.0-alpha.18 resolution: "@rrweb/types@npm:2.0.0-alpha.18" @@ -11025,6 +11735,16 @@ __metadata: languageName: node linkType: hard +"@safe-global/safe-apps-provider@npm:0.18.6": + version: 0.18.6 + resolution: "@safe-global/safe-apps-provider@npm:0.18.6" + dependencies: + "@safe-global/safe-apps-sdk": ^9.1.0 + events: ^3.3.0 + checksum: af7e054f5170c8bec6feddf6a3cc09277a93219f164c4d0b49cdaef5c7e725ba2e414df17b2b1df85fbab10a8d8fad66c63f76ce3dfe042ee37aefb246edab6d + languageName: node + linkType: hard + "@safe-global/safe-apps-sdk@npm:8.1.0, @safe-global/safe-apps-sdk@npm:^8.1.0": version: 8.1.0 resolution: "@safe-global/safe-apps-sdk@npm:8.1.0" @@ -11035,6 +11755,16 @@ __metadata: languageName: node linkType: hard +"@safe-global/safe-apps-sdk@npm:9.1.0, @safe-global/safe-apps-sdk@npm:^9.1.0": + version: 9.1.0 + resolution: "@safe-global/safe-apps-sdk@npm:9.1.0" + dependencies: + "@safe-global/safe-gateway-typescript-sdk": ^3.5.3 + viem: ^2.1.1 + checksum: e56c3fe83f52667b370072807468b011e9f3e6d690126af4cc5b13ee1544dd5a91b4b3e962d45d2dab065fc4401ef57c350896a9f43c70a9fb3269249f265d72 + languageName: node + linkType: hard + "@safe-global/safe-gateway-typescript-sdk@npm:^3.5.3": version: 3.21.1 resolution: "@safe-global/safe-gateway-typescript-sdk@npm:3.21.1" @@ -11391,7 +12121,7 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/caip@^8.15.0, @shapeshiftoss/caip@workspace:^, @shapeshiftoss/caip@workspace:packages/caip": +"@shapeshiftoss/caip@^8.15.0, @shapeshiftoss/caip@workspace:*, @shapeshiftoss/caip@workspace:^, @shapeshiftoss/caip@workspace:packages/caip": version: 0.0.0-use.local resolution: "@shapeshiftoss/caip@workspace:packages/caip" dependencies: @@ -11961,6 +12691,28 @@ __metadata: languageName: node linkType: hard +"@shapeshiftoss/swap-widget-poc@workspace:packages/swap-widget-poc": + version: 0.0.0-use.local + resolution: "@shapeshiftoss/swap-widget-poc@workspace:packages/swap-widget-poc" + dependencies: + "@rainbow-me/rainbowkit": ^2.2.3 + "@shapeshiftoss/caip": "workspace:*" + "@tanstack/react-query": ^5.60.0 + "@types/react": ^18.2.0 + "@types/react-dom": ^18.2.0 + "@vitejs/plugin-react": ^4.2.0 + react: ^18.2.0 + react-dom: ^18.2.0 + typescript: ^5.2.2 + viem: ^2.21.0 + vite: ^5.0.0 + wagmi: ^2.14.0 + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + languageName: unknown + linkType: soft + "@shapeshiftoss/swapper@workspace:^, @shapeshiftoss/swapper@workspace:packages/swapper": version: 0.0.0-use.local resolution: "@shapeshiftoss/swapper@workspace:packages/swapper" @@ -12348,6 +13100,15 @@ __metadata: languageName: node linkType: hard +"@solana-program/system@npm:^0.10.0": + version: 0.10.0 + resolution: "@solana-program/system@npm:0.10.0" + peerDependencies: + "@solana/kit": ^5.0 + checksum: 1772fc40e2edceb10e85a5b48d347271c8b484a0b8a887fc86453644e28f8590421c20b6d2abd2e74fd4093c000efb1bddddbd928c913c9b169c0e7f0f46b8d3 + languageName: node + linkType: hard + "@solana-program/system@npm:^0.7.0": version: 0.7.0 resolution: "@solana-program/system@npm:0.7.0" @@ -12376,6 +13137,15 @@ __metadata: languageName: node linkType: hard +"@solana-program/token@npm:^0.9.0": + version: 0.9.0 + resolution: "@solana-program/token@npm:0.9.0" + peerDependencies: + "@solana/kit": ^5.0 + checksum: 0f1fe272065a2381bdbc02c9d9d81affd77ab5a7a8872aab10474ac903471b3cb4c2033e2f60ef2f386de563d6c4434289723093c50a7d0e47744045aa40fa64 + languageName: node + linkType: hard + "@solana/accounts@npm:2.3.0": version: 2.3.0 resolution: "@solana/accounts@npm:2.3.0" @@ -12392,6 +13162,22 @@ __metadata: languageName: node linkType: hard +"@solana/accounts@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/accounts@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/codecs-core": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/rpc-spec": 5.3.0 + "@solana/rpc-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: b58b7cd2b05ba106e54cd377091bb77f834980f3302e5b5475629ef36f4b1df22a448366cdb5e0b7fc1e486065e50aa9de621fed76b44c64c2ff0744471aede5 + languageName: node + linkType: hard + "@solana/addresses@npm:2.3.0": version: 2.3.0 resolution: "@solana/addresses@npm:2.3.0" @@ -12407,6 +13193,21 @@ __metadata: languageName: node linkType: hard +"@solana/addresses@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/addresses@npm:5.3.0" + dependencies: + "@solana/assertions": 5.3.0 + "@solana/codecs-core": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/nominal-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 2706472b3b78877ad972ee4ede02c65fb0be415b5ce93818804e1e1ea7c785df5c0f142e50edb5145c49cfc32eb25f0764e0affe2a8d747001a6defd1473933c + languageName: node + linkType: hard + "@solana/assertions@npm:2.3.0": version: 2.3.0 resolution: "@solana/assertions@npm:2.3.0" @@ -12418,6 +13219,17 @@ __metadata: languageName: node linkType: hard +"@solana/assertions@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/assertions@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 4fb998fd72560fb2c4077427def1831fd33332b2d8c8ddea97da906e608be5dec8e5a4212f04f2f7a6b930b9247ac1d3b7c45f57bd67a843f6c89e0e0e520c0a + languageName: node + linkType: hard + "@solana/buffer-layout-utils@npm:^0.2.0": version: 0.2.0 resolution: "@solana/buffer-layout-utils@npm:0.2.0" @@ -12470,6 +13282,17 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-core@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/codecs-core@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: a471169cab42aa8e0ceeb9386f1af40b3952b57aaa8eebc7b9cb27a7b6aacc67bcb7b6c6b008f235b58905f22a8f773cd6420a46604b4ae3a8cd1014d0a4f565 + languageName: node + linkType: hard + "@solana/codecs-data-structures@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/codecs-data-structures@npm:2.0.0-rc.1" @@ -12496,6 +13319,19 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-data-structures@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/codecs-data-structures@npm:5.3.0" + dependencies: + "@solana/codecs-core": 5.3.0 + "@solana/codecs-numbers": 5.3.0 + "@solana/errors": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: bf77f799d083a635947cebab25d7aa9440af4480a3123c717cfdb5ce82db0aa66c6cd3a663e359ecd85f99a41b3bee89d0e827e2e4db886aa8da7284205c4b6d + languageName: node + linkType: hard + "@solana/codecs-numbers@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/codecs-numbers@npm:2.0.0-rc.1" @@ -12520,6 +13356,18 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-numbers@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/codecs-numbers@npm:5.3.0" + dependencies: + "@solana/codecs-core": 5.3.0 + "@solana/errors": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 6b0dbc6a1766fa372f38a9ece3af20fdfaff9f139fb9e0c1902b6b4e13c6df1823bfe9f08ea26a9b66469471accfccc8ad9f8a50aa52eabf896e5d2e89cd12d4 + languageName: node + linkType: hard + "@solana/codecs-strings@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/codecs-strings@npm:2.0.0-rc.1" @@ -12548,6 +13396,23 @@ __metadata: languageName: node linkType: hard +"@solana/codecs-strings@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/codecs-strings@npm:5.3.0" + dependencies: + "@solana/codecs-core": 5.3.0 + "@solana/codecs-numbers": 5.3.0 + "@solana/errors": 5.3.0 + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: ">=5.9.3" + peerDependenciesMeta: + fastestsmallesttextencoderdecoder: + optional: true + checksum: ef8688c3dd97b951e3bc855e62fe3c93625b4e504f80b088ff1e9d29865d85f92e647f76d5d454daa0e053253ac455030662d841c185a8c9123eb3d43eb850e3 + languageName: node + linkType: hard + "@solana/codecs@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/codecs@npm:2.0.0-rc.1" @@ -12578,6 +13443,21 @@ __metadata: languageName: node linkType: hard +"@solana/codecs@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/codecs@npm:5.3.0" + dependencies: + "@solana/codecs-core": 5.3.0 + "@solana/codecs-data-structures": 5.3.0 + "@solana/codecs-numbers": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/options": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 5e821cea6af50659c381e924718bd7efa1c0ed3b9a4135470374a505397da967e9ea3cf6108a96243d001262cb35cc34c3768f037cf9a722facce1c099f589bc + languageName: node + linkType: hard + "@solana/errors@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/errors@npm:2.0.0-rc.1" @@ -12606,6 +13486,20 @@ __metadata: languageName: node linkType: hard +"@solana/errors@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/errors@npm:5.3.0" + dependencies: + chalk: 5.6.2 + commander: 14.0.2 + peerDependencies: + typescript: ">=5.9.3" + bin: + errors: bin/cli.mjs + checksum: 393edb5171aeac3bf82e16eaa78c35c49167890fb2cdb93805d11729e31388a6080232a6eb14cb1f02540c12663753651561aff8ee6f831348e39d1cc0cb8708 + languageName: node + linkType: hard + "@solana/fast-stable-stringify@npm:2.3.0": version: 2.3.0 resolution: "@solana/fast-stable-stringify@npm:2.3.0" @@ -12615,6 +13509,15 @@ __metadata: languageName: node linkType: hard +"@solana/fast-stable-stringify@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/fast-stable-stringify@npm:5.3.0" + peerDependencies: + typescript: ">=5.9.3" + checksum: 004ea05ee9dda3e5948001ed8cfe0165b912227f762bb4af06809b7db0716afd4732e1e83b8bf14cb42b1ac58b595779a49271796644e76b57f3ef3fff1e71de + languageName: node + linkType: hard + "@solana/functional@npm:2.3.0": version: 2.3.0 resolution: "@solana/functional@npm:2.3.0" @@ -12624,6 +13527,31 @@ __metadata: languageName: node linkType: hard +"@solana/functional@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/functional@npm:5.3.0" + peerDependencies: + typescript: ">=5.9.3" + checksum: 434825c3a01c9dfea6477a1090e18caf74c9308a8ce27b6f2976de82766375877ab175a42e7d19db9459c1a038eb92a439bba9a0c7be9ca9c0eb0c40546e8763 + languageName: node + linkType: hard + +"@solana/instruction-plans@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/instruction-plans@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + "@solana/instructions": 5.3.0 + "@solana/keys": 5.3.0 + "@solana/promises": 5.3.0 + "@solana/transaction-messages": 5.3.0 + "@solana/transactions": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 01e8574663b58aa893056b9400037ef1ea83c2c7c6b3fcddfa366f892965a9b8cf409744d26ea445fcf0baa421bd904c7d04b2d7485a17b38105e834fad0eea3 + languageName: node + linkType: hard + "@solana/instructions@npm:2.3.0": version: 2.3.0 resolution: "@solana/instructions@npm:2.3.0" @@ -12636,6 +13564,18 @@ __metadata: languageName: node linkType: hard +"@solana/instructions@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/instructions@npm:5.3.0" + dependencies: + "@solana/codecs-core": 5.3.0 + "@solana/errors": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: e6195e2e0288009953d22de34086677173502beb5b04a811d397b91fc88c628daeebb96289bb456adf0f14bb4256f2419d736687cf82c8bce10d39e9ec9d263d + languageName: node + linkType: hard + "@solana/keys@npm:2.3.0": version: 2.3.0 resolution: "@solana/keys@npm:2.3.0" @@ -12651,6 +13591,21 @@ __metadata: languageName: node linkType: hard +"@solana/keys@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/keys@npm:5.3.0" + dependencies: + "@solana/assertions": 5.3.0 + "@solana/codecs-core": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/nominal-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 765ac154819f8c73de969799040d62f2ed831b5ed6a7a219adf3e3a9603ed1a178609d1234841105c12e82f8e63f995b7c93a5b0c3d806b0001bc91e35a094a9 + languageName: node + linkType: hard + "@solana/kit@npm:^2.3.0": version: 2.3.0 resolution: "@solana/kit@npm:2.3.0" @@ -12679,6 +13634,38 @@ __metadata: languageName: node linkType: hard +"@solana/kit@npm:^5.1.0": + version: 5.3.0 + resolution: "@solana/kit@npm:5.3.0" + dependencies: + "@solana/accounts": 5.3.0 + "@solana/addresses": 5.3.0 + "@solana/codecs": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/functional": 5.3.0 + "@solana/instruction-plans": 5.3.0 + "@solana/instructions": 5.3.0 + "@solana/keys": 5.3.0 + "@solana/offchain-messages": 5.3.0 + "@solana/plugin-core": 5.3.0 + "@solana/programs": 5.3.0 + "@solana/rpc": 5.3.0 + "@solana/rpc-api": 5.3.0 + "@solana/rpc-parsed-types": 5.3.0 + "@solana/rpc-spec-types": 5.3.0 + "@solana/rpc-subscriptions": 5.3.0 + "@solana/rpc-types": 5.3.0 + "@solana/signers": 5.3.0 + "@solana/sysvars": 5.3.0 + "@solana/transaction-confirmation": 5.3.0 + "@solana/transaction-messages": 5.3.0 + "@solana/transactions": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 0ea5b2caf98a823adf57e49f44758cc32f0bfdbaef4eecabf5aaf1239f8566bbe7923fcbe67338731402f2dfc5ddc7b5ba761a45ad588adb9c6d2a613bcadc0b + languageName: node + linkType: hard + "@solana/nominal-types@npm:2.3.0": version: 2.3.0 resolution: "@solana/nominal-types@npm:2.3.0" @@ -12688,6 +13675,33 @@ __metadata: languageName: node linkType: hard +"@solana/nominal-types@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/nominal-types@npm:5.3.0" + peerDependencies: + typescript: ">=5.9.3" + checksum: d4459587363823d04fb55b73366fa484d8738953c41aa88eb4df91a9715926f288320f31d1f90561169c62b809bd951d4ce2b475bd3eb1e97fd9a063ebee1f6c + languageName: node + linkType: hard + +"@solana/offchain-messages@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/offchain-messages@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/codecs-core": 5.3.0 + "@solana/codecs-data-structures": 5.3.0 + "@solana/codecs-numbers": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/keys": 5.3.0 + "@solana/nominal-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: ee40bda429829c64fb07ae3a29dbb0cea587b270abb52a29b22f8f807321b53b6e7cb839d8d53e326a6eccdd034bc5281c60a1e934420145c3630b26d2875379 + languageName: node + linkType: hard + "@solana/options@npm:2.0.0-rc.1": version: 2.0.0-rc.1 resolution: "@solana/options@npm:2.0.0-rc.1" @@ -12718,6 +13732,21 @@ __metadata: languageName: node linkType: hard +"@solana/options@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/options@npm:5.3.0" + dependencies: + "@solana/codecs-core": 5.3.0 + "@solana/codecs-data-structures": 5.3.0 + "@solana/codecs-numbers": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/errors": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 14e8054fdd904acb16cabdd5f89720c41ac881c1ac5f78eff0a9f8fad29efe773e46325633b29c01234fe58403d9d7def8c2a74c66762f567bd7a892ce97feb7 + languageName: node + linkType: hard + "@solana/pay@npm:^0.2.6": version: 0.2.6 resolution: "@solana/pay@npm:0.2.6" @@ -12733,6 +13762,15 @@ __metadata: languageName: node linkType: hard +"@solana/plugin-core@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/plugin-core@npm:5.3.0" + peerDependencies: + typescript: ">=5.3.3" + checksum: 63804038173ed0ba8ca82748079da6f4f9dabbbc0cc0fbf8730a24129b53c1d6dc749cd499d50a04816b4dd9d5184a3a91e0c8675f621ab4927bc2f6ac625619 + languageName: node + linkType: hard + "@solana/programs@npm:2.3.0": version: 2.3.0 resolution: "@solana/programs@npm:2.3.0" @@ -12745,6 +13783,18 @@ __metadata: languageName: node linkType: hard +"@solana/programs@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/programs@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/errors": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: eb71ce27589b63f6abb9b903a3f08d08cd5c2250af6376733273ab17f2dbf66da4528e3850755c8ec72e8a889490b45b50ba6c172eb63d57a8b868274e5a3011 + languageName: node + linkType: hard + "@solana/promises@npm:2.3.0": version: 2.3.0 resolution: "@solana/promises@npm:2.3.0" @@ -12754,6 +13804,15 @@ __metadata: languageName: node linkType: hard +"@solana/promises@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/promises@npm:5.3.0" + peerDependencies: + typescript: ">=5.9.3" + checksum: af7cae93ea2725450bbadef6cfeb79f804de2ccf7999dd22650043d13920cf180716b18fc7f7270e013ae424b1dc5c58fddb3a68dd5c38508fe86a84736795bc + languageName: node + linkType: hard + "@solana/qr-code-styling@npm:^1.6.0": version: 1.6.0 resolution: "@solana/qr-code-styling@npm:1.6.0" @@ -12784,6 +13843,27 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-api@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-api@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/codecs-core": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/keys": 5.3.0 + "@solana/rpc-parsed-types": 5.3.0 + "@solana/rpc-spec": 5.3.0 + "@solana/rpc-transformers": 5.3.0 + "@solana/rpc-types": 5.3.0 + "@solana/transaction-messages": 5.3.0 + "@solana/transactions": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 53e529b3bab88d165ae4f2345f7d870438cfd1678071e0f3453f775b3094b496c3530c09f2160c23541a8615160e5489f4feadcfcc823f2f62199354e2c5d3d9 + languageName: node + linkType: hard + "@solana/rpc-parsed-types@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc-parsed-types@npm:2.3.0" @@ -12793,6 +13873,15 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-parsed-types@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-parsed-types@npm:5.3.0" + peerDependencies: + typescript: ">=5.9.3" + checksum: 4d3e2e00b2809bfa27bfda8f8a0bdfac85becb4395e960a600a2c778e22638981016abf5f0bbc7d2f9102193d2e716f52acdc3ada5b8f1fd966c8b847d28d773 + languageName: node + linkType: hard + "@solana/rpc-spec-types@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc-spec-types@npm:2.3.0" @@ -12802,6 +13891,15 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-spec-types@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-spec-types@npm:5.3.0" + peerDependencies: + typescript: ">=5.9.3" + checksum: 817c1b33e5d2a0dcd95e4d7a69a9e1c30c3cecbd4555f794b59d1c0496a31b388057a4a655582bc0b72a76dbc4279d6661f5685c772e0d105e8a1019543e3faf + languageName: node + linkType: hard + "@solana/rpc-spec@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc-spec@npm:2.3.0" @@ -12814,6 +13912,18 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-spec@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-spec@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + "@solana/rpc-spec-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 696e51ce28abb9f127c0f883c01a49d0ce8ef50dda04b6225592348a1a693e73cd8a52283a74a026f7cca89fdb1dec7d551f92712e8b2f999f9e72abbaed31c4 + languageName: node + linkType: hard + "@solana/rpc-subscriptions-api@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc-subscriptions-api@npm:2.3.0" @@ -12831,6 +13941,23 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-subscriptions-api@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-subscriptions-api@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/keys": 5.3.0 + "@solana/rpc-subscriptions-spec": 5.3.0 + "@solana/rpc-transformers": 5.3.0 + "@solana/rpc-types": 5.3.0 + "@solana/transaction-messages": 5.3.0 + "@solana/transactions": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 3ef2f1004d46a49a08025bd2d3edba800e970f6a888fc5c96ee79e25699488a51d70be02bbab4acda12e2475f4597aa2a9ed1dd2f5896da398fff14fef8d7a9a + languageName: node + linkType: hard + "@solana/rpc-subscriptions-channel-websocket@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc-subscriptions-channel-websocket@npm:2.3.0" @@ -12846,6 +13973,21 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-subscriptions-channel-websocket@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-subscriptions-channel-websocket@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + "@solana/functional": 5.3.0 + "@solana/rpc-subscriptions-spec": 5.3.0 + "@solana/subscribable": 5.3.0 + ws: ^8.18.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 524c523f762fd112e9aa19d1f41db1f8012abd3ae24458d82a9f955726c18544a0c6fe23e335d3b25f05bd1df4aceb3e012c873913f6f952dc0b37939618adc9 + languageName: node + linkType: hard + "@solana/rpc-subscriptions-spec@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc-subscriptions-spec@npm:2.3.0" @@ -12860,6 +14002,20 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-subscriptions-spec@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-subscriptions-spec@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + "@solana/promises": 5.3.0 + "@solana/rpc-spec-types": 5.3.0 + "@solana/subscribable": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 033519dc755acfc2e73460a6fdcfaf65e87c8c91e30ce05b98a384403cafb29d8a63f5a682000c078c3d0b337d16df6f67395283073742ad60a84b890ed913a2 + languageName: node + linkType: hard + "@solana/rpc-subscriptions@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc-subscriptions@npm:2.3.0" @@ -12881,6 +14037,27 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-subscriptions@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-subscriptions@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + "@solana/fast-stable-stringify": 5.3.0 + "@solana/functional": 5.3.0 + "@solana/promises": 5.3.0 + "@solana/rpc-spec-types": 5.3.0 + "@solana/rpc-subscriptions-api": 5.3.0 + "@solana/rpc-subscriptions-channel-websocket": 5.3.0 + "@solana/rpc-subscriptions-spec": 5.3.0 + "@solana/rpc-transformers": 5.3.0 + "@solana/rpc-types": 5.3.0 + "@solana/subscribable": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 6df25dd306cc72438513d0fd941b943d41e097e116f536bf9558c8b4fb2dcffe2d71e23d8a1184efc673f11b7b221436a01766f8805fa80a2a51260e1f80d7aa + languageName: node + linkType: hard + "@solana/rpc-transformers@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc-transformers@npm:2.3.0" @@ -12896,6 +14073,21 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-transformers@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-transformers@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + "@solana/functional": 5.3.0 + "@solana/nominal-types": 5.3.0 + "@solana/rpc-spec-types": 5.3.0 + "@solana/rpc-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 5824c325fc91c6e96dbc1dc69c9f4d74a577099619554574cb67d39ceb42c45a5e40af6aabd667b89c1a91b30a30661c46eecb7334eae7d6cf714171809b15e3 + languageName: node + linkType: hard + "@solana/rpc-transport-http@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc-transport-http@npm:2.3.0" @@ -12910,6 +14102,20 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-transport-http@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-transport-http@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + "@solana/rpc-spec": 5.3.0 + "@solana/rpc-spec-types": 5.3.0 + undici-types: ^7.16.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 41cf2a5ffbfb127d699b1d611d8e960734fe11d626b0bb078c7d8d8dea638e81f28cbda015e33123bac61f2f77017c0a9e985ca089a94e68ca15437b5be3fb66 + languageName: node + linkType: hard + "@solana/rpc-types@npm:2.3.0, @solana/rpc-types@npm:^2.3.0": version: 2.3.0 resolution: "@solana/rpc-types@npm:2.3.0" @@ -12926,6 +14132,22 @@ __metadata: languageName: node linkType: hard +"@solana/rpc-types@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc-types@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/codecs-core": 5.3.0 + "@solana/codecs-numbers": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/nominal-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: da451b92fcfa8021a3b2b81271042aebb00b0de543709bb7c94964e7b3ff60fac4317eff9d7dc79236d218f785b911392d5231aef5d217961bb6ba7069e725c5 + languageName: node + linkType: hard + "@solana/rpc@npm:2.3.0": version: 2.3.0 resolution: "@solana/rpc@npm:2.3.0" @@ -12945,6 +14167,25 @@ __metadata: languageName: node linkType: hard +"@solana/rpc@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/rpc@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + "@solana/fast-stable-stringify": 5.3.0 + "@solana/functional": 5.3.0 + "@solana/rpc-api": 5.3.0 + "@solana/rpc-spec": 5.3.0 + "@solana/rpc-spec-types": 5.3.0 + "@solana/rpc-transformers": 5.3.0 + "@solana/rpc-transport-http": 5.3.0 + "@solana/rpc-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: c1575dc712bd9b44cc1794ab8a913d1891d97705e31a4b229100058a945964f46fd921ac9b482737af92aff0fdb7ac6f48316ab14517d186a6b24271de9e20f2 + languageName: node + linkType: hard + "@solana/signers@npm:2.3.0": version: 2.3.0 resolution: "@solana/signers@npm:2.3.0" @@ -12963,6 +14204,25 @@ __metadata: languageName: node linkType: hard +"@solana/signers@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/signers@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/codecs-core": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/instructions": 5.3.0 + "@solana/keys": 5.3.0 + "@solana/nominal-types": 5.3.0 + "@solana/offchain-messages": 5.3.0 + "@solana/transaction-messages": 5.3.0 + "@solana/transactions": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 5afd0135f61ef8ac121380463cd2814349d008680c1ef78cc255d65b9b1e57ebafa3177cbb76bc9723fdc7dd5734bd44d7dd22ebfd938171626523c55ca81050 + languageName: node + linkType: hard + "@solana/spl-account-compression@npm:^0.1.4, @solana/spl-account-compression@npm:^0.1.8": version: 0.1.10 resolution: "@solana/spl-account-compression@npm:0.1.10" @@ -13070,6 +14330,17 @@ __metadata: languageName: node linkType: hard +"@solana/subscribable@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/subscribable@npm:5.3.0" + dependencies: + "@solana/errors": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 9c728375a2b67420372d023e541e6e02ca4e7ebe631c2692273c4b4c37613b7ffc19876bcd5b78054f39d27452661dde1839c2512da4933514de96dd94ac1c5c + languageName: node + linkType: hard + "@solana/sysvars@npm:2.3.0": version: 2.3.0 resolution: "@solana/sysvars@npm:2.3.0" @@ -13084,6 +14355,20 @@ __metadata: languageName: node linkType: hard +"@solana/sysvars@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/sysvars@npm:5.3.0" + dependencies: + "@solana/accounts": 5.3.0 + "@solana/codecs": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/rpc-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 59637ba828e17d871d06315c9874e482eb678b878e566c0c4f071298e8a6b49060785a36ac970a0b965e3882647dab90c2b55e014708b83a7dc221821a318c31 + languageName: node + linkType: hard + "@solana/transaction-confirmation@npm:2.3.0": version: 2.3.0 resolution: "@solana/transaction-confirmation@npm:2.3.0" @@ -13104,6 +14389,26 @@ __metadata: languageName: node linkType: hard +"@solana/transaction-confirmation@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/transaction-confirmation@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/keys": 5.3.0 + "@solana/promises": 5.3.0 + "@solana/rpc": 5.3.0 + "@solana/rpc-subscriptions": 5.3.0 + "@solana/rpc-types": 5.3.0 + "@solana/transaction-messages": 5.3.0 + "@solana/transactions": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: 68393819e0d8a9f693befd9638134a127c8828ee317f500761a49bd3e0c82bfcd91cff2d62d0f9548550472ea0ee9be598ea4dd43584e378971fc2da0cb06003 + languageName: node + linkType: hard + "@solana/transaction-messages@npm:2.3.0": version: 2.3.0 resolution: "@solana/transaction-messages@npm:2.3.0" @@ -13123,6 +14428,25 @@ __metadata: languageName: node linkType: hard +"@solana/transaction-messages@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/transaction-messages@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/codecs-core": 5.3.0 + "@solana/codecs-data-structures": 5.3.0 + "@solana/codecs-numbers": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/functional": 5.3.0 + "@solana/instructions": 5.3.0 + "@solana/nominal-types": 5.3.0 + "@solana/rpc-types": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: a8830e4d44c0fd62bdcf342d06357952afe74eb2e009e5b9ce8f3259c1c88f31d73a27a63e245214f6b2efaaa67decd58b6ae5b2989f4b8be0a5aa224b186743 + languageName: node + linkType: hard + "@solana/transactions@npm:2.3.0": version: 2.3.0 resolution: "@solana/transactions@npm:2.3.0" @@ -13145,6 +14469,28 @@ __metadata: languageName: node linkType: hard +"@solana/transactions@npm:5.3.0": + version: 5.3.0 + resolution: "@solana/transactions@npm:5.3.0" + dependencies: + "@solana/addresses": 5.3.0 + "@solana/codecs-core": 5.3.0 + "@solana/codecs-data-structures": 5.3.0 + "@solana/codecs-numbers": 5.3.0 + "@solana/codecs-strings": 5.3.0 + "@solana/errors": 5.3.0 + "@solana/functional": 5.3.0 + "@solana/instructions": 5.3.0 + "@solana/keys": 5.3.0 + "@solana/nominal-types": 5.3.0 + "@solana/rpc-types": 5.3.0 + "@solana/transaction-messages": 5.3.0 + peerDependencies: + typescript: ">=5.9.3" + checksum: bd33061f3f05d3dbc1e57baebc4e840afc2f7c2eb69bf32727a306652501bdc81d20329aced472f5d6d52495d720cb51a6d060d4da01934fa6ae39f174ed592c + languageName: node + linkType: hard + "@solana/web3.js@npm:1.95.8, @solana/web3.js@npm:^1.68.0": version: 1.95.8 resolution: "@solana/web3.js@npm:1.95.8" @@ -13238,7 +14584,7 @@ __metadata: languageName: node linkType: hard -"@solana/web3.js@npm:^1.98.2": +"@solana/web3.js@npm:^1.98.1, @solana/web3.js@npm:^1.98.2": version: 1.98.4 resolution: "@solana/web3.js@npm:1.98.4" dependencies: @@ -13563,6 +14909,24 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:5.90.16": + version: 5.90.16 + resolution: "@tanstack/query-core@npm:5.90.16" + checksum: 2aadc3ba9af6625a440f1794e3f75eeab4845c42aa02b17d31fb7324c193329c33fb24620748335c257dcd64bf6a37b46245ee421f508010a52910150f3254bf + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^5.60.0": + version: 5.90.16 + resolution: "@tanstack/react-query@npm:5.90.16" + dependencies: + "@tanstack/query-core": 5.90.16 + peerDependencies: + react: ^18 || ^19 + checksum: 412a729821f3f9945a70a1f350ac0945509d1b35f6ead79ad83881c5648ba4589fed8c7324ffda3f543f56d686ed7d9e008a041f521b2bfe00e8b6f4072a0144 + languageName: node + linkType: hard + "@tanstack/react-query@npm:^5.69.0": version: 5.69.0 resolution: "@tanstack/react-query@npm:5.69.0" @@ -15220,6 +16584,51 @@ __metadata: languageName: node linkType: hard +"@vanilla-extract/css@npm:1.17.3": + version: 1.17.3 + resolution: "@vanilla-extract/css@npm:1.17.3" + dependencies: + "@emotion/hash": ^0.9.0 + "@vanilla-extract/private": ^1.0.8 + css-what: ^6.1.0 + cssesc: ^3.0.0 + csstype: ^3.0.7 + dedent: ^1.5.3 + deep-object-diff: ^1.1.9 + deepmerge: ^4.2.2 + lru-cache: ^10.4.3 + media-query-parser: ^2.0.2 + modern-ahocorasick: ^1.0.0 + picocolors: ^1.0.0 + checksum: 0af0f7532eeea1ea45b8e8214b0567a98d6f63b8422c480c829fe214fb360cc9e4f6a34db259d4682b7fbbbfacba5313f13674b3c1aad69f8fa7ccf90ad14997 + languageName: node + linkType: hard + +"@vanilla-extract/dynamic@npm:2.1.4": + version: 2.1.4 + resolution: "@vanilla-extract/dynamic@npm:2.1.4" + dependencies: + "@vanilla-extract/private": ^1.0.8 + checksum: 832ff943face07ee92882b5e72b812668a0c76caf71644ae638bda2211caf97c40d441d8f9d792c56eda9a76cba4a60e1a85368844b0c937f7b5174b88f6724c + languageName: node + linkType: hard + +"@vanilla-extract/private@npm:^1.0.8": + version: 1.0.9 + resolution: "@vanilla-extract/private@npm:1.0.9" + checksum: 0f47b9faa9dc40c6cf7d2b48c16a6398b870c448db9276850449ecab1059ab10b354c67a574e0b2f6cb54054a20dfa42cf30f2f39e55dc4ab7c6cdc0ffbf7bbe + languageName: node + linkType: hard + +"@vanilla-extract/sprinkles@npm:1.6.4": + version: 1.6.4 + resolution: "@vanilla-extract/sprinkles@npm:1.6.4" + peerDependencies: + "@vanilla-extract/css": ^1.0.0 + checksum: e4fea79a4f49c2b8095892f6d758958548ee8513c7ee27dd1f229c2639bd5271674d117facf79dbdb31d218a64d208e56489dc7a810c9ad30b7454b355298beb + languageName: node + linkType: hard + "@visx/annotation@npm:3.12.0": version: 3.12.0 resolution: "@visx/annotation@npm:3.12.0" @@ -15658,6 +17067,22 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:^4.2.0": + version: 4.7.0 + resolution: "@vitejs/plugin-react@npm:4.7.0" + dependencies: + "@babel/core": ^7.28.0 + "@babel/plugin-transform-react-jsx-self": ^7.27.1 + "@babel/plugin-transform-react-jsx-source": ^7.27.1 + "@rolldown/pluginutils": 1.0.0-beta.27 + "@types/babel__core": ^7.20.5 + react-refresh: ^0.17.0 + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 3e3c4c58f65e8041c4891736613732d572f232bc6b039e74ea00554b94b8010ac246e2f6e099e8b6c474d9c1741e2b166b48fa47fe0093312beefa99a1b13d00 + languageName: node + linkType: hard + "@vitejs/plugin-react@npm:^5.1.0": version: 5.1.0 resolution: "@vitejs/plugin-react@npm:5.1.0" @@ -15786,6 +17211,30 @@ __metadata: languageName: node linkType: hard +"@wagmi/connectors@npm:6.2.0": + version: 6.2.0 + resolution: "@wagmi/connectors@npm:6.2.0" + dependencies: + "@base-org/account": 2.4.0 + "@coinbase/wallet-sdk": 4.3.6 + "@gemini-wallet/core": 0.3.2 + "@metamask/sdk": 0.33.1 + "@safe-global/safe-apps-provider": 0.18.6 + "@safe-global/safe-apps-sdk": 9.1.0 + "@walletconnect/ethereum-provider": 2.21.1 + cbw-sdk: "npm:@coinbase/wallet-sdk@3.9.3" + porto: 0.2.35 + peerDependencies: + "@wagmi/core": 2.22.1 + typescript: ">=5.0.4" + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + checksum: 43881cb0ca5f757754bd3783b355a6e517dbcfe1520a0c91fc12a951ab01fc785012c82618a89f069e70e76228091b06d73f709e0eef7101ab4dbb2ab1385189 + languageName: node + linkType: hard + "@wagmi/core@npm:2.10.2": version: 2.10.2 resolution: "@wagmi/core@npm:2.10.2" @@ -15806,6 +17255,26 @@ __metadata: languageName: node linkType: hard +"@wagmi/core@npm:2.22.1": + version: 2.22.1 + resolution: "@wagmi/core@npm:2.22.1" + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7 + zustand: 5.0.0 + peerDependencies: + "@tanstack/query-core": ">=5.0.0" + typescript: ">=5.0.4" + viem: 2.x + peerDependenciesMeta: + "@tanstack/query-core": + optional: true + typescript: + optional: true + checksum: 5f9621024cb2a86825d6d1c62bb0d795f10dc60a16b4cc9ac78c72533889066a00c37ef8582169d5533d7d4061440397b1fa4cf86e69424036759e3413b6a765 + languageName: node + linkType: hard + "@wallet-standard/base@npm:^1.1.0": version: 1.1.0 resolution: "@wallet-standard/base@npm:1.1.0" @@ -15922,6 +17391,56 @@ __metadata: languageName: node linkType: hard +"@walletconnect/core@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/core@npm:2.21.0" + dependencies: + "@walletconnect/heartbeat": 1.2.2 + "@walletconnect/jsonrpc-provider": 1.0.14 + "@walletconnect/jsonrpc-types": 1.0.4 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/jsonrpc-ws-connection": 1.0.16 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/logger": 2.1.2 + "@walletconnect/relay-api": 1.0.11 + "@walletconnect/relay-auth": 1.1.0 + "@walletconnect/safe-json": 1.0.2 + "@walletconnect/time": 1.0.2 + "@walletconnect/types": 2.21.0 + "@walletconnect/utils": 2.21.0 + "@walletconnect/window-getters": 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + checksum: befd35b7a140af49d470020fa3b88a6ff83a3e10a6c82b6a434f376b5a87c4f0a827186d5322d16b942c404ff691bdf769ec29171a0c3db474a654ddb5d4b0a6 + languageName: node + linkType: hard + +"@walletconnect/core@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/core@npm:2.21.1" + dependencies: + "@walletconnect/heartbeat": 1.2.2 + "@walletconnect/jsonrpc-provider": 1.0.14 + "@walletconnect/jsonrpc-types": 1.0.4 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/jsonrpc-ws-connection": 1.0.16 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/logger": 2.1.2 + "@walletconnect/relay-api": 1.0.11 + "@walletconnect/relay-auth": 1.1.0 + "@walletconnect/safe-json": 1.0.2 + "@walletconnect/time": 1.0.2 + "@walletconnect/types": 2.21.1 + "@walletconnect/utils": 2.21.1 + "@walletconnect/window-getters": 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + checksum: 2ac8f4dca65b51bc449e8677b491d47a64a792929e2d624a8fbe153aac40d84706c16252904ae0050595b4d637f81e4b92ff70e4b063f048e1e069848531ed5a + languageName: node + linkType: hard + "@walletconnect/core@npm:2.21.2": version: 2.21.2 resolution: "@walletconnect/core@npm:2.21.2" @@ -16010,6 +17529,25 @@ __metadata: languageName: node linkType: hard +"@walletconnect/ethereum-provider@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/ethereum-provider@npm:2.21.1" + dependencies: + "@reown/appkit": 1.7.8 + "@walletconnect/jsonrpc-http-connection": 1.0.8 + "@walletconnect/jsonrpc-provider": 1.0.14 + "@walletconnect/jsonrpc-types": 1.0.4 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/sign-client": 2.21.1 + "@walletconnect/types": 2.21.1 + "@walletconnect/universal-provider": 2.21.1 + "@walletconnect/utils": 2.21.1 + events: 3.3.0 + checksum: 97af3b10f6c7fcd8d86bb9c445983e2736b4f590c9170d9075e6345815a6c4a515461b004064aa5c8555c35b2603fd9027e883fe126a7af9ac1d73c9d226d4e2 + languageName: node + linkType: hard + "@walletconnect/ethereum-provider@npm:^2.20.2": version: 2.20.2 resolution: "@walletconnect/ethereum-provider@npm:2.20.2" @@ -16383,6 +17921,40 @@ __metadata: languageName: node linkType: hard +"@walletconnect/sign-client@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/sign-client@npm:2.21.0" + dependencies: + "@walletconnect/core": 2.21.0 + "@walletconnect/events": 1.0.1 + "@walletconnect/heartbeat": 1.2.2 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/logger": 2.1.2 + "@walletconnect/time": 1.0.2 + "@walletconnect/types": 2.21.0 + "@walletconnect/utils": 2.21.0 + events: 3.3.0 + checksum: e68375a367540b443c4571e0d85e02e914a650ca871c9c2381a9c6499ce27af7d6ef881669a034eb3fe7cdad85d7d6475bbb10ccd87ffc57c73d6e999f87d1b9 + languageName: node + linkType: hard + +"@walletconnect/sign-client@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/sign-client@npm:2.21.1" + dependencies: + "@walletconnect/core": 2.21.1 + "@walletconnect/events": 1.0.1 + "@walletconnect/heartbeat": 1.2.2 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/logger": 2.1.2 + "@walletconnect/time": 1.0.2 + "@walletconnect/types": 2.21.1 + "@walletconnect/utils": 2.21.1 + events: 3.3.0 + checksum: 261f4b2f0d17afd9de5652e47312a023126c1330493382068b8f7d42dba6f43d930d6f1d4f61544ec94a05961d666aa182ae9e3dd08814653d52072e0f293c32 + languageName: node + linkType: hard + "@walletconnect/sign-client@npm:2.21.2": version: 2.21.2 resolution: "@walletconnect/sign-client@npm:2.21.2" @@ -16462,6 +18034,34 @@ __metadata: languageName: node linkType: hard +"@walletconnect/types@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/types@npm:2.21.0" + dependencies: + "@walletconnect/events": 1.0.1 + "@walletconnect/heartbeat": 1.2.2 + "@walletconnect/jsonrpc-types": 1.0.4 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/logger": 2.1.2 + events: 3.3.0 + checksum: 9991ebba37ed82cffa7c165054d5c22d05f5d504ba07986c6961d0cf6f67e1a8081725f19d74f48367a0b7072faa9db5a12e5a752a7377b5af64cbf536a30aff + languageName: node + linkType: hard + +"@walletconnect/types@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/types@npm:2.21.1" + dependencies: + "@walletconnect/events": 1.0.1 + "@walletconnect/heartbeat": 1.2.2 + "@walletconnect/jsonrpc-types": 1.0.4 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/logger": 2.1.2 + events: 3.3.0 + checksum: fe76c5bbe28baaeabe308f0c2b82c15388f0609b0138d7f0148f520467660fc7920a1fdea52ce3cf2c830ba3699645492ba9fdd229ad6dda4cb400f4fd114ced + languageName: node + linkType: hard + "@walletconnect/types@npm:2.21.2": version: 2.21.2 resolution: "@walletconnect/types@npm:2.21.2" @@ -16540,6 +18140,46 @@ __metadata: languageName: node linkType: hard +"@walletconnect/universal-provider@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/universal-provider@npm:2.21.0" + dependencies: + "@walletconnect/events": 1.0.1 + "@walletconnect/jsonrpc-http-connection": 1.0.8 + "@walletconnect/jsonrpc-provider": 1.0.14 + "@walletconnect/jsonrpc-types": 1.0.4 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/logger": 2.1.2 + "@walletconnect/sign-client": 2.21.0 + "@walletconnect/types": 2.21.0 + "@walletconnect/utils": 2.21.0 + es-toolkit: 1.33.0 + events: 3.3.0 + checksum: 2d73cf259ab4518d005c0af8d49c6a9142397e212e74fe966263a3843160f3b4be8b1d6e11a8421341dc0595bde443703d619a0e4bc3b731299263d4b43182cd + languageName: node + linkType: hard + +"@walletconnect/universal-provider@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/universal-provider@npm:2.21.1" + dependencies: + "@walletconnect/events": 1.0.1 + "@walletconnect/jsonrpc-http-connection": 1.0.8 + "@walletconnect/jsonrpc-provider": 1.0.14 + "@walletconnect/jsonrpc-types": 1.0.4 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/logger": 2.1.2 + "@walletconnect/sign-client": 2.21.1 + "@walletconnect/types": 2.21.1 + "@walletconnect/utils": 2.21.1 + es-toolkit: 1.33.0 + events: 3.3.0 + checksum: fe75754137f779da299888abfe167c96e7dda5263c11c2c217f8c46107beef4f4c8e38eba445e65a2beb21ba7b491bca86c0ce12596da6b0400faad9468ca40c + languageName: node + linkType: hard + "@walletconnect/utils@npm:2.13.0": version: 2.13.0 resolution: "@walletconnect/utils@npm:2.13.0" @@ -16612,6 +18252,56 @@ __metadata: languageName: node linkType: hard +"@walletconnect/utils@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/utils@npm:2.21.0" + dependencies: + "@noble/ciphers": 1.2.1 + "@noble/curves": 1.8.1 + "@noble/hashes": 1.7.1 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/relay-api": 1.0.11 + "@walletconnect/relay-auth": 1.1.0 + "@walletconnect/safe-json": 1.0.2 + "@walletconnect/time": 1.0.2 + "@walletconnect/types": 2.21.0 + "@walletconnect/window-getters": 1.0.1 + "@walletconnect/window-metadata": 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2 + checksum: 3cd7ad9a8714ec955422832f384d21d0591ac130d0953ed45b34884b05489943a0fff324eaaf916eb058b334e42e338feb421aeca8f7900e470c56ef2a27faf0 + languageName: node + linkType: hard + +"@walletconnect/utils@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/utils@npm:2.21.1" + dependencies: + "@noble/ciphers": 1.2.1 + "@noble/curves": 1.8.1 + "@noble/hashes": 1.7.1 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/relay-api": 1.0.11 + "@walletconnect/relay-auth": 1.1.0 + "@walletconnect/safe-json": 1.0.2 + "@walletconnect/time": 1.0.2 + "@walletconnect/types": 2.21.1 + "@walletconnect/window-getters": 1.0.1 + "@walletconnect/window-metadata": 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2 + checksum: 53e4f31f64ed1d9dadc385f11788ff8a9094b76f72d405a207bb7a22d4a2a1d65412da59e31b504bed13a8b3edb1821868bf64c5ec69c86a4ffced6b7946e413 + languageName: node + linkType: hard + "@walletconnect/utils@npm:2.21.2": version: 2.21.2 resolution: "@walletconnect/utils@npm:2.21.2" @@ -16854,6 +18544,21 @@ __metadata: languageName: node linkType: hard +"abitype@npm:1.0.6": + version: 1.0.6 + resolution: "abitype@npm:1.0.6" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: 0bf6ed5ec785f372746c3ec5d6c87bf4d8cf0b6db30867b8d24e86fbc66d9f6599ae3d463ccd49817e67eedec6deba7cdae317bcf4da85b02bc48009379b9f84 + languageName: node + linkType: hard + "abitype@npm:1.0.8, abitype@npm:^1.0.6": version: 1.0.8 resolution: "abitype@npm:1.0.8" @@ -16884,6 +18589,21 @@ __metadata: languageName: node linkType: hard +"abitype@npm:1.2.3, abitype@npm:^1.2.3": + version: 1.2.3 + resolution: "abitype@npm:1.2.3" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: b5b5620f8e55a6dd7ae829630c0ded02b30f589f0f8f5ca931cdfcf6d7daa8154e30e3fe3593b3f6c4872a955ac55d447ccc2f801fd6a6aa698bdad966e3fe2e + languageName: node + linkType: hard + "abitype@npm:^1.0.9": version: 1.2.0 resolution: "abitype@npm:1.2.0" @@ -17897,7 +19617,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.13.0, axios@npm:^1.6.8, axios@npm:^1.8.4": +"axios@npm:^1.12.2, axios@npm:^1.13.0, axios@npm:^1.6.8, axios@npm:^1.8.4": version: 1.13.2 resolution: "axios@npm:1.13.2" dependencies: @@ -19474,6 +21194,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:5.6.2, chalk@npm:^5.4.1": + version: 5.6.2 + resolution: "chalk@npm:5.6.2" + checksum: 4ee2d47a626d79ca27cb5299ecdcce840ef5755e287412536522344db0fc51ca0f6d6433202332c29e2288c6a90a2b31f3bd626bc8c14743b6b6ee28abd3b796 + languageName: node + linkType: hard + "chalk@npm:^2.0.0, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -19485,13 +21212,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.4.1": - version: 5.6.2 - resolution: "chalk@npm:5.6.2" - checksum: 4ee2d47a626d79ca27cb5299ecdcce840ef5755e287412536522344db0fc51ca0f6d6433202332c29e2288c6a90a2b31f3bd626bc8c14743b6b6ee28abd3b796 - languageName: node - linkType: hard - "chardet@npm:^0.7.0": version: 0.7.0 resolution: "chardet@npm:0.7.0" @@ -19499,6 +21219,13 @@ __metadata: languageName: node linkType: hard +"charenc@npm:0.0.2": + version: 0.0.2 + resolution: "charenc@npm:0.0.2" + checksum: 81dcadbe57e861d527faf6dd3855dc857395a1c4d6781f4847288ab23cffb7b3ee80d57c15bba7252ffe3e5e8019db767757ee7975663ad2ca0939bb8fcaf2e5 + languageName: node + linkType: hard + "check-error@npm:^2.1.1": version: 2.1.1 resolution: "check-error@npm:2.1.1" @@ -19737,14 +21464,14 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^1.1.0, clsx@npm:^1.2.1": +"clsx@npm:1.2.1, clsx@npm:^1.1.0, clsx@npm:^1.2.1": version: 1.2.1 resolution: "clsx@npm:1.2.1" checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 languageName: node linkType: hard -"clsx@npm:^2.1.1": +"clsx@npm:2.1.1, clsx@npm:^2.1.1": version: 2.1.1 resolution: "clsx@npm:2.1.1" checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57 @@ -19853,6 +21580,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:14.0.2": + version: 14.0.2 + resolution: "commander@npm:14.0.2" + checksum: 0a9e549565d368dde2965821833324069b92b099b415c2106996e47db1f0b8c10c77367e9876873c00a52ca627af4c7472eba9b51dc0d6a3ef152ea063d3e9e9 + languageName: node + linkType: hard + "commander@npm:2.9.0": version: 2.9.0 resolution: "commander@npm:2.9.0" @@ -20371,6 +22105,13 @@ __metadata: languageName: node linkType: hard +"crypt@npm:0.0.2": + version: 0.0.2 + resolution: "crypt@npm:0.0.2" + checksum: baf4c7bbe05df656ec230018af8cf7dbe8c14b36b98726939cef008d473f6fe7a4fad906cfea4062c93af516f1550a3f43ceb4d6615329612c6511378ed9fe34 + languageName: node + linkType: hard + "crypto-browserify@npm:3.12.0, crypto-browserify@npm:^3.12.0": version: 3.12.0 resolution: "crypto-browserify@npm:3.12.0" @@ -20474,6 +22215,22 @@ __metadata: languageName: node linkType: hard +"css-what@npm:^6.1.0": + version: 6.2.2 + resolution: "css-what@npm:6.2.2" + checksum: 4d1f07b348a638e1f8b4c72804a1e93881f35e0f541256aec5ac0497c5855df7db7ab02da030de950d4813044f6d029a14ca657e0f92c3987e4b604246235b2b + languageName: node + linkType: hard + +"cssesc@npm:^3.0.0": + version: 3.0.0 + resolution: "cssesc@npm:3.0.0" + bin: + cssesc: bin/cssesc + checksum: f8c4ababffbc5e2ddf2fa9957dda1ee4af6048e22aeda1869d0d00843223c1b13ad3f5d88b51caa46c994225eacb636b764eb807a8883e2fb6f99b4f4e8c48b2 + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.0 resolution: "csstype@npm:3.1.0" @@ -20481,6 +22238,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.0.7": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: cb882521b3398958a1ce6ca98c011aec0bde1c77ecaf8a1dd4db3b112a189939beae3b1308243b2fe50fc27eb3edeb0f73a5a4d91d928765dc6d5ecc7bda92ee + languageName: node + linkType: hard + "csstype@npm:^3.1.2": version: 3.1.2 resolution: "csstype@npm:3.1.2" @@ -20521,6 +22285,22 @@ __metadata: languageName: node linkType: hard +"cuer@npm:0.0.3": + version: 0.0.3 + resolution: "cuer@npm:0.0.3" + dependencies: + qr: ~0 + peerDependencies: + react: ">=18" + react-dom: ">=18" + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: e86cc8cb7a6b291c5902e54ce4836c31dd4cce1df09dcb1ac691972392e1c7bd39bdaa1eeac17c9397fd7535d62904c522b8f1d3fc1cd6c41233d6bb247f6486 + languageName: node + linkType: hard + "cwise-compiler@npm:^1.1.2": version: 1.1.3 resolution: "cwise-compiler@npm:1.1.3" @@ -20828,7 +22608,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": +"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -20908,6 +22688,18 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^1.5.3": + version: 1.7.1 + resolution: "dedent@npm:1.7.1" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 66dc34f61dabc85597a95ce8678c93f0793ec437cc6510e0e6c14da159ce15c6209dee483aa3cccb3238a2f708382c4d26eeb1a47a4c1831a0b7bb56873041cf + languageName: node + linkType: hard + "deep-eql@npm:^5.0.1": version: 5.0.2 resolution: "deep-eql@npm:5.0.2" @@ -20929,6 +22721,13 @@ __metadata: languageName: node linkType: hard +"deep-object-diff@npm:^1.1.9": + version: 1.1.9 + resolution: "deep-object-diff@npm:1.1.9" + checksum: ecd42455e4773f653595d28070295e7aaa8402db5f8ab21d0bec115a7cb4de5e207a5665514767da5f025c96597f1d3a0a4888aeb4dd49e03c996871a3aa05ef + languageName: node + linkType: hard + "deepmerge@npm:^4.2.2": version: 4.2.2 resolution: "deepmerge@npm:4.2.2" @@ -21435,6 +23234,18 @@ __metadata: languageName: node linkType: hard +"eciesjs@npm:^0.4.11": + version: 0.4.16 + resolution: "eciesjs@npm:0.4.16" + dependencies: + "@ecies/ciphers": ^0.2.4 + "@noble/ciphers": ^1.3.0 + "@noble/curves": ^1.9.7 + "@noble/hashes": ^1.8.0 + checksum: 1c34a4356fbc1b502c41960ac12ac7b40cbe686afdbcf188c92ad5b3d48d9adb9f931bb5e096bf822841d44ec35943fa56c113a3edec9facd108964599e258b8 + languageName: node + linkType: hard + "ecpair@npm:^3.0.0-rc.0": version: 3.0.0-rc.0 resolution: "ecpair@npm:3.0.0-rc.0" @@ -22227,6 +24038,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.21.3": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": 0.21.5 + "@esbuild/android-arm": 0.21.5 + "@esbuild/android-arm64": 0.21.5 + "@esbuild/android-x64": 0.21.5 + "@esbuild/darwin-arm64": 0.21.5 + "@esbuild/darwin-x64": 0.21.5 + "@esbuild/freebsd-arm64": 0.21.5 + "@esbuild/freebsd-x64": 0.21.5 + "@esbuild/linux-arm": 0.21.5 + "@esbuild/linux-arm64": 0.21.5 + "@esbuild/linux-ia32": 0.21.5 + "@esbuild/linux-loong64": 0.21.5 + "@esbuild/linux-mips64el": 0.21.5 + "@esbuild/linux-ppc64": 0.21.5 + "@esbuild/linux-riscv64": 0.21.5 + "@esbuild/linux-s390x": 0.21.5 + "@esbuild/linux-x64": 0.21.5 + "@esbuild/netbsd-x64": 0.21.5 + "@esbuild/openbsd-x64": 0.21.5 + "@esbuild/sunos-x64": 0.21.5 + "@esbuild/win32-arm64": 0.21.5 + "@esbuild/win32-ia32": 0.21.5 + "@esbuild/win32-x64": 0.21.5 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 2911c7b50b23a9df59a7d6d4cdd3a4f85855787f374dce751148dbb13305e0ce7e880dde1608c2ab7a927fc6cec3587b80995f7fc87a64b455f8b70b55fd8ec1 + languageName: node + linkType: hard + "esbuild@npm:^0.24.0": version: 0.24.2 resolution: "esbuild@npm:0.24.2" @@ -23449,7 +25340,7 @@ __metadata: languageName: node linkType: hard -"eventemitter2@npm:^6.4.7": +"eventemitter2@npm:^6.4.7, eventemitter2@npm:^6.4.9": version: 6.4.9 resolution: "eventemitter2@npm:6.4.9" checksum: be59577c1e1c35509c7ba0e2624335c35bbcfd9485b8a977384c6cc6759341ea1a98d3cb9dbaa5cea4fff9b687e504504e3f9c2cc1674cf3bd8a43a7c74ea3eb @@ -25103,6 +26994,13 @@ __metadata: languageName: node linkType: hard +"hono@npm:^4.10.3": + version: 4.11.3 + resolution: "hono@npm:4.11.3" + checksum: 52e5ecf5229ad1fbebdd8edfe674d13316ee088085f0ae258a8d55c5211c551d5643cb203073e16485974a4d9e840a7d973a8238c7b4e1ed652fc844da464888 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -25364,6 +27262,13 @@ __metadata: languageName: node linkType: hard +"idb-keyval@npm:6.2.1, idb-keyval@npm:^6.2.1": + version: 6.2.1 + resolution: "idb-keyval@npm:6.2.1" + checksum: 7c0836f832096086e99258167740181132a71dd2694c8b8454a4f5ec69114ba6d70983115153306f0b6de1c8d3bad04f67eed3dff8f50c96815b9985d6d78470 + languageName: node + linkType: hard + "idb-keyval@npm:^6.0.3": version: 6.1.0 resolution: "idb-keyval@npm:6.1.0" @@ -25373,13 +27278,6 @@ __metadata: languageName: node linkType: hard -"idb-keyval@npm:^6.2.1": - version: 6.2.1 - resolution: "idb-keyval@npm:6.2.1" - checksum: 7c0836f832096086e99258167740181132a71dd2694c8b8454a4f5ec69114ba6d70983115153306f0b6de1c8d3bad04f67eed3dff8f50c96815b9985d6d78470 - languageName: node - linkType: hard - "idna-uts46-hx@npm:^2.3.1": version: 2.3.1 resolution: "idna-uts46-hx@npm:2.3.1" @@ -25839,7 +27737,7 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^1.0.2": +"is-buffer@npm:^1.0.2, is-buffer@npm:~1.1.6": version: 1.1.6 resolution: "is-buffer@npm:1.1.6" checksum: 4a186d995d8bbf9153b4bd9ff9fd04ae75068fe695d29025d25e592d9488911eeece84eefbd8fa41b8ddcc0711058a71d4c466dcf6f1f6e1d83830052d8ca707 @@ -26657,6 +28555,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^6.0.8": + version: 6.1.3 + resolution: "jose@npm:6.1.3" + checksum: 7f51c7e77f82b70ef88ede9fd1760298bc0ffbf143b9d94f78c08462987ae61864535c1856bc6c26d335f857c7d41f4fffcc29134212c19ea929ce34a4c790f0 + languageName: node + linkType: hard + "jpeg-js@npm:^0.4.1": version: 0.4.4 resolution: "jpeg-js@npm:0.4.4" @@ -27323,6 +29228,17 @@ __metadata: languageName: node linkType: hard +"lit-element@npm:^4.2.0": + version: 4.2.2 + resolution: "lit-element@npm:4.2.2" + dependencies: + "@lit-labs/ssr-dom-shim": ^1.5.0 + "@lit/reactive-element": ^2.1.0 + lit-html: ^3.3.0 + checksum: 554254b87c7a5b486351a4aecb78919f3665d91de0466c85041572e37f94dd4887f7a4765a90a59e3933f1a54fcf68c20a411319bf06c3e8dce6cfac8de4ca30 + languageName: node + linkType: hard + "lit-html@npm:^2.6.1": version: 2.7.0 resolution: "lit-html@npm:2.7.0" @@ -27372,6 +29288,17 @@ __metadata: languageName: node linkType: hard +"lit@npm:3.3.0": + version: 3.3.0 + resolution: "lit@npm:3.3.0" + dependencies: + "@lit/reactive-element": ^2.1.0 + lit-element: ^4.2.0 + lit-html: ^3.3.0 + checksum: 9b9b1ee6c9283ad2995cc7b3db1ad06ba218b42f31bd53d47ff28ab7959aa5fd9620187ac2df706d307e2bd51ae3f5ff4d21a7a2a86745e1bf78ac05dbd56573 + languageName: node + linkType: hard + "litecoin-regex@npm:^1.0.8": version: 1.0.9 resolution: "litecoin-regex@npm:1.0.9" @@ -27579,6 +29506,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^10.4.3": + version: 10.4.3 + resolution: "lru-cache@npm:10.4.3" + checksum: 6476138d2125387a6d20f100608c2583d415a4f64a0fecf30c9e2dda976614f09cad4baa0842447bd37dd459a7bd27f57d9d8f8ce558805abd487c583f3d774a + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -27742,6 +29676,26 @@ __metadata: languageName: node linkType: hard +"md5@npm:^2.3.0": + version: 2.3.0 + resolution: "md5@npm:2.3.0" + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: ~1.1.6 + checksum: a63cacf4018dc9dee08c36e6f924a64ced735b37826116c905717c41cebeb41a522f7a526ba6ad578f9c80f02cb365033ccd67fe186ffbcc1a1faeb75daa9b6e + languageName: node + linkType: hard + +"media-query-parser@npm:^2.0.2": + version: 2.0.2 + resolution: "media-query-parser@npm:2.0.2" + dependencies: + "@babel/runtime": ^7.12.5 + checksum: 8ef956d9e63fe6f4041988beda69843b3a6bb48228ea2923a066f6e7c8f7c5dba75fae357318c48a97ed5beae840b8425cb7e727fc1bb77acc65f2005f8945ab + languageName: node + linkType: hard + "memdown@npm:^1.0.0": version: 1.4.1 resolution: "memdown@npm:1.4.1" @@ -28163,7 +30117,7 @@ __metadata: languageName: node linkType: hard -"mipd@npm:^0.0.7": +"mipd@npm:0.0.7, mipd@npm:^0.0.7": version: 0.0.7 resolution: "mipd@npm:0.0.7" peerDependencies: @@ -28226,6 +30180,13 @@ __metadata: languageName: node linkType: hard +"modern-ahocorasick@npm:^1.0.0": + version: 1.1.0 + resolution: "modern-ahocorasick@npm:1.1.0" + checksum: 78b99840c9af086c1e36a594ee85bebd8c19d48e2ef31a67d1bad0e673ac12fc931e5961abb5b16daaf820af4923e700f76b1793b7413e18782230162866a0af + languageName: node + linkType: hard + "module-alias@npm:^2.2.3": version: 2.2.3 resolution: "module-alias@npm:2.2.3" @@ -29292,6 +31253,22 @@ __metadata: languageName: node linkType: hard +"openapi-fetch@npm:^0.13.5": + version: 0.13.8 + resolution: "openapi-fetch@npm:0.13.8" + dependencies: + openapi-typescript-helpers: ^0.0.15 + checksum: 8bdca4befdaa6106cbe1feb7aa211328f724f260ace5e37b69640442d28b63fdf59170eeda001757f9745a1e6c1e857d597848976eca2b6e16db1cd01d1c5575 + languageName: node + linkType: hard + +"openapi-typescript-helpers@npm:^0.0.15": + version: 0.0.15 + resolution: "openapi-typescript-helpers@npm:0.0.15" + checksum: feec0f25d708aaacc086dafd3e21001329b47984f30e24877b34d053fbccd52f3b3a19b1715fda2ed5afaca7056872576eed7de8f64855d2472507dc6df626ca + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.3 resolution: "optionator@npm:0.9.3" @@ -29378,6 +31355,27 @@ __metadata: languageName: node linkType: hard +"ox@npm:0.11.3": + version: 0.11.3 + resolution: "ox@npm:0.11.3" + dependencies: + "@adraffy/ens-normalize": ^1.11.0 + "@noble/ciphers": ^1.3.0 + "@noble/curves": 1.9.1 + "@noble/hashes": ^1.8.0 + "@scure/bip32": ^1.7.0 + "@scure/bip39": ^1.6.0 + abitype: ^1.2.3 + eventemitter3: 5.0.1 + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: a3245ecf26451fe681f094ad3d082b2b59f57f52ff02a8d74b2e8e21e95e612328785efb1f65222927fdc5a2670b4965ff2372fcef8daf90effd5744bcfa3262 + languageName: node + linkType: hard + "ox@npm:0.6.7": version: 0.6.7 resolution: "ox@npm:0.6.7" @@ -29480,6 +31478,27 @@ __metadata: languageName: node linkType: hard +"ox@npm:^0.9.6": + version: 0.9.17 + resolution: "ox@npm:0.9.17" + dependencies: + "@adraffy/ens-normalize": ^1.11.0 + "@noble/ciphers": ^1.3.0 + "@noble/curves": 1.9.1 + "@noble/hashes": ^1.8.0 + "@scure/bip32": ^1.7.0 + "@scure/bip39": ^1.6.0 + abitype: ^1.0.9 + eventemitter3: 5.0.1 + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: d5ec8d0d6f141a5e7b52613050dd95a745dc751b689e4f5a6faf99e16fc9b5193241e92aa1cd127351769968b41db1718c4738b210464bffa0538aa300a50a07 + languageName: node + linkType: hard + "p-debounce@npm:^4.0.0": version: 4.0.0 resolution: "p-debounce@npm:4.0.0" @@ -30024,6 +32043,50 @@ __metadata: languageName: node linkType: hard +"porto@npm:0.2.35": + version: 0.2.35 + resolution: "porto@npm:0.2.35" + dependencies: + hono: ^4.10.3 + idb-keyval: ^6.2.1 + mipd: ^0.0.7 + ox: ^0.9.6 + zod: ^4.1.5 + zustand: ^5.0.1 + peerDependencies: + "@tanstack/react-query": ">=5.59.0" + "@wagmi/core": ">=2.16.3" + expo-auth-session: ">=7.0.8" + expo-crypto: ">=15.0.7" + expo-web-browser: ">=15.0.8" + react: ">=18" + react-native: ">=0.81.4" + typescript: ">=5.4.0" + viem: ">=2.37.0" + wagmi: ">=2.0.0" + peerDependenciesMeta: + "@tanstack/react-query": + optional: true + expo-auth-session: + optional: true + expo-crypto: + optional: true + expo-web-browser: + optional: true + react: + optional: true + react-native: + optional: true + typescript: + optional: true + wagmi: + optional: true + bin: + porto: dist/cli/bin/index.js + checksum: abf70fa7867c7e075f0cbc3c3281fed6611f35026a6fe9cdcfaf15ec3db9a7dd109ad8b087126923f97bfdb49114b48e5715292addaf38b4105b656345653509 + languageName: node + linkType: hard + "poseidon-lite@npm:0.2.1": version: 0.2.1 resolution: "poseidon-lite@npm:0.2.1" @@ -30056,7 +32119,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.38": +"postcss@npm:^8.4.38, postcss@npm:^8.4.43": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -30078,6 +32141,13 @@ __metadata: languageName: node linkType: hard +"preact@npm:10.24.2": + version: 10.24.2 + resolution: "preact@npm:10.24.2" + checksum: 429584bbe65d5322b4cd449abd54d61d777f329a23badead36ad510f91d04f42d0615ad2bc4d5e80c3c531be53081932a027ee5f2d6f2805e10666f2ac3d70db + languageName: node + linkType: hard + "preact@npm:10.4.1": version: 10.4.1 resolution: "preact@npm:10.4.1" @@ -30432,6 +32502,13 @@ pvutils@latest: languageName: node linkType: hard +"qr@npm:~0": + version: 0.5.4 + resolution: "qr@npm:0.5.4" + checksum: 8d662c3e74484f22df71e299818501ef1abf42f7f8eb76a0d46daa12839031623ec0ca1de85e5a6d3298fbfcb29065563e8176c68920ea2f294100c214bda72a + languageName: node + linkType: hard + "qrcode-generator@npm:^1.4.3": version: 1.4.4 resolution: "qrcode-generator@npm:1.4.4" @@ -30687,6 +32764,18 @@ pvutils@latest: languageName: node linkType: hard +"react-dom@npm:18.2.0": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.0 + peerDependencies: + react: ^18.2.0 + checksum: 7d323310bea3a91be2965f9468d552f201b1c27891e45ddc2d6b8f717680c95a75ae0bc1e3f5cf41472446a2589a75aed4483aee8169287909fcd59ad149e8cc + languageName: node + linkType: hard + "react-dom@npm:18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" @@ -30710,6 +32799,18 @@ pvutils@latest: languageName: node linkType: hard +"react-dom@patch:react-dom@npm%3A18.2.0#./.yarn/patches/react-dom-npm-18.2.0-dd675bca1c.patch::locator=%40shapeshiftoss%2Fweb%40workspace%3A.": + version: 18.2.0 + resolution: "react-dom@patch:react-dom@npm%3A18.2.0#./.yarn/patches/react-dom-npm-18.2.0-dd675bca1c.patch::version=18.2.0&hash=6969d0&locator=%40shapeshiftoss%2Fweb%40workspace%3A." + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.0 + peerDependencies: + react: ^18.2.0 + checksum: 5c26ed7fa39bc8d6a9b9266d693b32d03436014b60e14518720587cb416ab29fbad60db16217f786ef8c8da40a369fbac57483024eba68c188f97d2eeaf0571f + languageName: node + linkType: hard + "react-error-boundary@npm:^3.1.4": version: 3.1.4 resolution: "react-error-boundary@npm:3.1.4" @@ -30912,6 +33013,13 @@ pvutils@latest: languageName: node linkType: hard +"react-refresh@npm:^0.17.0": + version: 0.17.0 + resolution: "react-refresh@npm:0.17.0" + checksum: e9d23a70543edde879263976d7909cd30c6f698fa372a1240142cf7c8bf99e0396378b9c07c2d39c3a10261d7ba07dc49f990cd8f1ac7b88952e99040a0be5e9 + languageName: node + linkType: hard + "react-refresh@npm:^0.18.0": version: 0.18.0 resolution: "react-refresh@npm:0.18.0" @@ -30935,6 +33043,25 @@ pvutils@latest: languageName: node linkType: hard +"react-remove-scroll@npm:2.6.2": + version: 2.6.2 + resolution: "react-remove-scroll@npm:2.6.2" + dependencies: + react-remove-scroll-bar: ^2.3.7 + react-style-singleton: ^2.2.1 + tslib: ^2.1.0 + use-callback-ref: ^1.3.3 + use-sidecar: ^1.1.2 + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 310e6e6d2f28226a1751dc5084a2dce49167f0b69e3d78d6510f329f423ee313d4f6477f5e1adccb68baef40a7af75541e980a8c398cb82ea0d3573e514e8124 + languageName: node + linkType: hard + "react-remove-scroll@npm:^2.5.7, react-remove-scroll@npm:^2.6.2": version: 2.6.3 resolution: "react-remove-scroll@npm:2.6.3" @@ -31046,7 +33173,7 @@ pvutils@latest: languageName: node linkType: hard -"react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3": +"react-style-singleton@npm:^2.2.1, react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3": version: 2.2.3 resolution: "react-style-singleton@npm:2.2.3" dependencies: @@ -31129,7 +33256,7 @@ pvutils@latest: languageName: node linkType: hard -"react@npm:18.3.1": +"react@npm:18.3.1, react@npm:^18.2.0": version: 18.3.1 resolution: "react@npm:18.3.1" dependencies: @@ -31990,6 +34117,96 @@ pvutils@latest: languageName: node linkType: hard +"rollup@npm:^4.20.0": + version: 4.55.1 + resolution: "rollup@npm:4.55.1" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.55.1 + "@rollup/rollup-android-arm64": 4.55.1 + "@rollup/rollup-darwin-arm64": 4.55.1 + "@rollup/rollup-darwin-x64": 4.55.1 + "@rollup/rollup-freebsd-arm64": 4.55.1 + "@rollup/rollup-freebsd-x64": 4.55.1 + "@rollup/rollup-linux-arm-gnueabihf": 4.55.1 + "@rollup/rollup-linux-arm-musleabihf": 4.55.1 + "@rollup/rollup-linux-arm64-gnu": 4.55.1 + "@rollup/rollup-linux-arm64-musl": 4.55.1 + "@rollup/rollup-linux-loong64-gnu": 4.55.1 + "@rollup/rollup-linux-loong64-musl": 4.55.1 + "@rollup/rollup-linux-ppc64-gnu": 4.55.1 + "@rollup/rollup-linux-ppc64-musl": 4.55.1 + "@rollup/rollup-linux-riscv64-gnu": 4.55.1 + "@rollup/rollup-linux-riscv64-musl": 4.55.1 + "@rollup/rollup-linux-s390x-gnu": 4.55.1 + "@rollup/rollup-linux-x64-gnu": 4.55.1 + "@rollup/rollup-linux-x64-musl": 4.55.1 + "@rollup/rollup-openbsd-x64": 4.55.1 + "@rollup/rollup-openharmony-arm64": 4.55.1 + "@rollup/rollup-win32-arm64-msvc": 4.55.1 + "@rollup/rollup-win32-ia32-msvc": 4.55.1 + "@rollup/rollup-win32-x64-gnu": 4.55.1 + "@rollup/rollup-win32-x64-msvc": 4.55.1 + "@types/estree": 1.0.8 + fsevents: ~2.3.2 + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: fd5374cd7e6046404d59d64e1346821fee0900bc9cac021078bfdd342fc54305defba882fb53e6543b5c17ce4573b30fb45ee28b9a4122548358004efdc550d2 + languageName: node + linkType: hard + "rollup@npm:^4.30.1": version: 4.34.8 resolution: "rollup@npm:4.34.8" @@ -32435,7 +34652,7 @@ pvutils@latest: languageName: node linkType: hard -"scheduler@npm:^0.23.2": +"scheduler@npm:^0.23.0, scheduler@npm:^0.23.2": version: 0.23.2 resolution: "scheduler@npm:0.23.2" dependencies: @@ -34795,6 +37012,15 @@ pvutils@latest: languageName: node linkType: hard +"ua-parser-js@npm:^1.0.37": + version: 1.0.41 + resolution: "ua-parser-js@npm:1.0.41" + bin: + ua-parser-js: script/cli.js + checksum: a57c258ea3a242ade7601460ddf9a7e990d8d8bffc15df2ca87057a81993ca19f5045432c744d07bf2d9f280665d84aebb08630c5af5bea3922fdbe8f6fe6cb0 + languageName: node + linkType: hard + "ua-parser-js@npm:^2.0.4": version: 2.0.6 resolution: "ua-parser-js@npm:2.0.6" @@ -34910,6 +37136,13 @@ pvutils@latest: languageName: node linkType: hard +"undici-types@npm:^7.16.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 23da306c8366574adec305b06a8519ab5c7d09e3f5d16c1a98709a34fae17da09ec95198f30f86c00055e02efa8bfcc843e84e8aebeb9b8d6bb3e06afccae07a + languageName: node + linkType: hard + "undici-types@npm:~6.19.2": version: 6.19.8 resolution: "undici-types@npm:6.19.8" @@ -35281,7 +37514,7 @@ pvutils@latest: languageName: node linkType: hard -"use-sidecar@npm:^1.1.3": +"use-sidecar@npm:^1.1.2, use-sidecar@npm:^1.1.3": version: 1.1.3 resolution: "use-sidecar@npm:1.1.3" dependencies: @@ -35306,6 +37539,15 @@ pvutils@latest: languageName: node linkType: hard +"use-sync-external-store@npm:1.4.0": + version: 1.4.0 + resolution: "use-sync-external-store@npm:1.4.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: dc3843a1b59ac8bd01417bd79498d4c688d5df8bf4801be50008ef4bfaacb349058c0b1605b5b43c828e0a2d62722d7e861573b3f31cea77a7f23e8b0fc2f7e3 + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.4.0": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" @@ -35684,6 +37926,27 @@ pvutils@latest: languageName: node linkType: hard +"viem@npm:>=2.29.0, viem@npm:^2.1.1, viem@npm:^2.21.0, viem@npm:^2.21.26, viem@npm:^2.27.2, viem@npm:^2.31.7": + version: 2.44.1 + resolution: "viem@npm:2.44.1" + dependencies: + "@noble/curves": 1.9.1 + "@noble/hashes": 1.8.0 + "@scure/bip32": 1.7.0 + "@scure/bip39": 1.6.0 + abitype: 1.2.3 + isows: 1.0.7 + ox: 0.11.3 + ws: 8.18.3 + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: fa06705f341084fac0923219ef16533f1866772e70e0a78b5d2d59753526a2842f47b2cd44d3577d5613d90461481d1577c43d91d2a07672f87e3dc612595f11 + languageName: node + linkType: hard + "viem@npm:^1.0.0, viem@npm:^1.1.4": version: 1.21.4 resolution: "viem@npm:1.21.4" @@ -35859,6 +38122,49 @@ pvutils@latest: languageName: node linkType: hard +"vite@npm:^5.0.0": + version: 5.4.21 + resolution: "vite@npm:5.4.21" + dependencies: + esbuild: ^0.21.3 + fsevents: ~2.3.3 + postcss: ^8.4.43 + rollup: ^4.20.0 + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 7177fa03cff6a382f225290c9889a0d0e944d17eab705bcba89b58558a6f7adfa1f47e469b88f42a044a0eb40c12a1bf68b3cb42abb5295d04f9d7d4dd320837 + languageName: node + linkType: hard + "vite@npm:^5.0.0 || ^6.0.0": version: 6.2.6 resolution: "vite@npm:6.2.6" @@ -35985,6 +38291,25 @@ pvutils@latest: languageName: node linkType: hard +"wagmi@npm:^2.14.0": + version: 2.19.5 + resolution: "wagmi@npm:2.19.5" + dependencies: + "@wagmi/connectors": 6.2.0 + "@wagmi/core": 2.22.1 + use-sync-external-store: 1.4.0 + peerDependencies: + "@tanstack/react-query": ">=5.0.0" + react: ">=18" + typescript: ">=5.0.4" + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + checksum: 6a0b0f7f91ddf37f6d7f6db919afa1d512a0bca693606552f902761f37e20e1ef154f9664fd1291cb02c71dd19b02c06ebf91f70f99318d71a705cefe3454cb3 + languageName: node + linkType: hard + "wagmi@npm:^2.9.2": version: 2.9.2 resolution: "wagmi@npm:2.9.2" @@ -36993,13 +39318,20 @@ pvutils@latest: languageName: node linkType: hard -"zod@npm:^3.23.8": +"zod@npm:^3.23.8, zod@npm:^3.24.4": version: 3.25.76 resolution: "zod@npm:3.25.76" checksum: c9a403a62b329188a5f6bd24d5d935d2bba345f7ab8151d1baa1505b5da9f227fb139354b043711490c798e91f3df75991395e40142e6510a4b16409f302b849 languageName: node linkType: hard +"zod@npm:^4.1.5": + version: 4.3.5 + resolution: "zod@npm:4.3.5" + checksum: 68691183a91c67c4102db20139f3b5af288c59b4b11eb2239d712aae99dc6c1cecaeebcb0c012b44489771be05fecba21e79f65af4b3163b220239ef0af3ec49 + languageName: node + linkType: hard + "zod@npm:^4.2.1": version: 4.2.1 resolution: "zod@npm:4.2.1" @@ -37026,3 +39358,66 @@ pvutils@latest: checksum: 80acd0fbf633782996642802c8692bbb80ae5c80a8dff4c501b88250acd5ccd468fbc6398bdce198475a25e3839c91385b81da921274f33ffb5c2d08c3eab400 languageName: node linkType: hard + +"zustand@npm:5.0.0": + version: 5.0.0 + resolution: "zustand@npm:5.0.0" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: dc7414de234f9d2c0afad472d6971e9ac32281292faa8ee0910521cad063f84eeeb6f792efab068d6750dab5854fb1a33ac6e9294b796925eb680a59fc1b42f9 + languageName: node + linkType: hard + +"zustand@npm:5.0.3": + version: 5.0.3 + resolution: "zustand@npm:5.0.3" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: 72da39ac3017726c3562c615a0f76cee0c9ea678d664f82ee7669f8cb5e153ee81059363473094e4154d73a2935ee3459f6792d1ec9d08d2e72ebe641a16a6ba + languageName: node + linkType: hard + +"zustand@npm:^5.0.1": + version: 5.0.10 + resolution: "zustand@npm:5.0.10" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: 52d39ad5a0a496a443ced50e773a47df4bda4f718c96e45a08c92675e45d7ac77ce75903b8e3754f17a2e99c71f5864ae8c2b2477aeb4c6f5c2a19e3e64e57ba + languageName: node + linkType: hard From 1d9106187f8950e412ca5a002c6dda83b541511f Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:26:00 +0100 Subject: [PATCH 02/41] feat: widget poc --- packages/swap-widget-poc/railway.json | 12 +++ .../src/hooks/useMarketData.ts | 83 +++++++++++++++---- packages/swap-widget-poc/vite.config.ts | 38 +++++---- 3 files changed, 101 insertions(+), 32 deletions(-) create mode 100644 packages/swap-widget-poc/railway.json diff --git a/packages/swap-widget-poc/railway.json b/packages/swap-widget-poc/railway.json new file mode 100644 index 00000000000..af5558d69d8 --- /dev/null +++ b/packages/swap-widget-poc/railway.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "NIXPACKS", + "buildCommand": "corepack enable && yarn install && yarn workspace @shapeshiftoss/swap-widget-poc build" + }, + "deploy": { + "startCommand": "yarn workspace @shapeshiftoss/swap-widget-poc preview --host --port ${PORT:-3000}", + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 10 + } +} diff --git a/packages/swap-widget-poc/src/hooks/useMarketData.ts b/packages/swap-widget-poc/src/hooks/useMarketData.ts index da87ffc1aee..7680525957e 100644 --- a/packages/swap-widget-poc/src/hooks/useMarketData.ts +++ b/packages/swap-widget-poc/src/hooks/useMarketData.ts @@ -2,8 +2,8 @@ import { useQuery } from "@tanstack/react-query"; import { adapters } from "@shapeshiftoss/caip"; import type { AssetId } from "../types"; -const MARKET_DATA_STALE_TIME = 5 * 60 * 1000; -const COINGECKO_API_URL = "https://api.coingecko.com/api/v3"; +const MARKET_DATA_STALE_TIME = 10 * 60 * 1000; +const MARKET_DATA_GC_TIME = 60 * 60 * 1000; type MarketData = { price: string; @@ -23,25 +23,70 @@ type CoinGeckoMarketCap = { 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; + if (response.status === 429 && i < retries) { + await new Promise((r) => setTimeout(r, delay * (i + 1))); + continue; + } + return response; + } catch (error) { + if (i === retries) throw error; + await new Promise((r) => setTimeout(r, delay * (i + 1))); + } + } + 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 fetchAllMarketData = async (): Promise => { const result: MarketDataById = {}; - const maxPerPage = 250; - const totalPages = 4; + const perPage = 250; + const totalPages = 2; + + let baseUrl = COINGECKO_PROXY_URL; + + const testResponse = await fetch( + `${COINGECKO_PROXY_URL}/coins/markets?vs_currency=usd&per_page=1&page=1`, + ).catch(() => null); + + if (!testResponse?.ok) { + baseUrl = COINGECKO_DIRECT_URL; + } try { - const allData = await Promise.all( - Array.from({ length: totalPages }, (_, i) => i + 1).map(async (page) => { - const response = await fetch( - `${COINGECKO_API_URL}/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=${maxPerPage}&page=${page}&sparkline=false`, - ); - if (!response.ok) return []; - return response.json() as Promise; - }), - ); - - const flatData = allData.flat(); - - for (const asset of flatData) { + const allData: CoinGeckoMarketCap[] = []; + + for (let page = 1; page <= totalPages; page++) { + const pageData = await fetchMarketsPage(baseUrl, page, perPage); + allData.push(...pageData); + + if (page < totalPages) { + await new Promise((r) => setTimeout(r, 500)); + } + } + + for (const asset of allData) { const assetIds = adapters.coingeckoToAssetIds(asset.id); if (!assetIds?.length) continue; @@ -68,7 +113,9 @@ export const useAllMarketData = () => { queryKey: ["allMarketData"], queryFn: fetchAllMarketData, staleTime: MARKET_DATA_STALE_TIME, - gcTime: 30 * 60 * 1000, + gcTime: MARKET_DATA_GC_TIME, + retry: 1, + retryDelay: 5000, }); }; diff --git a/packages/swap-widget-poc/vite.config.ts b/packages/swap-widget-poc/vite.config.ts index 074a898debe..e53536e0120 100644 --- a/packages/swap-widget-poc/vite.config.ts +++ b/packages/swap-widget-poc/vite.config.ts @@ -1,6 +1,8 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; +const isLibBuild = process.env.BUILD_LIB === "true"; + export default defineConfig({ plugins: [react()], define: { @@ -10,20 +12,28 @@ export default defineConfig({ port: 3001, open: true, }, - build: { - lib: { - entry: "src/index.ts", - name: "SwapWidget", - fileName: "index", - }, - rollupOptions: { - external: ["react", "react-dom"], - output: { - globals: { - react: "React", - "react-dom": "ReactDOM", + preview: { + port: Number(process.env.PORT) || 3000, + host: true, + }, + build: isLibBuild + ? { + lib: { + entry: "src/index.ts", + name: "SwapWidget", + fileName: "index", }, + rollupOptions: { + external: ["react", "react-dom"], + output: { + globals: { + react: "React", + "react-dom": "ReactDOM", + }, + }, + }, + } + : { + outDir: "dist", }, - }, - }, }); From 18e3836174bc791d61bdf0ea6f2c1427a6d26692 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:43:49 +0100 Subject: [PATCH 03/41] feat: widget poc --- packages/swap-widget-poc/Dockerfile | 29 ++ packages/swap-widget-poc/README.md | 467 ++++++++++++++++++++++++++ packages/swap-widget-poc/railway.json | 5 +- 3 files changed, 498 insertions(+), 3 deletions(-) create mode 100644 packages/swap-widget-poc/Dockerfile create mode 100644 packages/swap-widget-poc/README.md diff --git a/packages/swap-widget-poc/Dockerfile b/packages/swap-widget-poc/Dockerfile new file mode 100644 index 00000000000..5c61f491161 --- /dev/null +++ b/packages/swap-widget-poc/Dockerfile @@ -0,0 +1,29 @@ +FROM node:20-slim AS builder + +WORKDIR /app + +RUN corepack enable && corepack prepare yarn@4.4.0 --activate + +COPY package.json yarn.lock .yarnrc.yml ./ +COPY .yarn ./.yarn +COPY packages/swap-widget-poc ./packages/swap-widget-poc +COPY packages/caip ./packages/caip +COPY packages/types ./packages/types + +RUN yarn workspaces focus @shapeshiftoss/swap-widget-poc --production=false + +WORKDIR /app/packages/swap-widget-poc +RUN yarn build + +FROM node:20-slim AS runner + +WORKDIR /app + +RUN corepack enable && corepack prepare yarn@4.4.0 --activate && \ + npm install -g serve + +COPY --from=builder /app/packages/swap-widget-poc/dist ./dist + +EXPOSE 3000 + +CMD ["serve", "-s", "dist", "-l", "3000"] diff --git a/packages/swap-widget-poc/README.md b/packages/swap-widget-poc/README.md new file mode 100644 index 00000000000..210cbe8a4d4 --- /dev/null +++ b/packages/swap-widget-poc/README.md @@ -0,0 +1,467 @@ +# @shapeshiftoss/swap-widget-poc + +An embeddable React widget that enables multi-chain token swaps using ShapeShift's aggregation API. Integrate swaps into your application with minimal configuration. + +## Table of Contents + +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Props Reference](#props-reference) +- [Theming](#theming) +- [Examples](#examples) +- [Exported Types](#exported-types) +- [Exported Utilities](#exported-utilities) +- [Exported Hooks](#exported-hooks) +- [Supported Chains](#supported-chains) +- [Notes and Limitations](#notes-and-limitations) + +## Installation + +```bash +yarn add @shapeshiftoss/swap-widget-poc +# or +npm install @shapeshiftoss/swap-widget-poc +``` + +### Peer Dependencies + +This package requires React 18 or later: + +```json +{ + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } +} +``` + +## Quick Start + +```tsx +import { SwapWidget } from "@shapeshiftoss/swap-widget-poc"; + +function App() { + return ( + console.log("Success:", txHash)} + onSwapError={(error) => console.error("Error:", error)} + /> + ); +} +``` + +## Props Reference + +### 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. | + +## Theming + +The widget supports both simple theme modes and full customization. + +### Simple Theme Mode + +```tsx + +// or + +``` + +### Custom Theme Configuration + +```tsx +import { SwapWidget } from "@shapeshiftoss/swap-widget-poc"; +import type { ThemeConfig } from "@shapeshiftoss/swap-widget-poc"; + +const customTheme: ThemeConfig = { + mode: "dark", + accentColor: "#3861fb", // Primary accent color (buttons, focus states) + backgroundColor: "#0a0a14", // Widget background + cardColor: "#12121c", // Card/panel background + textColor: "#ffffff", // Primary text color + borderRadius: "12px", // Border radius for elements + fontFamily: "Inter, sans-serif", +}; + +function App() { + return ; +} +``` + +### ThemeConfig Properties + +| Property | Type | Description | +| ----------------- | ------------------- | -------------------------------------------------- | +| `mode` | `"light" \| "dark"` | Base theme mode. Required. | +| `accentColor` | `string` | Primary accent color for buttons and focus states. | +| `backgroundColor` | `string` | Widget background color. | +| `cardColor` | `string` | Card and panel background color. | +| `textColor` | `string` | Primary text color. | +| `borderRadius` | `string` | Border radius for UI elements. | +| `fontFamily` | `string` | Font family for the widget. | + +## Examples + +### Basic Usage + +```tsx +import { SwapWidget } from "@shapeshiftoss/swap-widget-poc"; + +function App() { + return ; +} +``` + +### With Wallet Connection (wagmi/viem) + +```tsx +import { SwapWidget } from "@shapeshiftoss/swap-widget-poc"; +import { useWalletClient } from "wagmi"; +import { useConnectModal } from "@rainbow-me/rainbowkit"; + +function App() { + const { data: walletClient } = useWalletClient(); + const { openConnectModal } = useConnectModal(); + + return ( + { + console.log("Swap successful:", txHash); + }} + onSwapError={(error) => { + console.error("Swap failed:", error); + }} + theme={{ + mode: "dark", + accentColor: "#3861fb", + backgroundColor: "#0a0a14", + cardColor: "#12121c", + }} + /> + ); +} +``` + +### With Custom Default Assets + +```tsx +import { SwapWidget } from "@shapeshiftoss/swap-widget-poc"; +import type { Asset } from "@shapeshiftoss/swap-widget-poc"; + +const defaultSellAsset: Asset = { + assetId: "eip155:137/slip44:966", + chainId: "eip155:137", + symbol: "MATIC", + name: "Polygon", + precision: 18, + icon: "https://example.com/matic.png", +}; + +const defaultBuyAsset: Asset = { + assetId: "eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + chainId: "eip155:137", + symbol: "USDC", + name: "USD Coin", + precision: 6, + icon: "https://example.com/usdc.png", +}; + +function App() { + return ( + + ); +} +``` + +### Restricting Available Chains and Assets + +```tsx +import { SwapWidget, EVM_CHAIN_IDS } from "@shapeshiftoss/swap-widget-poc"; + +function App() { + return ( + + ); +} +``` + +## Exported Types + +```typescript +import type { + Asset, + AssetId, + ChainId, + Chain, + TradeRate, + TradeQuote, + SwapperName, + SwapWidgetProps, + ThemeMode, + ThemeConfig, +} from "@shapeshiftoss/swap-widget-poc"; +``` + +### Asset + +```typescript +type Asset = { + assetId: AssetId; // CAIP-19 format: "eip155:1/slip44:60" + chainId: ChainId; // CAIP-2 format: "eip155:1" + symbol: string; // e.g., "ETH" + name: string; // e.g., "Ethereum" + precision: number; // e.g., 18 + icon?: string; // URL to asset icon + color?: string; // Brand color + networkName?: string; // Display name for the network + networkIcon?: string; // URL to network icon + explorer?: string; // Block explorer URL + explorerTxLink?: string; // Transaction explorer link template + explorerAddressLink?: string; // Address explorer link template + relatedAssetKey?: AssetId | null; // Related asset for bridged tokens +}; +``` + +### SwapperName + +```typescript +type SwapperName = + | "THORChain" + | "MAYAChain" + | "CoW Swap" + | "0x" + | "Portals" + | "Chainflip" + | "Relay" + | "Bebop" + | "Jupiter" + | "1inch" + | "ButterSwap" + | "ArbitrumBridge"; +``` + +### TradeRate + +```typescript +type TradeRate = { + swapperName: SwapperName; + rate: string; + buyAmountCryptoBaseUnit: string; + sellAmountCryptoBaseUnit: string; + steps: number; + estimatedExecutionTimeMs?: number; + affiliateBps: string; + networkFeeCryptoBaseUnit?: string; + error?: { + code: string; + message: string; + }; + id?: string; +}; +``` + +## Exported Utilities + +```typescript +import { + isEvmChainId, + getEvmChainIdNumber, + getChainType, + formatAmount, + parseAmount, + truncateAddress, + EVM_CHAIN_IDS, + UTXO_CHAIN_IDS, + COSMOS_CHAIN_IDS, + OTHER_CHAIN_IDS, + CHAIN_METADATA, + getChainMeta, + getChainName, + getChainIcon, + getChainColor, +} from "@shapeshiftoss/swap-widget-poc"; +``` + +### Chain Type Utilities + +| Function | Signature | Description | +| --------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------- | +| `isEvmChainId` | `(chainId: string) => boolean` | Check if a chain ID is an EVM chain. | +| `getEvmChainIdNumber` | `(chainId: string) => number` | Extract the numeric chain ID from a CAIP-2 chain ID. | +| `getChainType` | `(chainId: string) => "evm" \| "utxo" \| "cosmos" \| "solana" \| "other"` | Get the chain type from a chain ID. | + +### Amount Formatting + +| Function | Signature | Description | +| ----------------- | -------------------------------------------------------------------- | -------------------------------------------------------- | +| `formatAmount` | `(amount: string, decimals: number, maxDecimals?: number) => string` | Format a base unit amount for display. | +| `parseAmount` | `(amount: string, decimals: number) => string` | Parse a human-readable amount to base units. | +| `truncateAddress` | `(address: string, chars?: number) => string` | Truncate an address for display (e.g., `0x1234...5678`). | + +### Chain Metadata + +| Function | Signature | Description | +| --------------- | ---------------------------------------------- | --------------------------------- | +| `getChainMeta` | `(chainId: ChainId) => ChainMeta \| undefined` | Get full metadata for a chain. | +| `getChainName` | `(chainId: ChainId) => string` | Get the display name for a chain. | +| `getChainIcon` | `(chainId: ChainId) => string \| undefined` | Get the icon URL for a chain. | +| `getChainColor` | `(chainId: ChainId) => string` | Get the brand color for a chain. | + +### Chain ID Constants + +```typescript +const EVM_CHAIN_IDS = { + ethereum: "eip155:1", + arbitrum: "eip155:42161", + arbitrumNova: "eip155:42170", + optimism: "eip155:10", + polygon: "eip155:137", + base: "eip155:8453", + avalanche: "eip155:43114", + bsc: "eip155:56", + gnosis: "eip155:100", +}; + +const UTXO_CHAIN_IDS = { + bitcoin: "bip122:000000000019d6689c085ae165831e93", + bitcoinCash: "bip122:000000000000000000651ef99cb9fcbe", + dogecoin: "bip122:00000000001a91e3dace36e2be3bf030", + litecoin: "bip122:12a765e31ffd4059bada1e25190f6e98", +}; + +const COSMOS_CHAIN_IDS = { + cosmos: "cosmos:cosmoshub-4", + thorchain: "cosmos:thorchain-1", + mayachain: "cosmos:mayachain-mainnet-v1", +}; + +const OTHER_CHAIN_IDS = { + solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", +}; +``` + +## Exported Hooks + +```typescript +import { + useAssets, + useAssetById, + useChains, + useAssetsByChainId, + useAssetSearch, +} from "@shapeshiftoss/swap-widget-poc"; +``` + +| Hook | Return Type | Description | +| --------------------------------- | ------------------------------------------ | -------------------------------------------------------------- | +| `useAssets()` | `{ data: Asset[], isLoading, error, ... }` | Fetch all available assets. | +| `useAssetById(assetId)` | `{ data: Asset \| undefined, ... }` | Fetch a specific asset by ID. | +| `useChains()` | `{ data: ChainInfo[], ... }` | Fetch all available chains with their native assets. | +| `useAssetsByChainId(chainId)` | `{ data: Asset[], ... }` | Fetch all assets for a specific chain. | +| `useAssetSearch(query, chainId?)` | `{ data: Asset[], ... }` | Search assets by symbol or name, optionally filtered by chain. | + +## Supported Chains + +| Chain | Chain ID | Type | +| ----------------- | ----------------------------------------- | ------ | +| Ethereum | `eip155:1` | EVM | +| Arbitrum One | `eip155:42161` | EVM | +| Arbitrum Nova | `eip155:42170` | EVM | +| Optimism | `eip155:10` | EVM | +| Polygon | `eip155:137` | EVM | +| Base | `eip155:8453` | EVM | +| Avalanche C-Chain | `eip155:43114` | EVM | +| BNB Smart Chain | `eip155:56` | EVM | +| Gnosis | `eip155:100` | EVM | +| Bitcoin | `bip122:000000000019d6689c085ae165831e93` | UTXO | +| Bitcoin Cash | `bip122:000000000000000000651ef99cb9fcbe` | UTXO | +| Dogecoin | `bip122:00000000001a91e3dace36e2be3bf030` | UTXO | +| Litecoin | `bip122:12a765e31ffd4059bada1e25190f6e98` | UTXO | +| Cosmos Hub | `cosmos:cosmoshub-4` | Cosmos | +| THORChain | `cosmos:thorchain-1` | Cosmos | +| MAYAChain | `cosmos:mayachain-mainnet-v1` | Cosmos | +| Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | Solana | + +## Notes and Limitations + +### EVM vs Non-EVM Swaps + +- **EVM swaps** (e.g., ETH to USDC, MATIC to WETH) can be executed directly within the widget when a wallet is connected via the `walletClient` prop. +- **Non-EVM swaps** (e.g., BTC to ETH, SOL to USDC) redirect the user to [app.shapeshift.com](https://app.shapeshift.com) to complete the transaction. This is because non-EVM chains require different wallet types. + +### API Key + +An API key is required for fetching swap rates in production. Contact ShapeShift for API access. + +### Internal QueryClient + +The widget manages its own React Query `QueryClient` internally. You do not need to wrap it in a `QueryClientProvider`. + +### Swap Aggregation + +The widget fetches quotes from multiple DEXs and aggregators including: + +- THORChain +- MAYAChain +- CoW Swap +- 0x +- 1inch +- Portals +- Chainflip +- Jupiter (Solana) +- Bebop +- Relay +- ButterSwap +- Arbitrum Bridge + +### Wallet Balance Display + +When a wallet is connected (`walletClient` prop), the widget displays the user's balance for the selected sell and buy assets. This only works for EVM chains where the connected wallet has assets. + +### USD Price Display + +The widget automatically fetches and displays USD prices for selected assets. + +### Mobile Responsive + +The widget is designed to be responsive and works well on mobile devices. diff --git a/packages/swap-widget-poc/railway.json b/packages/swap-widget-poc/railway.json index af5558d69d8..9f2b224a318 100644 --- a/packages/swap-widget-poc/railway.json +++ b/packages/swap-widget-poc/railway.json @@ -1,11 +1,10 @@ { "$schema": "https://railway.app/railway.schema.json", "build": { - "builder": "NIXPACKS", - "buildCommand": "corepack enable && yarn install && yarn workspace @shapeshiftoss/swap-widget-poc build" + "builder": "DOCKERFILE", + "dockerfilePath": "packages/swap-widget-poc/Dockerfile" }, "deploy": { - "startCommand": "yarn workspace @shapeshiftoss/swap-widget-poc preview --host --port ${PORT:-3000}", "restartPolicyType": "ON_FAILURE", "restartPolicyMaxRetries": 10 } From 814852f9c1700749a01bcd2d9b8767ad02165372 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:07:21 +0100 Subject: [PATCH 04/41] feat: widget deploy --- packages/swap-widget-poc/Dockerfile | 26 ++++++++++--------- .../railway.json => railway.json | 0 2 files changed, 14 insertions(+), 12 deletions(-) rename packages/swap-widget-poc/railway.json => railway.json (100%) diff --git a/packages/swap-widget-poc/Dockerfile b/packages/swap-widget-poc/Dockerfile index 5c61f491161..d9c5abf66ba 100644 --- a/packages/swap-widget-poc/Dockerfile +++ b/packages/swap-widget-poc/Dockerfile @@ -1,28 +1,30 @@ -FROM node:20-slim AS builder +FROM node:20-slim + +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* +RUN npm install -g serve WORKDIR /app -RUN corepack enable && corepack prepare yarn@4.4.0 --activate +RUN git init COPY package.json yarn.lock .yarnrc.yml ./ COPY .yarn ./.yarn + COPY packages/swap-widget-poc ./packages/swap-widget-poc -COPY packages/caip ./packages/caip +COPY packages/caip ./packages/caip COPY packages/types ./packages/types -RUN yarn workspaces focus @shapeshiftoss/swap-widget-poc --production=false - -WORKDIR /app/packages/swap-widget-poc -RUN yarn build +RUN corepack enable -FROM node:20-slim AS runner +ENV YARN_ENABLE_SCRIPTS=false -WORKDIR /app +RUN yarn workspaces focus @shapeshiftoss/swap-widget-poc @shapeshiftoss/caip @shapeshiftoss/types --production=false -RUN corepack enable && corepack prepare yarn@4.4.0 --activate && \ - npm install -g serve +RUN yarn workspace @shapeshiftoss/types build +RUN yarn workspace @shapeshiftoss/caip build +RUN yarn workspace @shapeshiftoss/swap-widget-poc build -COPY --from=builder /app/packages/swap-widget-poc/dist ./dist +WORKDIR /app/packages/swap-widget-poc EXPOSE 3000 diff --git a/packages/swap-widget-poc/railway.json b/railway.json similarity index 100% rename from packages/swap-widget-poc/railway.json rename to railway.json From 640f0b739cfddc28274891f88d726fe4242a3fd1 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:21:17 +0100 Subject: [PATCH 05/41] feat: more fixes --- packages/swap-widget-poc/Dockerfile | 2 +- packages/swap-widget-poc/src/api/client.ts | 45 +++++++++++-------- .../src/components/TokenSelectModal.css | 2 +- .../src/components/TokenSelectModal.tsx | 32 +++++++------ 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/packages/swap-widget-poc/Dockerfile b/packages/swap-widget-poc/Dockerfile index d9c5abf66ba..5436ef19d67 100644 --- a/packages/swap-widget-poc/Dockerfile +++ b/packages/swap-widget-poc/Dockerfile @@ -18,7 +18,7 @@ RUN corepack enable ENV YARN_ENABLE_SCRIPTS=false -RUN yarn workspaces focus @shapeshiftoss/swap-widget-poc @shapeshiftoss/caip @shapeshiftoss/types --production=false +RUN yarn workspaces focus @shapeshiftoss/swap-widget-poc --production=false RUN yarn workspace @shapeshiftoss/types build RUN yarn workspace @shapeshiftoss/caip build diff --git a/packages/swap-widget-poc/src/api/client.ts b/packages/swap-widget-poc/src/api/client.ts index b75a4a0e8dc..78c201081b1 100644 --- a/packages/swap-widget-poc/src/api/client.ts +++ b/packages/swap-widget-poc/src/api/client.ts @@ -18,14 +18,9 @@ export const createApiClient = (config: ApiClientConfig = {}) => { const fetchWithConfig = async ( endpoint: string, params?: Record, + method: "GET" | "POST" = "GET", ): Promise => { const url = new URL(`${baseUrl}${endpoint}`); - if (params) { - Object.entries(params).forEach(([key, value]) => { - url.searchParams.append(key, value); - }); - } - const headers: Record = { "Content-Type": "application/json", }; @@ -33,7 +28,17 @@ export const createApiClient = (config: ApiClientConfig = {}) => { headers["x-api-key"] = config.apiKey; } - const response = await fetch(url.toString(), { headers }); + const fetchOptions: RequestInit = { headers, method }; + + 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); + } + + const response = await fetch(url.toString(), fetchOptions); if (!response.ok) { throw new Error(`API error: ${response.status} ${response.statusText}`); } @@ -62,17 +67,21 @@ export const createApiClient = (config: ApiClientConfig = {}) => { swapperName: string; slippageTolerancePercentageDecimal?: string; }) => - fetchWithConfig("/v1/swap/quote", { - sellAssetId: params.sellAssetId, - buyAssetId: params.buyAssetId, - sellAmountCryptoBaseUnit: params.sellAmountCryptoBaseUnit, - receiveAddress: params.receiveAddress, - swapperName: params.swapperName, - ...(params.slippageTolerancePercentageDecimal && { - slippageTolerancePercentageDecimal: - params.slippageTolerancePercentageDecimal, - }), - }), + fetchWithConfig( + "/v1/swap/quote", + { + sellAssetId: params.sellAssetId, + buyAssetId: params.buyAssetId, + sellAmountCryptoBaseUnit: params.sellAmountCryptoBaseUnit, + receiveAddress: params.receiveAddress, + swapperName: params.swapperName, + ...(params.slippageTolerancePercentageDecimal && { + slippageTolerancePercentageDecimal: + params.slippageTolerancePercentageDecimal, + }), + }, + "POST", + ), }; }; diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.css b/packages/swap-widget-poc/src/components/TokenSelectModal.css index 97d6a6decac..169fc324826 100644 --- a/packages/swap-widget-poc/src/components/TokenSelectModal.css +++ b/packages/swap-widget-poc/src/components/TokenSelectModal.css @@ -269,7 +269,7 @@ flex-shrink: 0; } -.ssw-token-price { +.ssw-token-fiat-value { font-size: 14px; font-weight: 500; color: var(--ssw-text-primary); diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx index a55aa85e10a..f075d78cd32 100644 --- a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx +++ b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx @@ -320,24 +320,28 @@ export const TokenSelectModal = ({
- {marketData?.[asset.assetId]?.price && ( - - $ - {Number( - marketData[asset.assetId].price, - ).toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} - - )} {walletAddress && (loadingAssetIds.has(asset.assetId) ? ( ) : balance && balance.balance !== "0" ? ( - - {balance.balanceFormatted} - + <> + {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} + + ) : null)}
From 82783458f6c65b86e2d8863c98929a653c4699ed Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:45:44 +0100 Subject: [PATCH 06/41] fix: more fixes --- packages/swap-widget-poc/Dockerfile | 2 +- packages/swap-widget-poc/src/api/client.ts | 2 + .../src/components/SwapWidget.css | 101 +++++++++ .../src/components/SwapWidget.tsx | 200 ++++++++++++++++-- packages/swap-widget-poc/src/demo/App.tsx | 2 - .../swap-widget-poc/src/hooks/useBalances.ts | 25 ++- .../swap-widget-poc/src/hooks/useSwapQuote.ts | 6 + packages/swap-widget-poc/src/types/index.ts | 41 +++- 8 files changed, 350 insertions(+), 29 deletions(-) diff --git a/packages/swap-widget-poc/Dockerfile b/packages/swap-widget-poc/Dockerfile index 5436ef19d67..aacc817981f 100644 --- a/packages/swap-widget-poc/Dockerfile +++ b/packages/swap-widget-poc/Dockerfile @@ -18,7 +18,7 @@ RUN corepack enable ENV YARN_ENABLE_SCRIPTS=false -RUN yarn workspaces focus @shapeshiftoss/swap-widget-poc --production=false +RUN yarn install RUN yarn workspace @shapeshiftoss/types build RUN yarn workspace @shapeshiftoss/caip build diff --git a/packages/swap-widget-poc/src/api/client.ts b/packages/swap-widget-poc/src/api/client.ts index 78c201081b1..a7a9981b895 100644 --- a/packages/swap-widget-poc/src/api/client.ts +++ b/packages/swap-widget-poc/src/api/client.ts @@ -63,6 +63,7 @@ export const createApiClient = (config: ApiClientConfig = {}) => { sellAssetId: AssetId; buyAssetId: AssetId; sellAmountCryptoBaseUnit: string; + sendAddress: string; receiveAddress: string; swapperName: string; slippageTolerancePercentageDecimal?: string; @@ -73,6 +74,7 @@ export const createApiClient = (config: ApiClientConfig = {}) => { sellAssetId: params.sellAssetId, buyAssetId: params.buyAssetId, sellAmountCryptoBaseUnit: params.sellAmountCryptoBaseUnit, + sendAddress: params.sendAddress, receiveAddress: params.receiveAddress, swapperName: params.swapperName, ...(params.slippageTolerancePercentageDecimal && { diff --git a/packages/swap-widget-poc/src/components/SwapWidget.css b/packages/swap-widget-poc/src/components/SwapWidget.css index ad63d5cf01e..01a8334538b 100644 --- a/packages/swap-widget-poc/src/components/SwapWidget.css +++ b/packages/swap-widget-poc/src/components/SwapWidget.css @@ -334,3 +334,104 @@ .ssw-powered-by a:hover { opacity: 0.8; } + +/* Transaction Status */ +.ssw-tx-status { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + margin: 0 16px 16px; + border-radius: 12px; + background: var(--ssw-bg-tertiary); + border: 1px solid var(--ssw-border); +} + +.ssw-tx-status-pending { + border-color: var(--ssw-accent); + background: var(--ssw-accent-light); +} + +.ssw-tx-status-success { + border-color: var(--ssw-success); + background: rgba(0, 211, 149, 0.1); +} + +.ssw-tx-status-error { + border-color: var(--ssw-error); + background: rgba(244, 67, 54, 0.1); +} + +.ssw-tx-status-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; +} + +.ssw-tx-status-pending .ssw-tx-status-icon { + color: var(--ssw-accent); +} + +.ssw-tx-status-success .ssw-tx-status-icon { + color: var(--ssw-success); +} + +.ssw-tx-status-error .ssw-tx-status-icon { + color: var(--ssw-error); +} + +.ssw-tx-status-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.ssw-tx-status-message { + font-size: 14px; + font-weight: 500; + color: var(--ssw-text-primary); + word-break: break-word; +} + +.ssw-tx-status-link { + font-size: 13px; + color: var(--ssw-accent); + text-decoration: none; +} + +.ssw-tx-status-link:hover { + text-decoration: underline; +} + +.ssw-tx-status-close { + flex-shrink: 0; + background: none; + border: none; + padding: 4px; + cursor: pointer; + color: var(--ssw-text-muted); + transition: color 0.15s ease; +} + +.ssw-tx-status-close:hover { + color: var(--ssw-text-primary); +} + +/* Spinner animation */ +@keyframes ssw-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.ssw-spinner { + animation: ssw-spin 1s linear infinite; +} diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget-poc/src/components/SwapWidget.tsx index cc246df317d..1c5452a758b 100644 --- a/packages/swap-widget-poc/src/components/SwapWidget.tsx +++ b/packages/swap-widget-poc/src/components/SwapWidget.tsx @@ -1,5 +1,6 @@ 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"; @@ -71,6 +72,11 @@ const SwapWidgetInner = ({ 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); const [tokenModalType, setTokenModalType] = useState<"sell" | "buy" | null>( null, @@ -106,6 +112,17 @@ const SwapWidgetInner = ({ const isBuyAssetEvm = getChainType(buyAsset.chainId) === "evm"; const canExecuteDirectly = isSellAssetEvm && isBuyAssetEvm; + const { + data: sellAssetBalance, + isLoading: isSellBalanceLoading, + refetch: refetchSellBalance, + } = useAssetBalance(walletAddress, sellAsset.assetId, sellAsset.precision); + const { + data: buyAssetBalance, + isLoading: isBuyBalanceLoading, + refetch: refetchBuyBalance, + } = useAssetBalance(walletAddress, buyAsset.assetId, buyAsset.precision); + const handleSwapTokens = useCallback(() => { const tempSell = sellAsset; setSellAsset(buyAsset); @@ -152,38 +169,117 @@ const SwapWidgetInner = ({ setIsExecuting(true); try { + const requiredChainId = getEvmChainIdNumber(sellAsset.chainId); + const client = walletClient as WalletClient; + + const currentChainId = await client.getChainId(); + if (currentChainId !== requiredChainId) { + await client.switchChain({ id: requiredChainId }); + } + const slippageDecimal = (parseFloat(slippage) / 100).toString(); const quoteResponse = await apiClient.getQuote({ sellAssetId: sellAsset.assetId, buyAssetId: buyAsset.assetId, sellAmountCryptoBaseUnit: sellAmountBaseUnit!, + sendAddress: walletAddress, receiveAddress: walletAddress, swapperName: rateToUse.swapperName, slippageTolerancePercentageDecimal: slippageDecimal, }); - if (!quoteResponse.transactionData) { - throw new Error("No transaction data returned"); + const chain = { + id: requiredChainId, + name: "Chain", + nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: [] } }, + }; + + if (quoteResponse.approval?.isRequired) { + const sellAssetAddress = sellAsset.assetId.split("/")[1]?.split(":")[1]; + if (sellAssetAddress) { + const approvalData = encodeFunctionData({ + abi: [ + { + name: "approve", + type: "function", + inputs: [ + { name: "spender", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ name: "", type: "bool" }], + }, + ], + functionName: "approve", + args: [ + quoteResponse.approval.spender as `0x${string}`, + BigInt(sellAmountBaseUnit!), + ], + }); + + await client.sendTransaction({ + to: sellAssetAddress as `0x${string}`, + data: approvalData, + value: BigInt(0), + chain, + account: walletAddress as `0x${string}`, + }); + } } - const { to, data, value, gasLimit } = quoteResponse.transactionData; + const outerStep = quoteResponse.steps?.[0]; + const innerStep = quoteResponse.quote?.steps?.[0]; + + const transactionData = + quoteResponse.transactionData ?? + outerStep?.transactionData ?? + outerStep?.relayTransactionMetadata ?? + outerStep?.butterSwapTransactionMetadata ?? + innerStep?.transactionData ?? + innerStep?.relayTransactionMetadata ?? + innerStep?.butterSwapTransactionMetadata; + + if (!transactionData) { + throw new Error( + `No transaction data returned. Response keys: ${Object.keys( + quoteResponse, + ).join(", ")}`, + ); + } - const txHash = await (walletClient as WalletClient).sendTransaction({ + 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...", + }); + + const txHash = await client.sendTransaction({ to: to as `0x${string}`, data: data as `0x${string}`, value: BigInt(value), gas: gasLimit ? BigInt(gasLimit) : undefined, - chain: { - id: getEvmChainIdNumber(sellAsset.chainId), - name: "Chain", - nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, - rpcUrls: { default: { http: [] } }, - }, + chain, account: walletAddress as `0x${string}`, }); + setTxStatus({ status: "success", txHash, message: "Swap successful!" }); onSwapSuccess?.(txHash); + + setSellAmount(""); + setSelectedRate(null); + + setTimeout(() => { + refetchSellBalance?.(); + refetchBuyBalance?.(); + }, 3000); } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Transaction failed"; + setTxStatus({ status: "error", message: errorMessage }); onSwapError?.(error as Error); } finally { setIsExecuting(false); @@ -202,6 +298,8 @@ const SwapWidgetInner = ({ apiClient, onSwapSuccess, onSwapError, + refetchSellBalance, + refetchBuyBalance, ]); const handleButtonClick = useCallback(() => { @@ -245,11 +343,6 @@ const SwapWidgetInner = ({ const displayRate = selectedRate ?? rates?.[0]; const buyAmount = displayRate?.buyAmountCryptoBaseUnit; - const { data: sellAssetBalance, isLoading: isSellBalanceLoading } = - useAssetBalance(walletAddress, sellAsset.assetId, sellAsset.precision); - const { data: buyAssetBalance, isLoading: isBuyBalanceLoading } = - useAssetBalance(walletAddress, buyAsset.assetId, buyAsset.precision); - const assetIdsForPrices = useMemo( () => [sellAsset.assetId, buyAsset.assetId], [sellAsset.assetId, buyAsset.assetId], @@ -515,6 +608,83 @@ const SwapWidgetInner = ({ {buttonText} + {txStatus && ( +
+
+ {txStatus.status === "pending" && ( + + + + + )} + {txStatus.status === "success" && ( + + + + )} + {txStatus.status === "error" && ( + + + + + )} +
+
+ {txStatus.message} + {txStatus.txHash && ( + + View transaction + + )} +
+ +
+ )} + {showPoweredBy && (
Powered by{" "} diff --git a/packages/swap-widget-poc/src/demo/App.tsx b/packages/swap-widget-poc/src/demo/App.tsx index 7b2fa30cc16..14b6132a92a 100644 --- a/packages/swap-widget-poc/src/demo/App.tsx +++ b/packages/swap-widget-poc/src/demo/App.tsx @@ -133,12 +133,10 @@ const DemoContent = () => { const handleSwapSuccess = (txHash: string) => { console.log("Swap successful:", txHash); - alert(`Swap successful! TxHash: ${txHash}`); }; const handleSwapError = (error: Error) => { console.error("Swap failed:", error); - alert(`Swap failed: ${error.message}`); }; const demoStyle = useMemo( diff --git a/packages/swap-widget-poc/src/hooks/useBalances.ts b/packages/swap-widget-poc/src/hooks/useBalances.ts index 77c9de48cb2..d3ea6e219dd 100644 --- a/packages/swap-widget-poc/src/hooks/useBalances.ts +++ b/packages/swap-widget-poc/src/hooks/useBalances.ts @@ -47,7 +47,11 @@ export const useAssetBalance = ( const isNative = parsed && !parsed.tokenAddress; const isErc20 = parsed && !!parsed.tokenAddress; - const { data: nativeBalance } = useBalance({ + const { + data: nativeBalance, + isLoading: isNativeLoading, + refetch: refetchNative, + } = useBalance({ address: address as `0x${string}` | undefined, chainId: isNative ? parsed.chainId : undefined, query: { @@ -55,7 +59,11 @@ export const useAssetBalance = ( }, }); - const { data: erc20Balance } = useBalance({ + const { + data: erc20Balance, + isLoading: isErc20Loading, + refetch: refetchErc20, + } = useBalance({ address: address as `0x${string}` | undefined, chainId: isErc20 ? parsed.chainId : undefined, token: isErc20 ? parsed.tokenAddress : undefined, @@ -65,10 +73,16 @@ export const useAssetBalance = ( }); 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: false }; + return { data: undefined, isLoading, refetch }; } return { @@ -77,9 +91,10 @@ export const useAssetBalance = ( balance: balance.value.toString(), balanceFormatted: formatAmount(balance.value.toString(), precision), }, - isLoading: false, + isLoading, + refetch, }; - }, [balance, assetId, precision]); + }, [balance, assetId, precision, isLoading, refetch]); }; export const useEvmBalances = ( diff --git a/packages/swap-widget-poc/src/hooks/useSwapQuote.ts b/packages/swap-widget-poc/src/hooks/useSwapQuote.ts index fb5b0af1e32..dd82ab2c2d9 100644 --- a/packages/swap-widget-poc/src/hooks/useSwapQuote.ts +++ b/packages/swap-widget-poc/src/hooks/useSwapQuote.ts @@ -6,6 +6,7 @@ export type UseSwapQuoteParams = { sellAssetId: AssetId | undefined; buyAssetId: AssetId | undefined; sellAmountCryptoBaseUnit: string | undefined; + sendAddress: string | undefined; receiveAddress: string | undefined; swapperName: SwapperName | undefined; slippageTolerancePercentageDecimal?: string; @@ -20,6 +21,7 @@ export const useSwapQuote = ( sellAssetId, buyAssetId, sellAmountCryptoBaseUnit, + sendAddress, receiveAddress, swapperName, slippageTolerancePercentageDecimal, @@ -32,6 +34,7 @@ export const useSwapQuote = ( sellAssetId, buyAssetId, sellAmountCryptoBaseUnit, + sendAddress, receiveAddress, swapperName, ], @@ -40,6 +43,7 @@ export const useSwapQuote = ( !sellAssetId || !buyAssetId || !sellAmountCryptoBaseUnit || + !sendAddress || !receiveAddress || !swapperName ) { @@ -49,6 +53,7 @@ export const useSwapQuote = ( sellAssetId, buyAssetId, sellAmountCryptoBaseUnit, + sendAddress, receiveAddress, swapperName, slippageTolerancePercentageDecimal, @@ -59,6 +64,7 @@ export const useSwapQuote = ( !!sellAssetId && !!buyAssetId && !!sellAmountCryptoBaseUnit && + !!sendAddress && !!receiveAddress && !!swapperName, staleTime: 30_000, diff --git a/packages/swap-widget-poc/src/types/index.ts b/packages/swap-widget-poc/src/types/index.ts index 2430872c227..14a1fe24211 100644 --- a/packages/swap-widget-poc/src/types/index.ts +++ b/packages/swap-widget-poc/src/types/index.ts @@ -116,16 +116,45 @@ export type RatesResponse = { rates: TradeRate[]; }; +type TransactionData = { + to: string; + data: string; + value?: string; + gasLimit?: string; + chainId?: number; + relayId?: string; +}; + +type QuoteStep = { + transactionData?: TransactionData; + relayTransactionMetadata?: TransactionData; + butterSwapTransactionMetadata?: TransactionData; +}; + export type QuoteResponse = { - quote: TradeQuote; - transactionData?: { - to: string; - data: string; - value: string; - gasLimit?: string; + quote?: { + steps?: QuoteStep[]; + }; + transactionData?: TransactionData; + steps?: QuoteStep[]; + approval?: { + isRequired: boolean; + spender: string; }; }; +export const ERC20_APPROVE_ABI = [ + { + name: "approve", + type: "function", + inputs: [ + { name: "spender", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ name: "", type: "bool" }], + }, +] as const; + export type AssetsResponse = { byId: Record; ids: AssetId[]; From 96cbe2832441a8288de6901fae265e232a05f74b Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:57:26 +0100 Subject: [PATCH 07/41] fix: update Dockerfile to build caip locally with npm --- packages/swap-widget-poc/Dockerfile | 38 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/swap-widget-poc/Dockerfile b/packages/swap-widget-poc/Dockerfile index aacc817981f..5135669371b 100644 --- a/packages/swap-widget-poc/Dockerfile +++ b/packages/swap-widget-poc/Dockerfile @@ -1,30 +1,34 @@ -FROM node:20-slim - -RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* -RUN npm install -g serve +FROM node:20-slim AS builder WORKDIR /app -RUN git init - -COPY package.json yarn.lock .yarnrc.yml ./ -COPY .yarn ./.yarn +COPY packages/caip/package.json ./packages/caip/ +COPY packages/caip/src ./packages/caip/src +COPY packages/caip/tsconfig.json ./packages/caip/ +COPY packages/caip/tsconfig.cjs.json ./packages/caip/ +COPY packages/caip/tsconfig.esm.json ./packages/caip/ COPY packages/swap-widget-poc ./packages/swap-widget-poc -COPY packages/caip ./packages/caip -COPY packages/types ./packages/types -RUN corepack enable +WORKDIR /app/packages/caip +RUN npm install +RUN npm install -D typescript tsc-esm-fix +RUN npx tsc --build +RUN npx tsc-esm-fix --target=dist/esm --ext=.js +RUN echo '{"type": "commonjs"}' > dist/cjs/package.json + +WORKDIR /app/packages/swap-widget-poc +RUN sed -i 's/"@shapeshiftoss\/caip": "workspace:\*"/"@shapeshiftoss\/caip": "file:..\/caip"/g' package.json +RUN npm install --legacy-peer-deps +RUN npm run build -ENV YARN_ENABLE_SCRIPTS=false +FROM node:20-slim -RUN yarn install +RUN npm install -g serve -RUN yarn workspace @shapeshiftoss/types build -RUN yarn workspace @shapeshiftoss/caip build -RUN yarn workspace @shapeshiftoss/swap-widget-poc build +WORKDIR /app -WORKDIR /app/packages/swap-widget-poc +COPY --from=builder /app/packages/swap-widget-poc/dist ./dist EXPOSE 3000 From 2c488e4fd014b58707d7662f1c0daf4ad576ddbf Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:15:51 +0100 Subject: [PATCH 08/41] fix: remove caip dependency, simplify Dockerfile for standalone build --- packages/swap-widget-poc/Dockerfile | 21 +--- packages/swap-widget-poc/package.json | 1 - .../src/components/SwapWidget.tsx | 12 +- .../src/hooks/useMarketData.ts | 105 +++++++++++++++--- 4 files changed, 105 insertions(+), 34 deletions(-) diff --git a/packages/swap-widget-poc/Dockerfile b/packages/swap-widget-poc/Dockerfile index 5135669371b..421409c1ad5 100644 --- a/packages/swap-widget-poc/Dockerfile +++ b/packages/swap-widget-poc/Dockerfile @@ -2,23 +2,8 @@ FROM node:20-slim AS builder WORKDIR /app -COPY packages/caip/package.json ./packages/caip/ -COPY packages/caip/src ./packages/caip/src -COPY packages/caip/tsconfig.json ./packages/caip/ -COPY packages/caip/tsconfig.cjs.json ./packages/caip/ -COPY packages/caip/tsconfig.esm.json ./packages/caip/ - -COPY packages/swap-widget-poc ./packages/swap-widget-poc - -WORKDIR /app/packages/caip -RUN npm install -RUN npm install -D typescript tsc-esm-fix -RUN npx tsc --build -RUN npx tsc-esm-fix --target=dist/esm --ext=.js -RUN echo '{"type": "commonjs"}' > dist/cjs/package.json - -WORKDIR /app/packages/swap-widget-poc -RUN sed -i 's/"@shapeshiftoss\/caip": "workspace:\*"/"@shapeshiftoss\/caip": "file:..\/caip"/g' package.json +COPY packages/swap-widget-poc ./ + RUN npm install --legacy-peer-deps RUN npm run build @@ -28,7 +13,7 @@ RUN npm install -g serve WORKDIR /app -COPY --from=builder /app/packages/swap-widget-poc/dist ./dist +COPY --from=builder /app/dist ./dist EXPOSE 3000 diff --git a/packages/swap-widget-poc/package.json b/packages/swap-widget-poc/package.json index 92cc4e06ec2..778d880ced2 100644 --- a/packages/swap-widget-poc/package.json +++ b/packages/swap-widget-poc/package.json @@ -13,7 +13,6 @@ }, "dependencies": { "@rainbow-me/rainbowkit": "^2.2.3", - "@shapeshiftoss/caip": "workspace:*", "@tanstack/react-query": "^5.60.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget-poc/src/components/SwapWidget.tsx index 1c5452a758b..a16e5c7c09c 100644 --- a/packages/swap-widget-poc/src/components/SwapWidget.tsx +++ b/packages/swap-widget-poc/src/components/SwapWidget.tsx @@ -347,7 +347,17 @@ const SwapWidgetInner = ({ () => [sellAsset.assetId, buyAsset.assetId], [sellAsset.assetId, buyAsset.assetId], ); - const { data: marketData } = useMarketData(assetIdsForPrices); + const symbolsForPrices = useMemo( + () => ({ + [sellAsset.assetId]: sellAsset.symbol, + [buyAsset.assetId]: buyAsset.symbol, + }), + [sellAsset.assetId, sellAsset.symbol, buyAsset.assetId, buyAsset.symbol], + ); + const { data: marketData } = useMarketData( + assetIdsForPrices, + symbolsForPrices, + ); const sellAssetUsdPrice = marketData?.[sellAsset.assetId]?.price; const buyAssetUsdPrice = marketData?.[buyAsset.assetId]?.price; diff --git a/packages/swap-widget-poc/src/hooks/useMarketData.ts b/packages/swap-widget-poc/src/hooks/useMarketData.ts index 7680525957e..da7743ba079 100644 --- a/packages/swap-widget-poc/src/hooks/useMarketData.ts +++ b/packages/swap-widget-poc/src/hooks/useMarketData.ts @@ -1,5 +1,4 @@ import { useQuery } from "@tanstack/react-query"; -import { adapters } from "@shapeshiftoss/caip"; import type { AssetId } from "../types"; const MARKET_DATA_STALE_TIME = 10 * 60 * 1000; @@ -26,6 +25,50 @@ type CoinGeckoMarketCap = { const COINGECKO_PROXY_URL = "https://api.proxy.shapeshift.com/api/v1/markets"; const COINGECKO_DIRECT_URL = "https://api.coingecko.com/api/v3"; +const SYMBOL_TO_COINGECKO_ID: Record = { + ETH: "ethereum", + BTC: "bitcoin", + USDC: "usd-coin", + USDT: "tether", + DAI: "dai", + WETH: "weth", + WBTC: "wrapped-bitcoin", + MATIC: "matic-network", + POL: "matic-network", + AVAX: "avalanche-2", + BNB: "binancecoin", + ARB: "arbitrum", + OP: "optimism", + SOL: "solana", + ATOM: "cosmos", + RUNE: "thorchain", + FOX: "shapeshift-fox-token", + LINK: "chainlink", + UNI: "uniswap", + AAVE: "aave", + CRV: "curve-dao-token", + LDO: "lido-dao", + MKR: "maker", + SNX: "havven", + COMP: "compound-governance-token", + GRT: "the-graph", + ENS: "ethereum-name-service", + SHIB: "shiba-inu", + PEPE: "pepe", + APE: "apecoin", + DOGE: "dogecoin", + LTC: "litecoin", + BCH: "bitcoin-cash", + XRP: "ripple", + ADA: "cardano", + DOT: "polkadot", + TRX: "tron", + NEAR: "near", + FTM: "fantom", + GNO: "gnosis", + XDAI: "xdai", +}; + const fetchWithRetry = async ( url: string, retries = 2, @@ -86,21 +129,33 @@ const fetchAllMarketData = async (): Promise => { } } + const coingeckoIdToMarketData = new Map(); for (const asset of allData) { - const assetIds = adapters.coingeckoToAssetIds(asset.id); - if (!assetIds?.length) continue; - - const marketData: MarketData = { + coingeckoIdToMarketData.set(asset.id, { 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; + for (const [symbol, coingeckoId] of Object.entries( + SYMBOL_TO_COINGECKO_ID, + )) { + const marketData = coingeckoIdToMarketData.get(coingeckoId); + if (marketData) { + result[`symbol:${symbol.toLowerCase()}`] = marketData; } } + + for (const asset of allData) { + result[`coingecko:${asset.id}`] = { + 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, + }; + } } catch (error) { console.error("Failed to fetch market data:", error); } @@ -119,7 +174,10 @@ export const useAllMarketData = () => { }); }; -export const useMarketData = (assetIds: AssetId[]) => { +export const useMarketData = ( + assetIds: AssetId[], + symbols?: Record, +) => { const { data: allMarketData, ...rest } = useAllMarketData(); const filteredData = (() => { @@ -127,6 +185,14 @@ export const useMarketData = (assetIds: AssetId[]) => { const result: MarketDataById = {}; for (const assetId of assetIds) { + const symbol = symbols?.[assetId]; + if (symbol) { + const symbolKey = `symbol:${symbol.toLowerCase()}`; + if (allMarketData[symbolKey]) { + result[assetId] = allMarketData[symbolKey]; + continue; + } + } if (allMarketData[assetId]) { result[assetId] = allMarketData[assetId]; } @@ -137,13 +203,24 @@ export const useMarketData = (assetIds: AssetId[]) => { return { data: filteredData, ...rest }; }; -export const useAssetPrice = (assetId: AssetId | undefined) => { +export const useAssetPrice = ( + assetId: AssetId | undefined, + symbol?: string, +) => { const { data: allMarketData, ...rest } = useAllMarketData(); - return { - data: assetId ? allMarketData?.[assetId]?.price : undefined, - ...rest, - }; + const price = (() => { + if (!assetId || !allMarketData) return undefined; + if (symbol) { + const symbolKey = `symbol:${symbol.toLowerCase()}`; + if (allMarketData[symbolKey]) { + return allMarketData[symbolKey].price; + } + } + return allMarketData[assetId]?.price; + })(); + + return { data: price, ...rest }; }; export const formatUsdValue = ( From 32744cbbe0ff28fff287ad6cf218359a529cf439 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:25:07 +0100 Subject: [PATCH 09/41] fix: use npm published caip package instead of workspace, remove title gradient --- packages/swap-widget-poc/package.json | 1 + .../src/components/SwapWidget.tsx | 12 +- packages/swap-widget-poc/src/demo/App.css | 5 +- .../src/hooks/useMarketData.ts | 105 +++--------------- 4 files changed, 17 insertions(+), 106 deletions(-) diff --git a/packages/swap-widget-poc/package.json b/packages/swap-widget-poc/package.json index 778d880ced2..374b99a3fb2 100644 --- a/packages/swap-widget-poc/package.json +++ b/packages/swap-widget-poc/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@rainbow-me/rainbowkit": "^2.2.3", + "@shapeshiftoss/caip": "^8.16.5", "@tanstack/react-query": "^5.60.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget-poc/src/components/SwapWidget.tsx index a16e5c7c09c..1c5452a758b 100644 --- a/packages/swap-widget-poc/src/components/SwapWidget.tsx +++ b/packages/swap-widget-poc/src/components/SwapWidget.tsx @@ -347,17 +347,7 @@ const SwapWidgetInner = ({ () => [sellAsset.assetId, buyAsset.assetId], [sellAsset.assetId, buyAsset.assetId], ); - const symbolsForPrices = useMemo( - () => ({ - [sellAsset.assetId]: sellAsset.symbol, - [buyAsset.assetId]: buyAsset.symbol, - }), - [sellAsset.assetId, sellAsset.symbol, buyAsset.assetId, buyAsset.symbol], - ); - const { data: marketData } = useMarketData( - assetIdsForPrices, - symbolsForPrices, - ); + const { data: marketData } = useMarketData(assetIdsForPrices); const sellAssetUsdPrice = marketData?.[sellAsset.assetId]?.price; const buyAssetUsdPrice = marketData?.[buyAsset.assetId]?.price; diff --git a/packages/swap-widget-poc/src/demo/App.css b/packages/swap-widget-poc/src/demo/App.css index b9dfb20f70e..8ac66e33e0c 100644 --- a/packages/swap-widget-poc/src/demo/App.css +++ b/packages/swap-widget-poc/src/demo/App.css @@ -144,10 +144,7 @@ body, font-weight: 700; letter-spacing: -0.03em; margin-bottom: 12px; - background: linear-gradient(135deg, var(--demo-text) 30%, var(--demo-accent)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + color: var(--demo-accent); } .demo-subtitle { diff --git a/packages/swap-widget-poc/src/hooks/useMarketData.ts b/packages/swap-widget-poc/src/hooks/useMarketData.ts index da7743ba079..7680525957e 100644 --- a/packages/swap-widget-poc/src/hooks/useMarketData.ts +++ b/packages/swap-widget-poc/src/hooks/useMarketData.ts @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { adapters } from "@shapeshiftoss/caip"; import type { AssetId } from "../types"; const MARKET_DATA_STALE_TIME = 10 * 60 * 1000; @@ -25,50 +26,6 @@ type CoinGeckoMarketCap = { const COINGECKO_PROXY_URL = "https://api.proxy.shapeshift.com/api/v1/markets"; const COINGECKO_DIRECT_URL = "https://api.coingecko.com/api/v3"; -const SYMBOL_TO_COINGECKO_ID: Record = { - ETH: "ethereum", - BTC: "bitcoin", - USDC: "usd-coin", - USDT: "tether", - DAI: "dai", - WETH: "weth", - WBTC: "wrapped-bitcoin", - MATIC: "matic-network", - POL: "matic-network", - AVAX: "avalanche-2", - BNB: "binancecoin", - ARB: "arbitrum", - OP: "optimism", - SOL: "solana", - ATOM: "cosmos", - RUNE: "thorchain", - FOX: "shapeshift-fox-token", - LINK: "chainlink", - UNI: "uniswap", - AAVE: "aave", - CRV: "curve-dao-token", - LDO: "lido-dao", - MKR: "maker", - SNX: "havven", - COMP: "compound-governance-token", - GRT: "the-graph", - ENS: "ethereum-name-service", - SHIB: "shiba-inu", - PEPE: "pepe", - APE: "apecoin", - DOGE: "dogecoin", - LTC: "litecoin", - BCH: "bitcoin-cash", - XRP: "ripple", - ADA: "cardano", - DOT: "polkadot", - TRX: "tron", - NEAR: "near", - FTM: "fantom", - GNO: "gnosis", - XDAI: "xdai", -}; - const fetchWithRetry = async ( url: string, retries = 2, @@ -129,32 +86,20 @@ const fetchAllMarketData = async (): Promise => { } } - const coingeckoIdToMarketData = new Map(); for (const asset of allData) { - coingeckoIdToMarketData.set(asset.id, { - 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 [symbol, coingeckoId] of Object.entries( - SYMBOL_TO_COINGECKO_ID, - )) { - const marketData = coingeckoIdToMarketData.get(coingeckoId); - if (marketData) { - result[`symbol:${symbol.toLowerCase()}`] = marketData; - } - } + const assetIds = adapters.coingeckoToAssetIds(asset.id); + if (!assetIds?.length) continue; - for (const asset of allData) { - result[`coingecko:${asset.id}`] = { + const marketData: MarketData = { 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; + } } } catch (error) { console.error("Failed to fetch market data:", error); @@ -174,10 +119,7 @@ export const useAllMarketData = () => { }); }; -export const useMarketData = ( - assetIds: AssetId[], - symbols?: Record, -) => { +export const useMarketData = (assetIds: AssetId[]) => { const { data: allMarketData, ...rest } = useAllMarketData(); const filteredData = (() => { @@ -185,14 +127,6 @@ export const useMarketData = ( const result: MarketDataById = {}; for (const assetId of assetIds) { - const symbol = symbols?.[assetId]; - if (symbol) { - const symbolKey = `symbol:${symbol.toLowerCase()}`; - if (allMarketData[symbolKey]) { - result[assetId] = allMarketData[symbolKey]; - continue; - } - } if (allMarketData[assetId]) { result[assetId] = allMarketData[assetId]; } @@ -203,24 +137,13 @@ export const useMarketData = ( return { data: filteredData, ...rest }; }; -export const useAssetPrice = ( - assetId: AssetId | undefined, - symbol?: string, -) => { +export const useAssetPrice = (assetId: AssetId | undefined) => { const { data: allMarketData, ...rest } = useAllMarketData(); - const price = (() => { - if (!assetId || !allMarketData) return undefined; - if (symbol) { - const symbolKey = `symbol:${symbol.toLowerCase()}`; - if (allMarketData[symbolKey]) { - return allMarketData[symbolKey].price; - } - } - return allMarketData[assetId]?.price; - })(); - - return { data: price, ...rest }; + return { + data: assetId ? allMarketData?.[assetId]?.price : undefined, + ...rest, + }; }; export const formatUsdValue = ( From 512ab9f363e1cca9379f29f29f03e40752ff4a5f Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:34:47 +0100 Subject: [PATCH 10/41] fix: lock body scroll when modal open, style scrollbars to match theme --- .../src/components/QuotesModal.css | 12 +- .../src/components/QuotesModal.tsx | 217 +++++++++++------- .../src/components/SettingsModal.tsx | 14 +- .../src/components/TokenSelectModal.css | 38 +++ .../src/components/TokenSelectModal.tsx | 14 +- 5 files changed, 205 insertions(+), 90 deletions(-) diff --git a/packages/swap-widget-poc/src/components/QuotesModal.css b/packages/swap-widget-poc/src/components/QuotesModal.css index d985647df69..3234bde1318 100644 --- a/packages/swap-widget-poc/src/components/QuotesModal.css +++ b/packages/swap-widget-poc/src/components/QuotesModal.css @@ -96,10 +96,12 @@ display: flex; flex-direction: column; gap: 6px; + scrollbar-width: thin; + scrollbar-color: var(--ssw-border) transparent; } .ssw-quotes-modal-list::-webkit-scrollbar { - width: 4px; + width: 6px; } .ssw-quotes-modal-list::-webkit-scrollbar-track { @@ -107,8 +109,12 @@ } .ssw-quotes-modal-list::-webkit-scrollbar-thumb { - background: var(--ssw-border, rgba(255, 255, 255, 0.08)); - border-radius: 2px; + background: var(--ssw-border); + border-radius: 3px; +} + +.ssw-quotes-modal-list::-webkit-scrollbar-thumb:hover { + background: var(--ssw-border-hover); } .ssw-quote-row { diff --git a/packages/swap-widget-poc/src/components/QuotesModal.tsx b/packages/swap-widget-poc/src/components/QuotesModal.tsx index 131ba479498..c77863142c7 100644 --- a/packages/swap-widget-poc/src/components/QuotesModal.tsx +++ b/packages/swap-widget-poc/src/components/QuotesModal.tsx @@ -1,31 +1,45 @@ -import './QuotesModal.css' +import "./QuotesModal.css"; -import { useCallback, 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"; + return () => { + 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, @@ -38,121 +52,154 @@ export const QuotesModal = ({ sellAmountBaseUnit, buyAssetUsdPrice, }: QuotesModalProps) => { + 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} +
+
+
+
+

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 ( - ) + ); })}
- ) -} + ); +}; diff --git a/packages/swap-widget-poc/src/components/SettingsModal.tsx b/packages/swap-widget-poc/src/components/SettingsModal.tsx index 7c781839f0e..850211b5f54 100644 --- a/packages/swap-widget-poc/src/components/SettingsModal.tsx +++ b/packages/swap-widget-poc/src/components/SettingsModal.tsx @@ -1,8 +1,19 @@ -import { useState, useCallback } from "react"; +import { useState, useCallback, useEffect } from "react"; import "./SettingsModal.css"; 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"; + return () => { + document.body.style.overflow = originalOverflow; + }; + }, [isLocked]); +}; + type SettingsModalProps = { isOpen: boolean; onClose: () => void; @@ -16,6 +27,7 @@ export const SettingsModal = ({ slippage, onSlippageChange, }: SettingsModalProps) => { + useLockBodyScroll(isOpen); const [customSlippage, setCustomSlippage] = useState(""); const [isCustom, setIsCustom] = useState( !SLIPPAGE_PRESETS.includes(slippage), diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.css b/packages/swap-widget-poc/src/components/TokenSelectModal.css index 169fc324826..5f318a44fe3 100644 --- a/packages/swap-widget-poc/src/components/TokenSelectModal.css +++ b/packages/swap-widget-poc/src/components/TokenSelectModal.css @@ -107,6 +107,25 @@ flex: 1; overflow-y: auto; padding: 0 8px 16px; + scrollbar-width: thin; + scrollbar-color: var(--ssw-border) transparent; +} + +.ssw-chain-list::-webkit-scrollbar { + width: 6px; +} + +.ssw-chain-list::-webkit-scrollbar-track { + background: transparent; +} + +.ssw-chain-list::-webkit-scrollbar-thumb { + background: var(--ssw-border); + border-radius: 3px; +} + +.ssw-chain-list::-webkit-scrollbar-thumb:hover { + background: var(--ssw-border-hover); } .ssw-chain-item { @@ -181,6 +200,25 @@ flex: 1; overflow-y: auto; padding: 0 16px 16px; + scrollbar-width: thin; + scrollbar-color: var(--ssw-border) transparent; +} + +.ssw-token-list::-webkit-scrollbar { + width: 6px; +} + +.ssw-token-list::-webkit-scrollbar-track { + background: transparent; +} + +.ssw-token-list::-webkit-scrollbar-thumb { + background: var(--ssw-border); + border-radius: 3px; +} + +.ssw-token-list::-webkit-scrollbar-thumb:hover { + background: var(--ssw-border-hover); } .ssw-token-item { diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx index f075d78cd32..a5a0c925423 100644 --- a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx +++ b/packages/swap-widget-poc/src/components/TokenSelectModal.tsx @@ -1,10 +1,21 @@ -import { useState, useMemo, useCallback } from "react"; +import { useState, useMemo, useCallback, useEffect } from "react"; 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 useLockBodyScroll = (isLocked: boolean) => { + useEffect(() => { + if (!isLocked) return; + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = originalOverflow; + }; + }, [isLocked]); +}; + type TokenSelectModalProps = { isOpen: boolean; onClose: () => void; @@ -46,6 +57,7 @@ export const TokenSelectModal = ({ disabledChainIds = [], walletAddress, }: TokenSelectModalProps) => { + useLockBodyScroll(isOpen); const [searchQuery, setSearchQuery] = useState(""); const [chainSearchQuery, setChainSearchQuery] = useState(""); const [selectedChainId, setSelectedChainId] = useState(null); From a2b6e85a6e633c3117d1e7a2939f498bed38adfa Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:39:39 +0100 Subject: [PATCH 11/41] fix: reverts --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index d2e0c642f48..1e409a035cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12121,7 +12121,7 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/caip@^8.15.0, @shapeshiftoss/caip@workspace:*, @shapeshiftoss/caip@workspace:^, @shapeshiftoss/caip@workspace:packages/caip": +"@shapeshiftoss/caip@^8.15.0, @shapeshiftoss/caip@^8.16.5, @shapeshiftoss/caip@workspace:^, @shapeshiftoss/caip@workspace:packages/caip": version: 0.0.0-use.local resolution: "@shapeshiftoss/caip@workspace:packages/caip" dependencies: @@ -12696,7 +12696,7 @@ __metadata: resolution: "@shapeshiftoss/swap-widget-poc@workspace:packages/swap-widget-poc" dependencies: "@rainbow-me/rainbowkit": ^2.2.3 - "@shapeshiftoss/caip": "workspace:*" + "@shapeshiftoss/caip": ^8.16.5 "@tanstack/react-query": ^5.60.0 "@types/react": ^18.2.0 "@types/react-dom": ^18.2.0 From 08ec1633a0195ce0254356311130628f8b9113d2 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:46:26 +0100 Subject: [PATCH 12/41] fix: skip rate fetching for non-EVM sell assets, show redirect button immediately --- .../src/components/SwapWidget.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget-poc/src/components/SwapWidget.tsx index 1c5452a758b..84e8df5c6da 100644 --- a/packages/swap-widget-poc/src/components/SwapWidget.tsx +++ b/packages/swap-widget-poc/src/components/SwapWidget.tsx @@ -92,6 +92,10 @@ const SwapWidgetInner = ({ [sellAmount, sellAsset.precision], ); + const isSellAssetEvm = getChainType(sellAsset.chainId) === "evm"; + const isBuyAssetEvm = getChainType(buyAsset.chainId) === "evm"; + const canExecuteDirectly = isSellAssetEvm && isBuyAssetEvm; + const { data: rates, isLoading: isLoadingRates, @@ -100,7 +104,8 @@ const SwapWidgetInner = ({ sellAssetId: sellAsset.assetId, buyAssetId: buyAsset.assetId, sellAmountCryptoBaseUnit: sellAmountBaseUnit, - enabled: !!sellAmountBaseUnit && sellAmountBaseUnit !== "0", + enabled: + !!sellAmountBaseUnit && sellAmountBaseUnit !== "0" && isSellAssetEvm, }); const walletAddress = useMemo(() => { @@ -108,10 +113,6 @@ const SwapWidgetInner = ({ return (walletClient as WalletClient).account?.address; }, [walletClient]); - const isSellAssetEvm = getChainType(sellAsset.chainId) === "evm"; - const isBuyAssetEvm = getChainType(buyAsset.chainId) === "evm"; - const canExecuteDirectly = isSellAssetEvm && isBuyAssetEvm; - const { data: sellAssetBalance, isLoading: isSellBalanceLoading, @@ -311,8 +312,9 @@ const SwapWidgetInner = ({ }, [walletClient, canExecuteDirectly, onConnectWallet, handleExecuteSwap]); const buttonText = useMemo(() => { - if (!walletClient && canExecuteDirectly) return "Connect Wallet"; if (!sellAmount) return "Enter an amount"; + if (!isSellAssetEvm) return "Proceed on ShapeShift"; + if (!walletClient && canExecuteDirectly) return "Connect Wallet"; if (isLoadingRates) return "Finding rates..."; if (ratesError) return "No routes available"; if (!rates?.length) return "No routes found"; @@ -322,6 +324,7 @@ const SwapWidgetInner = ({ }, [ walletClient, canExecuteDirectly, + isSellAssetEvm, sellAmount, isLoadingRates, ratesError, @@ -331,12 +334,20 @@ const SwapWidgetInner = ({ const isButtonDisabled = useMemo(() => { if (!sellAmount) return true; + if (!isSellAssetEvm) return false; if (isLoadingRates) return true; if (ratesError) return true; if (!rates?.length) return true; if (isExecuting) return true; return false; - }, [sellAmount, isLoadingRates, ratesError, rates, isExecuting]); + }, [ + sellAmount, + isSellAssetEvm, + isLoadingRates, + ratesError, + rates, + isExecuting, + ]); const { data: sellChainInfo } = useChainInfo(sellAsset.chainId); const { data: buyChainInfo } = useChainInfo(buyAsset.chainId); From 15570b60318dbdfacbaa4fe3aed0af6736e34089 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:57:20 +0100 Subject: [PATCH 13/41] fix: redirect to ShapeShift for non-EVM sell assets before rate check --- .../swap-widget-poc/src/components/SwapWidget.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget-poc/src/components/SwapWidget.tsx index 84e8df5c6da..611ce658b8b 100644 --- a/packages/swap-widget-poc/src/components/SwapWidget.tsx +++ b/packages/swap-widget-poc/src/components/SwapWidget.tsx @@ -151,6 +151,19 @@ const SwapWidgetInner = ({ ); const handleExecuteSwap = useCallback(async () => { + if (!isSellAssetEvm) { + const params = new URLSearchParams({ + sellAssetId: sellAsset.assetId, + buyAssetId: buyAsset.assetId, + sellAmount, + }); + window.open( + `https://app.shapeshift.com/trade?${params.toString()}`, + "_blank", + ); + return; + } + const rateToUse = selectedRate ?? rates?.[0]; if (!rateToUse || !walletClient || !walletAddress) return; @@ -291,6 +304,7 @@ const SwapWidgetInner = ({ walletClient, walletAddress, canExecuteDirectly, + isSellAssetEvm, sellAsset, buyAsset, sellAmount, From 252d8fb2202238c701e5c78521ea7e71620ae070 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:02:12 +0100 Subject: [PATCH 14/41] fix: butter solana swaps --- packages/swap-widget-poc/src/components/SwapWidget.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget-poc/src/components/SwapWidget.tsx index 611ce658b8b..3293200fa24 100644 --- a/packages/swap-widget-poc/src/components/SwapWidget.tsx +++ b/packages/swap-widget-poc/src/components/SwapWidget.tsx @@ -326,8 +326,8 @@ const SwapWidgetInner = ({ }, [walletClient, canExecuteDirectly, onConnectWallet, handleExecuteSwap]); const buttonText = useMemo(() => { - if (!sellAmount) return "Enter an amount"; 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"; @@ -347,16 +347,16 @@ const SwapWidgetInner = ({ ]); const isButtonDisabled = useMemo(() => { + if (!isSellAssetEvm) return false; // Allow non-EVM without amount if (!sellAmount) return true; - if (!isSellAssetEvm) return false; if (isLoadingRates) return true; if (ratesError) return true; if (!rates?.length) return true; if (isExecuting) return true; return false; }, [ - sellAmount, isSellAssetEvm, + sellAmount, isLoadingRates, ratesError, rates, From 3d1368d8d36bd61d6f1621ffc66e2ab77eee9e75 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:23:54 +0100 Subject: [PATCH 15/41] feat: external address --- .../src/components/AddressInputModal.css | 144 ++++++ .../src/components/AddressInputModal.tsx | 223 +++++++++ .../src/components/SwapWidget.css | 43 +- .../src/components/SwapWidget.tsx | 48 +- packages/swap-widget-poc/src/demo/App.tsx | 426 +++++++++--------- .../src/utils/addressValidation.ts | 218 +++++++++ 6 files changed, 871 insertions(+), 231 deletions(-) create mode 100644 packages/swap-widget-poc/src/components/AddressInputModal.css create mode 100644 packages/swap-widget-poc/src/components/AddressInputModal.tsx create mode 100644 packages/swap-widget-poc/src/utils/addressValidation.ts diff --git a/packages/swap-widget-poc/src/components/AddressInputModal.css b/packages/swap-widget-poc/src/components/AddressInputModal.css new file mode 100644 index 00000000000..2e79f75a1b4 --- /dev/null +++ b/packages/swap-widget-poc/src/components/AddressInputModal.css @@ -0,0 +1,144 @@ +.ssw-address-modal { + background: var(--ssw-bg-secondary); + border-radius: 16px; + width: 90%; + max-width: 420px; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.ssw-address-content { + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.ssw-address-label { + font-size: 14px; + color: var(--ssw-text-secondary); +} + +.ssw-address-input-wrapper { + display: flex; + align-items: center; + gap: 8px; + background: var(--ssw-bg-tertiary); + border: 1px solid var(--ssw-border); + border-radius: 12px; + padding: 12px 16px; + transition: border-color 0.15s ease; +} + +.ssw-address-input-wrapper:focus-within { + border-color: var(--ssw-accent); +} + +.ssw-address-input-wrapper.ssw-invalid { + border-color: #ef4444; +} + +.ssw-address-input { + flex: 1; + background: none; + border: none; + outline: none; + color: var(--ssw-text-primary); + font-size: 14px; + font-family: monospace; +} + +.ssw-address-input::placeholder { + color: var(--ssw-text-muted); +} + +.ssw-address-clear-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + border: none; + background: none; + color: var(--ssw-text-muted); + cursor: pointer; + border-radius: 4px; + transition: all 0.15s ease; +} + +.ssw-address-clear-btn:hover { + color: var(--ssw-text-primary); + background: var(--ssw-bg-hover); +} + +.ssw-address-error { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: #ef4444; +} + +.ssw-use-wallet-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + background: var(--ssw-bg-tertiary); + border: 1px solid var(--ssw-border); + border-radius: 10px; + color: var(--ssw-text-secondary); + font-size: 14px; + cursor: pointer; + transition: all 0.15s ease; +} + +.ssw-use-wallet-btn:hover { + background: var(--ssw-bg-hover); + color: var(--ssw-text-primary); +} + +.ssw-address-actions { + display: flex; + gap: 12px; + margin-top: 8px; +} + +.ssw-address-btn { + flex: 1; + padding: 14px 16px; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; +} + +.ssw-address-btn-secondary { + background: var(--ssw-bg-tertiary); + border: 1px solid var(--ssw-border); + color: var(--ssw-text-secondary); +} + +.ssw-address-btn-secondary:hover { + background: var(--ssw-bg-hover); + color: var(--ssw-text-primary); +} + +.ssw-address-btn-primary { + background: var(--ssw-accent); + border: none; + color: white; +} + +.ssw-address-btn-primary:hover:not(:disabled) { + filter: brightness(1.1); +} + +.ssw-address-btn-primary:disabled { + background: var(--ssw-bg-tertiary); + color: var(--ssw-text-muted); + cursor: not-allowed; +} diff --git a/packages/swap-widget-poc/src/components/AddressInputModal.tsx b/packages/swap-widget-poc/src/components/AddressInputModal.tsx new file mode 100644 index 00000000000..54075b3d5e3 --- /dev/null +++ b/packages/swap-widget-poc/src/components/AddressInputModal.tsx @@ -0,0 +1,223 @@ +import { useState, useCallback, useEffect, useMemo } from "react"; +import type { ChainId } from "../types"; +import { + validateAddress, + getAddressFormatHint, +} from "../utils/addressValidation"; +import "./AddressInputModal.css"; + +const useLockBodyScroll = (isLocked: boolean) => { + useEffect(() => { + if (!isLocked) return; + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = originalOverflow; + }; + }, [isLocked]); +}; + +type AddressInputModalProps = { + isOpen: boolean; + onClose: () => void; + chainId: ChainId; + chainName: string; + currentAddress: string; + onAddressChange: (address: string) => void; + walletAddress?: string; +}; + +export const AddressInputModal = ({ + isOpen, + onClose, + chainId, + chainName, + currentAddress, + onAddressChange, + walletAddress, +}: AddressInputModalProps) => { + useLockBodyScroll(isOpen); + const [inputValue, setInputValue] = useState(currentAddress); + const [hasInteracted, setHasInteracted] = useState(false); + + useEffect(() => { + if (isOpen) { + setInputValue(currentAddress); + setHasInteracted(false); + } + }, [isOpen, currentAddress]); + + const validation = useMemo(() => { + if (!inputValue || !hasInteracted) { + return { valid: true, error: undefined }; + } + return validateAddress(inputValue, chainId); + }, [inputValue, chainId, hasInteracted]); + + const formatHint = useMemo(() => getAddressFormatHint(chainId), [chainId]); + + const handleInputChange = useCallback((value: string) => { + setInputValue(value); + setHasInteracted(true); + }, []); + + const handleUseWalletAddress = useCallback(() => { + if (walletAddress) { + setInputValue(walletAddress); + setHasInteracted(true); + } + }, [walletAddress]); + + const handleConfirm = useCallback(() => { + const result = validateAddress(inputValue, chainId); + if (result.valid) { + onAddressChange(inputValue); + onClose(); + } + }, [inputValue, chainId, onAddressChange, onClose]); + + const handleClear = useCallback(() => { + setInputValue(""); + setHasInteracted(false); + onAddressChange(""); + onClose(); + }, [onAddressChange, onClose]); + + const handleBackdropClick = useCallback( + (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }, + [onClose], + ); + + const isConfirmDisabled = useMemo(() => { + if (!inputValue) return true; + return !validateAddress(inputValue, chainId).valid; + }, [inputValue, chainId]); + + if (!isOpen) return null; + + return ( +
+
+
+

Receive Address

+ +
+ +
+
+ Enter {chainName} address +
+ +
+ handleInputChange(e.target.value)} + autoFocus + spellCheck={false} + autoComplete="off" + /> + {inputValue && ( + + )} +
+ + {!validation.valid && hasInteracted && validation.error && ( +
+ + + + + {validation.error} +
+ )} + + {walletAddress && ( + + )} + +
+ + +
+
+
+
+ ); +}; diff --git a/packages/swap-widget-poc/src/components/SwapWidget.css b/packages/swap-widget-poc/src/components/SwapWidget.css index 01a8334538b..c1fef415615 100644 --- a/packages/swap-widget-poc/src/components/SwapWidget.css +++ b/packages/swap-widget-poc/src/components/SwapWidget.css @@ -269,6 +269,43 @@ background: var(--ssw-bg-hover); } +.ssw-receive-address-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: var(--ssw-bg-tertiary); + border: 1px solid var(--ssw-border); + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease; +} + +.ssw-receive-address-btn:hover { + background: var(--ssw-bg-hover); + border-color: var(--ssw-accent); +} + +.ssw-receive-address-btn.ssw-custom { + border-color: var(--ssw-accent); + background: color-mix(in srgb, var(--ssw-accent) 10%, var(--ssw-bg-tertiary)); +} + +.ssw-receive-address-text { + font-size: 12px; + font-family: monospace; + color: var(--ssw-accent); +} + +.ssw-receive-address-btn svg { + color: var(--ssw-text-muted); + flex-shrink: 0; +} + +.ssw-receive-address-btn:hover svg { + color: var(--ssw-accent); +} + .ssw-quotes { padding: 0 16px 16px; } @@ -298,13 +335,13 @@ } .ssw-action-btn.ssw-secondary { - background: var(--ssw-bg-tertiary); + background: color-mix(in srgb, var(--ssw-accent) 15%, var(--ssw-bg-tertiary)); color: var(--ssw-text-primary); - border: 1px solid var(--ssw-border); + border: 1px solid color-mix(in srgb, var(--ssw-accent) 40%, transparent); } .ssw-action-btn.ssw-secondary:hover:not(:disabled) { - background: var(--ssw-bg-hover); + background: color-mix(in srgb, var(--ssw-accent) 25%, var(--ssw-bg-tertiary)); filter: none; } diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget-poc/src/components/SwapWidget.tsx index 3293200fa24..0da1c267da3 100644 --- a/packages/swap-widget-poc/src/components/SwapWidget.tsx +++ b/packages/swap-widget-poc/src/components/SwapWidget.tsx @@ -18,6 +18,7 @@ import { import { TokenSelectModal } from "./TokenSelectModal"; import { SettingsModal } from "./SettingsModal"; import { QuoteSelector } from "./QuoteSelector"; +import { AddressInputModal } from "./AddressInputModal"; import "./SwapWidget.css"; const DEFAULT_SELL_ASSET: Asset = { @@ -82,6 +83,8 @@ const SwapWidgetInner = ({ 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; @@ -113,6 +116,14 @@ const SwapWidgetInner = ({ return (walletClient as WalletClient).account?.address; }, [walletClient]); + const effectiveReceiveAddress = useMemo(() => { + return customReceiveAddress || walletAddress || ""; + }, [customReceiveAddress, walletAddress]); + + const isCustomAddress = useMemo(() => { + return !!customReceiveAddress && customReceiveAddress !== walletAddress; + }, [customReceiveAddress, walletAddress]); + const { data: sellAssetBalance, isLoading: isSellBalanceLoading, @@ -539,11 +550,30 @@ const SwapWidgetInner = ({
Buy - {walletAddress && isBuyAssetEvm && ( - - {truncateAddress(walletAddress)} +
@@ -746,6 +776,16 @@ const SwapWidgetInner = ({ slippage={slippage} onSlippageChange={setSlippage} /> + + setIsAddressModalOpen(false)} + chainId={buyAsset.chainId} + chainName={buyChainInfo?.name ?? buyAsset.networkName ?? buyAsset.name} + currentAddress={customReceiveAddress || walletAddress || ""} + onAddressChange={setCustomReceiveAddress} + walletAddress={walletAddress} + />
); }; diff --git a/packages/swap-widget-poc/src/demo/App.tsx b/packages/swap-widget-poc/src/demo/App.tsx index 14b6132a92a..f974987a851 100644 --- a/packages/swap-widget-poc/src/demo/App.tsx +++ b/packages/swap-widget-poc/src/demo/App.tsx @@ -1,91 +1,93 @@ -import { useState, useMemo, useCallback } from "react"; +import '@rainbow-me/rainbowkit/styles.css' +import './App.css' + import { - RainbowKitProvider, ConnectButton, - getDefaultConfig, darkTheme, + getDefaultConfig, lightTheme, -} from "@rainbow-me/rainbowkit"; -import { WagmiProvider, useAccount, useWalletClient } from "wagmi"; -import { mainnet, polygon, arbitrum, optimism, base } from "wagmi/chains"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { SwapWidget } from "../components/SwapWidget"; -import type { ThemeConfig } from "../types"; -import "@rainbow-me/rainbowkit/styles.css"; -import "./App.css"; + RainbowKitProvider, +} from '@rainbow-me/rainbowkit' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { useCallback, useMemo, useState } from 'react' +import { useAccount, useWalletClient, WagmiProvider } from 'wagmi' +import { arbitrum, base, mainnet, optimism, polygon } from 'wagmi/chains' + +import { SwapWidget } from '../components/SwapWidget' +import type { ThemeConfig } from '../types' const config = getDefaultConfig({ - appName: "ShapeShift Swap Widget", - projectId: "demo-project-id", + appName: 'ShapeShift Swap Widget', + projectId: 'demo-project-id', chains: [mainnet, polygon, arbitrum, optimism, base], ssr: false, -}); +}) -const queryClient = new QueryClient(); +const queryClient = new QueryClient() type ThemeColors = { - bg: string; - card: string; - accent: string; -}; + bg: string + card: string + accent: string +} -const THEME_PRESETS: Array<{ - name: string; - dark: ThemeColors; - light: ThemeColors; -}> = [ +const THEME_PRESETS: { + name: string + dark: ThemeColors + light: ThemeColors +}[] = [ { - name: "Blue", - dark: { bg: "#0a0a14", card: "#12121c", accent: "#3861fb" }, - light: { bg: "#f8f9fc", card: "#ffffff", accent: "#3861fb" }, + name: 'Blue', + dark: { bg: '#0a0a14', card: '#12121c', accent: '#3861fb' }, + light: { bg: '#f8f9fc', card: '#ffffff', accent: '#3861fb' }, }, { - name: "Rose", - dark: { bg: "#140a0f", card: "#1c1218", accent: "#f43f5e" }, - light: { bg: "#fef2f4", card: "#ffffff", accent: "#f43f5e" }, + name: 'Rose', + dark: { bg: '#140a0f', card: '#1c1218', accent: '#f43f5e' }, + light: { bg: '#fef2f4', card: '#ffffff', accent: '#f43f5e' }, }, { - name: "Purple", - dark: { bg: "#0e0a14", card: "#1a1424", accent: "#a855f7" }, - light: { bg: "#faf5ff", card: "#ffffff", accent: "#a855f7" }, + name: 'Purple', + dark: { bg: '#0e0a14', card: '#1a1424', accent: '#a855f7' }, + light: { bg: '#faf5ff', card: '#ffffff', accent: '#a855f7' }, }, { - name: "Cyan", - dark: { bg: "#0a1214", card: "#141d20", accent: "#06b6d4" }, - light: { bg: "#f0fdff", card: "#ffffff", accent: "#06b6d4" }, + name: 'Cyan', + dark: { bg: '#0a1214', card: '#141d20', accent: '#06b6d4' }, + light: { bg: '#f0fdff', card: '#ffffff', accent: '#06b6d4' }, }, { - name: "Green", - dark: { bg: "#0a140e", card: "#141c18", accent: "#10b981" }, - light: { bg: "#f0fdf6", card: "#ffffff", accent: "#10b981" }, + name: 'Green', + dark: { bg: '#0a140e', card: '#141c18', accent: '#10b981' }, + light: { bg: '#f0fdf6', card: '#ffffff', accent: '#10b981' }, }, { - name: "Orange", - dark: { bg: "#14100a", card: "#1c1814", accent: "#f97316" }, - light: { bg: "#fff8f3", card: "#ffffff", accent: "#f97316" }, + name: 'Orange', + dark: { bg: '#14100a', card: '#1c1814', accent: '#f97316' }, + light: { bg: '#fff8f3', card: '#ffffff', accent: '#f97316' }, }, -]; +] const DemoContent = () => { - const { address, isConnected } = useAccount(); - const { data: walletClient } = useWalletClient(); - const [theme, setTheme] = useState<"light" | "dark">("dark"); - const [showCustomizer, setShowCustomizer] = useState(true); + const { address, isConnected } = useAccount() + const { data: walletClient } = useWalletClient() + const [theme, setTheme] = useState<'light' | 'dark'>('dark') + const [showCustomizer, setShowCustomizer] = useState(true) const [darkColors, setDarkColors] = useState({ - bg: "#0a0a14", - card: "#12121c", - accent: "#3861fb", - }); + bg: '#0a0a14', + card: '#12121c', + accent: '#3861fb', + }) const [lightColors, setLightColors] = useState({ - bg: "#f8f9fc", - card: "#ffffff", - accent: "#3861fb", - }); + bg: '#f8f9fc', + card: '#ffffff', + accent: '#3861fb', + }) - const currentColors = theme === "dark" ? darkColors : lightColors; - const setCurrentColors = theme === "dark" ? setDarkColors : setLightColors; + const currentColors = theme === 'dark' ? darkColors : lightColors + const setCurrentColors = theme === 'dark' ? setDarkColors : setLightColors const themeConfig: ThemeConfig = useMemo( () => ({ @@ -95,14 +97,14 @@ const DemoContent = () => { cardColor: currentColors.card, }), [theme, currentColors], - ); + ) const applyPreset = (preset: (typeof THEME_PRESETS)[0]) => { - setDarkColors(preset.dark); - setLightColors(preset.light); - }; + setDarkColors(preset.dark) + setLightColors(preset.light) + } - const [copied, setCopied] = useState(false); + const [copied, setCopied] = useState(false) const copyConfig = useCallback(() => { const code = `const themeConfig = { @@ -123,62 +125,62 @@ const DemoContent = () => { // Usage: // or -`; +` navigator.clipboard.writeText(code).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - }, [darkColors, lightColors]); + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }) + }, [darkColors, lightColors]) const handleSwapSuccess = (txHash: string) => { - console.log("Swap successful:", txHash); - }; + console.log('Swap successful:', txHash) + } const handleSwapError = (error: Error) => { - console.error("Swap failed:", error); - }; + console.error('Swap failed:', error) + } const demoStyle = useMemo( () => ({ - "--demo-page-bg": currentColors.bg, - "--demo-page-accent": currentColors.accent, + '--demo-page-bg': currentColors.bg, + '--demo-page-accent': currentColors.accent, }) as React.CSSProperties, [currentColors], - ); + ) return (
-
+
- - + + - ShapeShift + ShapeShift -
+
@@ -186,236 +188,212 @@ const DemoContent = () => {
-
-
-
-

Swap Widget

-

+

+
+
+

Swap Widget

+

Embeddable multi-chain swap widget powered by ShapeShift

-
+
{showCustomizer && ( -
-

Customize Widget

- -
- -
- {THEME_PRESETS.map((preset) => { - const previewColors = - theme === "dark" ? preset.dark : preset.light; +
+

Customize Widget

+ +
+ +
+ {THEME_PRESETS.map(preset => { + const previewColors = theme === 'dark' ? preset.dark : preset.light return ( - ); + ) })}
-
- -
+
+ +
-
- -
+
+ +
- setCurrentColors((c) => ({ ...c, bg: e.target.value })) - } - className="demo-color-picker" + onChange={e => setCurrentColors(c => ({ ...c, bg: e.target.value }))} + className='demo-color-picker' /> - setCurrentColors((c) => ({ ...c, bg: e.target.value })) - } - className="demo-color-text" + onChange={e => setCurrentColors(c => ({ ...c, bg: e.target.value }))} + className='demo-color-text' />
-
- -
+
+ +
- setCurrentColors((c) => ({ + onChange={e => + setCurrentColors(c => ({ ...c, card: e.target.value, })) } - className="demo-color-picker" + className='demo-color-picker' /> - setCurrentColors((c) => ({ + onChange={e => + setCurrentColors(c => ({ ...c, card: e.target.value, })) } - className="demo-color-text" + className='demo-color-text' />
-
- -
+
+ +
- setCurrentColors((c) => ({ + onChange={e => + setCurrentColors(c => ({ ...c, accent: e.target.value, })) } - className="demo-color-picker" + className='demo-color-picker' /> - setCurrentColors((c) => ({ + onChange={e => + setCurrentColors(c => ({ ...c, accent: e.target.value, })) } - className="demo-color-text" + className='demo-color-text' />
-
- -
+
+ +
{isConnected ? ( <> - Connected - + Connected + {address?.slice(0, 6)}...{address?.slice(-4)} ) : ( - Not connected + Not connected )}
-
-
)} -
+
{
-
-

Built with ❤️ by ShapeShift DAO

+
+

Built by ShapeShift DAO

- ); -}; + ) +} 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 ( - - ); - }) + + ) : 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

-
-
-
+
+
Enter {chainName} address
handleInputChange(e.target.value)} + onChange={e => handleInputChange(e.target.value)} autoFocus spellCheck={false} - autoComplete="off" + autoComplete='off' /> {inputValue && ( )}
{!validation.valid && hasInteracted && validation.error && ( -
+
- - + + {validation.error}
)} {walletAddress && ( - )} -
+
@@ -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 ( -
-
-
+
+
+
Finding best rates...
- ); + ) } 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 ( <> -
-
+
{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 ( - ); + ) })}
- ); -}; + ) +} 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

-
-
-
-
+
+
+
Slippage Tolerance
-
- {SLIPPAGE_PRESETS.map((preset) => ( +
+ {SLIPPAGE_PRESETS.map(preset => ( ))} -
+
handleCustomChange(e.target.value)} + type='text' + placeholder='Custom' + value={isCustom ? customSlippage || slippage : ''} + onChange={e => handleCustomChange(e.target.value)} onFocus={() => { - setIsCustom(true); - if (!customSlippage) setCustomSlippage(slippage); + setIsCustom(true) + if (!customSlippage) setCustomSlippage(slippage) }} /> - % + %
{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 - + + + + + +
-
-
-
- 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) }} />
-
- {sellUsdValue} +
+ {sellUsdValue} {walletAddress && (isSellBalanceLoading ? ( - + ) : sellAssetBalance ? ( - - Balance: {sellAssetBalance.balanceFormatted} - + Balance: {sellAssetBalance.balanceFormatted} ) : null)}
-
-
-
-
- Buy - + + {effectiveReceiveAddress + ? truncateAddress(effectiveReceiveAddress, 4) + : 'Enter address'} + + + + + + + )}
-
+
-
- {buyUsdValue} +
+ {buyUsdValue} {walletAddress && (isBuyBalanceLoading ? ( - + ) : buyAssetBalance ? ( - - Balance: {buyAssetBalance.balanceFormatted} - + Balance: {buyAssetBalance.balanceFormatted} ) : null)}
- {sellAmountBaseUnit && - sellAmountBaseUnit !== "0" && - (rates?.length || isLoadingRates) && ( -
- -
- )} + {sellAmountBaseUnit && sellAmountBaseUnit !== '0' && (rates?.length || isLoadingRates) && ( +
+ +
+ )} {txStatus && (
-
- {txStatus.status === "pending" && ( +
+ {txStatus.status === 'pending' && ( - - + + )} - {txStatus.status === "success" && ( + {txStatus.status === 'success' && ( - + )} - {txStatus.status === "error" && ( + {txStatus.status === 'error' && ( - - + + )}
-
- {txStatus.message} +
+ {txStatus.message} {txStatus.txHash && ( View transaction )}
-
)} {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

-
-
-
-
+
+
+
- - + + setChainSearchQuery(e.target.value)} + onChange={e => setChainSearchQuery(e.target.value)} />
-
+
- {filteredChains.map((chain) => ( + {filteredChains.map(chain => ( ))}
-
-
+
+
- - + + setSearchQuery(e.target.value)} + onChange={e => setSearchQuery(e.target.value)} autoFocus />
-
+
{isLoading ? ( -
-
+
+
Loading assets...
) : 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 ( - ); + ) }} /> )} @@ -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 ( + + ) + } + + if (chain.unsupported) { + return ( + + ) + } + + return ( + + ) + })()} +
+ ) + }} +
+ ) +} 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
{THEME_PRESETS.map(preset => { const previewColors = theme === 'dark' ? preset.dark : preset.light @@ -236,7 +236,7 @@ const DemoContent = () => {
- + Theme
- + Background Color
{
- + Card Color
{
- + Accent Color
{
- + 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 ; @@ -139,7 +139,7 @@ function App() { ### With Wallet Connection (wagmi/viem) ```tsx -import { SwapWidget } from "@shapeshiftoss/swap-widget-poc"; +import { SwapWidget } from "@shapeshiftoss/swap-widget"; import { useWalletClient } from "wagmi"; import { useConnectModal } from "@rainbow-me/rainbowkit"; @@ -172,8 +172,8 @@ function App() { ### With Custom Default Assets ```tsx -import { SwapWidget } from "@shapeshiftoss/swap-widget-poc"; -import type { Asset } from "@shapeshiftoss/swap-widget-poc"; +import { SwapWidget } from "@shapeshiftoss/swap-widget"; +import type { Asset } from "@shapeshiftoss/swap-widget"; const defaultSellAsset: Asset = { assetId: "eip155:137/slip44:966", @@ -210,7 +210,7 @@ function App() { 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"; +import { SwapWidget, EVM_CHAIN_IDS } from "@shapeshiftoss/swap-widget"; function App() { return ( @@ -235,7 +235,7 @@ function App() { 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"; +import { SwapWidget } from "@shapeshiftoss/swap-widget"; function App() { return ( @@ -254,7 +254,7 @@ function App() { 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"; +import { SwapWidget } from "@shapeshiftoss/swap-widget"; function App() { return ( @@ -281,7 +281,7 @@ import type { SwapWidgetProps, ThemeMode, ThemeConfig, -} from "@shapeshiftoss/swap-widget-poc"; +} from "@shapeshiftoss/swap-widget"; ``` ### Asset @@ -361,7 +361,7 @@ import { getChainName, getChainIcon, getChainColor, -} from "@shapeshiftoss/swap-widget-poc"; +} from "@shapeshiftoss/swap-widget"; ``` ### Chain Type Utilities @@ -431,7 +431,7 @@ import { useChains, useAssetsByChainId, useAssetSearch, -} from "@shapeshiftoss/swap-widget-poc"; +} from "@shapeshiftoss/swap-widget"; ``` | Hook | Return Type | Description | diff --git a/packages/swap-widget-poc/index.html b/packages/swap-widget/index.html similarity index 100% rename from packages/swap-widget-poc/index.html rename to packages/swap-widget/index.html diff --git a/packages/swap-widget-poc/package.json b/packages/swap-widget/package.json similarity index 88% rename from packages/swap-widget-poc/package.json rename to packages/swap-widget/package.json index 9b44aa4cddf..ac4527425d6 100644 --- a/packages/swap-widget-poc/package.json +++ b/packages/swap-widget/package.json @@ -1,8 +1,8 @@ { - "name": "@shapeshiftoss/swap-widget-poc", + "name": "@shapeshiftoss/swap-widget", "version": "0.0.1", "private": true, - "description": "POC: Embeddable swap widget using ShapeShift API", + "description": "Embeddable swap widget using ShapeShift API", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/swap-widget-poc/src/api/client.ts b/packages/swap-widget/src/api/client.ts similarity index 100% rename from packages/swap-widget-poc/src/api/client.ts rename to packages/swap-widget/src/api/client.ts diff --git a/packages/swap-widget-poc/src/components/AddressInputModal.css b/packages/swap-widget/src/components/AddressInputModal.css similarity index 100% rename from packages/swap-widget-poc/src/components/AddressInputModal.css rename to packages/swap-widget/src/components/AddressInputModal.css diff --git a/packages/swap-widget-poc/src/components/AddressInputModal.tsx b/packages/swap-widget/src/components/AddressInputModal.tsx similarity index 100% rename from packages/swap-widget-poc/src/components/AddressInputModal.tsx rename to packages/swap-widget/src/components/AddressInputModal.tsx diff --git a/packages/swap-widget-poc/src/components/QuoteSelector.css b/packages/swap-widget/src/components/QuoteSelector.css similarity index 100% rename from packages/swap-widget-poc/src/components/QuoteSelector.css rename to packages/swap-widget/src/components/QuoteSelector.css diff --git a/packages/swap-widget-poc/src/components/QuoteSelector.tsx b/packages/swap-widget/src/components/QuoteSelector.tsx similarity index 100% rename from packages/swap-widget-poc/src/components/QuoteSelector.tsx rename to packages/swap-widget/src/components/QuoteSelector.tsx diff --git a/packages/swap-widget-poc/src/components/QuotesModal.css b/packages/swap-widget/src/components/QuotesModal.css similarity index 100% rename from packages/swap-widget-poc/src/components/QuotesModal.css rename to packages/swap-widget/src/components/QuotesModal.css diff --git a/packages/swap-widget-poc/src/components/QuotesModal.tsx b/packages/swap-widget/src/components/QuotesModal.tsx similarity index 100% rename from packages/swap-widget-poc/src/components/QuotesModal.tsx rename to packages/swap-widget/src/components/QuotesModal.tsx diff --git a/packages/swap-widget-poc/src/components/SettingsModal.css b/packages/swap-widget/src/components/SettingsModal.css similarity index 100% rename from packages/swap-widget-poc/src/components/SettingsModal.css rename to packages/swap-widget/src/components/SettingsModal.css diff --git a/packages/swap-widget-poc/src/components/SettingsModal.tsx b/packages/swap-widget/src/components/SettingsModal.tsx similarity index 100% rename from packages/swap-widget-poc/src/components/SettingsModal.tsx rename to packages/swap-widget/src/components/SettingsModal.tsx diff --git a/packages/swap-widget-poc/src/components/SwapWidget.css b/packages/swap-widget/src/components/SwapWidget.css similarity index 100% rename from packages/swap-widget-poc/src/components/SwapWidget.css rename to packages/swap-widget/src/components/SwapWidget.css diff --git a/packages/swap-widget-poc/src/components/SwapWidget.tsx b/packages/swap-widget/src/components/SwapWidget.tsx similarity index 100% rename from packages/swap-widget-poc/src/components/SwapWidget.tsx rename to packages/swap-widget/src/components/SwapWidget.tsx diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.css b/packages/swap-widget/src/components/TokenSelectModal.css similarity index 100% rename from packages/swap-widget-poc/src/components/TokenSelectModal.css rename to packages/swap-widget/src/components/TokenSelectModal.css diff --git a/packages/swap-widget-poc/src/components/TokenSelectModal.tsx b/packages/swap-widget/src/components/TokenSelectModal.tsx similarity index 100% rename from packages/swap-widget-poc/src/components/TokenSelectModal.tsx rename to packages/swap-widget/src/components/TokenSelectModal.tsx diff --git a/packages/swap-widget-poc/src/components/WalletProvider.tsx b/packages/swap-widget/src/components/WalletProvider.tsx similarity index 100% rename from packages/swap-widget-poc/src/components/WalletProvider.tsx rename to packages/swap-widget/src/components/WalletProvider.tsx diff --git a/packages/swap-widget-poc/src/constants/chains.ts b/packages/swap-widget/src/constants/chains.ts similarity index 100% rename from packages/swap-widget-poc/src/constants/chains.ts rename to packages/swap-widget/src/constants/chains.ts diff --git a/packages/swap-widget-poc/src/constants/swappers.ts b/packages/swap-widget/src/constants/swappers.ts similarity index 100% rename from packages/swap-widget-poc/src/constants/swappers.ts rename to packages/swap-widget/src/constants/swappers.ts diff --git a/packages/swap-widget-poc/src/demo/App.css b/packages/swap-widget/src/demo/App.css similarity index 100% rename from packages/swap-widget-poc/src/demo/App.css rename to packages/swap-widget/src/demo/App.css diff --git a/packages/swap-widget-poc/src/demo/App.tsx b/packages/swap-widget/src/demo/App.tsx similarity index 100% rename from packages/swap-widget-poc/src/demo/App.tsx rename to packages/swap-widget/src/demo/App.tsx diff --git a/packages/swap-widget-poc/src/demo/main.tsx b/packages/swap-widget/src/demo/main.tsx similarity index 100% rename from packages/swap-widget-poc/src/demo/main.tsx rename to packages/swap-widget/src/demo/main.tsx diff --git a/packages/swap-widget-poc/src/hooks/useAssets.ts b/packages/swap-widget/src/hooks/useAssets.ts similarity index 100% rename from packages/swap-widget-poc/src/hooks/useAssets.ts rename to packages/swap-widget/src/hooks/useAssets.ts diff --git a/packages/swap-widget-poc/src/hooks/useBalances.ts b/packages/swap-widget/src/hooks/useBalances.ts similarity index 100% rename from packages/swap-widget-poc/src/hooks/useBalances.ts rename to packages/swap-widget/src/hooks/useBalances.ts diff --git a/packages/swap-widget-poc/src/hooks/useMarketData.ts b/packages/swap-widget/src/hooks/useMarketData.ts similarity index 100% rename from packages/swap-widget-poc/src/hooks/useMarketData.ts rename to packages/swap-widget/src/hooks/useMarketData.ts diff --git a/packages/swap-widget-poc/src/hooks/useSwapQuote.ts b/packages/swap-widget/src/hooks/useSwapQuote.ts similarity index 100% rename from packages/swap-widget-poc/src/hooks/useSwapQuote.ts rename to packages/swap-widget/src/hooks/useSwapQuote.ts diff --git a/packages/swap-widget-poc/src/hooks/useSwapRates.ts b/packages/swap-widget/src/hooks/useSwapRates.ts similarity index 100% rename from packages/swap-widget-poc/src/hooks/useSwapRates.ts rename to packages/swap-widget/src/hooks/useSwapRates.ts diff --git a/packages/swap-widget-poc/src/index.ts b/packages/swap-widget/src/index.ts similarity index 100% rename from packages/swap-widget-poc/src/index.ts rename to packages/swap-widget/src/index.ts diff --git a/packages/swap-widget-poc/src/types/index.ts b/packages/swap-widget/src/types/index.ts similarity index 100% rename from packages/swap-widget-poc/src/types/index.ts rename to packages/swap-widget/src/types/index.ts diff --git a/packages/swap-widget-poc/src/utils/addressValidation.ts b/packages/swap-widget/src/utils/addressValidation.ts similarity index 100% rename from packages/swap-widget-poc/src/utils/addressValidation.ts rename to packages/swap-widget/src/utils/addressValidation.ts diff --git a/packages/swap-widget-poc/src/utils/redirect.ts b/packages/swap-widget/src/utils/redirect.ts similarity index 100% rename from packages/swap-widget-poc/src/utils/redirect.ts rename to packages/swap-widget/src/utils/redirect.ts diff --git a/packages/swap-widget-poc/src/vite-env.d.ts b/packages/swap-widget/src/vite-env.d.ts similarity index 100% rename from packages/swap-widget-poc/src/vite-env.d.ts rename to packages/swap-widget/src/vite-env.d.ts diff --git a/packages/swap-widget-poc/tsconfig.json b/packages/swap-widget/tsconfig.json similarity index 100% rename from packages/swap-widget-poc/tsconfig.json rename to packages/swap-widget/tsconfig.json diff --git a/packages/swap-widget-poc/tsconfig.node.json b/packages/swap-widget/tsconfig.node.json similarity index 100% rename from packages/swap-widget-poc/tsconfig.node.json rename to packages/swap-widget/tsconfig.node.json diff --git a/packages/swap-widget-poc/vite.config.ts b/packages/swap-widget/vite.config.ts similarity index 100% rename from packages/swap-widget-poc/vite.config.ts rename to packages/swap-widget/vite.config.ts diff --git a/railway.json b/railway.json index 9f2b224a318..46cd2ac60bd 100644 --- a/railway.json +++ b/railway.json @@ -2,7 +2,7 @@ "$schema": "https://railway.app/railway.schema.json", "build": { "builder": "DOCKERFILE", - "dockerfilePath": "packages/swap-widget-poc/Dockerfile" + "dockerfilePath": "packages/swap-widget/Dockerfile" }, "deploy": { "restartPolicyType": "ON_FAILURE", diff --git a/yarn.lock b/yarn.lock index 9a719cb4ffb..f0b32513183 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12691,9 +12691,9 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/swap-widget-poc@workspace:packages/swap-widget-poc": +"@shapeshiftoss/swap-widget@workspace:packages/swap-widget": version: 0.0.0-use.local - resolution: "@shapeshiftoss/swap-widget-poc@workspace:packages/swap-widget-poc" + resolution: "@shapeshiftoss/swap-widget@workspace:packages/swap-widget" dependencies: "@rainbow-me/rainbowkit": ^2.2.3 "@shapeshiftoss/caip": ^8.16.5 @@ -12701,6 +12701,7 @@ __metadata: "@types/react": ^18.2.0 "@types/react-dom": ^18.2.0 "@vitejs/plugin-react": ^4.2.0 + p-queue: ^8.0.1 react: ^18.2.0 react-dom: ^18.2.0 react-virtuoso: ^4.18.1 From 0768896acea0f88af4f4c83641c6bfa15d38509a Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:07:03 +0100 Subject: [PATCH 19/41] fix: change page title --- packages/swap-widget/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swap-widget/index.html b/packages/swap-widget/index.html index aea8eaadc3a..59ee2cafcb1 100644 --- a/packages/swap-widget/index.html +++ b/packages/swap-widget/index.html @@ -3,7 +3,7 @@ - ShapeShift Swap Widget POC + ShapeShift Widget