wgm-swap-components
Version:
A React component library for token swap interfaces
292 lines (291 loc) • 18.8 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useState, useEffect } from 'react';
import { CHAIN_ID, DEFAULT_PROTOCOLS, DEFAULT_EXCHANGE_RATE, RELOAD_COUNTDOWN_MAX } from './constants';
import { TokenInput } from './TokenInput';
import { SwapButton } from './SwapButton';
import { TokenSelectModal } from './TokenSelectModal';
import { useWagmiIntegration } from './hooks/useWagmiIntegration';
import { useFetchQuote } from './hooks/useFetchQuote';
import { useTokens } from './hooks/useTokens';
import { formatUnits } from 'viem';
import { waitForTransactionReceipt } from '@wagmi/core';
import toast, { Toaster } from 'react-hot-toast';
/**
* Complete Swap Form Component
*/
export var SwapForm = function (_a) {
var config = _a.config;
var _b = useTokens(''), fetchedTokens = _b.tokens, isLoadingTokens = _b.isLoading;
var slippageTolerance = 0.5;
var _c = useState(null), fromToken = _c[0], setFromToken = _c[1];
var _d = useState(null), toToken = _d[0], setToToken = _d[1];
useEffect(function () {
if (fetchedTokens && fetchedTokens.length > 0) {
if (!fromToken && fetchedTokens[0]) {
setFromToken(fetchedTokens[0]);
}
if (!toToken && fetchedTokens[1]) {
setToToken(fetchedTokens[1]);
}
}
}, [fetchedTokens, fromToken, toToken]);
var _e = useState(''), fromAmount = _e[0], setFromAmount = _e[1];
var _f = useState(''), toAmount = _f[0], setToAmount = _f[1];
var _g = useState(false), isLoading = _g[0], setIsLoading = _g[1];
var _h = useState(null), exchangeRate = _h[0], setExchangeRate = _h[1];
var _j = useState(false), isModalOpen = _j[0], setIsModalOpen = _j[1];
var _k = useState('from'), modalType = _k[0], setModalType = _k[1];
var _l = useState(DEFAULT_PROTOCOLS), protocols = _l[0], setProtocols = _l[1];
var _m = useState(false), priceImpactWarning = _m[0], setPriceImpactWarning = _m[1];
var _o = useState(RELOAD_COUNTDOWN_MAX), reloadCountdown = _o[0], setReloadCountdown = _o[1];
// Wagmi integration for balance and swap functionality
var _p = useWagmiIntegration({ token: fromToken, config: config }), fromTokenBalance = _p.balance, isWagmiLoading = _p.isLoading, isConnected = _p.isConnected, address = _p.address, chainId = _p.chainId, isWrongNetwork = _p.isWrongNetwork, switchToCorrectNetwork = _p.switchToCorrectNetwork, executeSwap = _p.executeSwap, wrapIP = _p.wrapIP, unwrapWIP = _p.unwrapWIP, checkAllowance = _p.checkAllowance, approveToken = _p.approveToken;
// Fetch quote for regular swaps (not wrap/unwrap)
var isWrapOrUnwrap = ((fromToken === null || fromToken === void 0 ? void 0 : fromToken.symbol) === 'IP' && (toToken === null || toToken === void 0 ? void 0 : toToken.symbol) === 'WIP') || ((fromToken === null || fromToken === void 0 ? void 0 : fromToken.symbol) === 'WIP' && (toToken === null || toToken === void 0 ? void 0 : toToken.symbol) === 'IP');
var _q = useFetchQuote({
tokenIn: fromToken,
tokenOut: toToken,
amountIn: fromAmount,
chainId: chainId || CHAIN_ID,
protocols: protocols.join(','),
enabled: !isWrapOrUnwrap,
}), quoteData = _q.data, isQuoteLoading = _q.isLoading, quoteError = _q.error, refetchQuote = _q.refetch;
var getTokenExchangeRate = function (amountIn, amountOut) {
try {
if (!amountIn || !amountOut || parseFloat(amountIn) <= 0 || parseFloat(amountOut) <= 0) {
return null;
}
return parseFloat(amountOut) / parseFloat(amountIn);
}
catch (_a) {
return null;
}
};
// Handle quote data and calculate amounts
useEffect(function () {
if (!fromToken || !toToken || !fromAmount || parseFloat(fromAmount) <= 0) {
setToAmount('');
setExchangeRate(null);
setPriceImpactWarning(false);
return;
}
// Handle wrap/unwrap operations (1:1 ratio)
if (fromToken.symbol === 'IP' && toToken.symbol === 'WIP') {
setToAmount(fromAmount);
setExchangeRate(DEFAULT_EXCHANGE_RATE);
setPriceImpactWarning(false);
return;
}
if (fromToken.symbol === 'WIP' && toToken.symbol === 'IP') {
setToAmount(fromAmount);
setExchangeRate(DEFAULT_EXCHANGE_RATE);
setPriceImpactWarning(false);
return;
}
// Handle regular swap with quote data
if ((quoteData === null || quoteData === void 0 ? void 0 : quoteData.quote) && (toToken === null || toToken === void 0 ? void 0 : toToken.decimals)) {
var quote = quoteData.quote;
// Calculate total amountOut from all routes
var totalAmountOut = quote.route.reduce(function (total, path) {
var lastStep = path[path.length - 1];
if (lastStep) {
var amount = parseFloat(formatUnits(BigInt(lastStep.amountOut || '0'), toToken.decimals || 18));
return total + amount;
}
return total;
}, 0);
var formattedTotalAmountOut = totalAmountOut.toFixed(4);
setToAmount(formattedTotalAmountOut);
var rate = getTokenExchangeRate(fromAmount, formattedTotalAmountOut);
setExchangeRate(rate);
// Check price impact warning
var priceImpact = parseFloat(quote.priceImpact || '0');
setPriceImpactWarning(priceImpact > 5); // 5% threshold
}
else {
// Clear amounts if no quote available
setToAmount('');
setExchangeRate(null);
}
}, [fromToken, toToken, fromAmount, quoteData, isQuoteLoading, quoteError]);
// Auto-reload quote
useEffect(function () {
if (!fromToken || !toToken || !fromAmount || parseFloat(fromAmount) <= 0) {
return;
}
var interval = setInterval(function () {
setReloadCountdown(function (prev) {
if (prev === 1) {
refetchQuote();
return RELOAD_COUNTDOWN_MAX;
}
return prev - 1;
});
}, 1000);
return function () { return clearInterval(interval); };
}, [fromToken, toToken, fromAmount, refetchQuote]);
var handleReloadClick = function () {
refetchQuote();
setReloadCountdown(RELOAD_COUNTDOWN_MAX);
};
var handleMaxClick = function () {
// Use wagmi balance if available, otherwise fallback to token balance
var availableBalance = fromTokenBalance || (fromToken === null || fromToken === void 0 ? void 0 : fromToken.balance) || '0';
setFromAmount(availableBalance);
};
var handleTokenSelect = function (tokenType) {
setModalType(tokenType);
setIsModalOpen(true);
};
var handleModalTokenSelect = function (token) {
if (modalType === 'from') {
if ((toToken === null || toToken === void 0 ? void 0 : toToken.address) === token.address) {
setToToken(fromToken); // Swap them
}
setFromToken(token);
}
else { // modalType is 'to'
if ((fromToken === null || fromToken === void 0 ? void 0 : fromToken.address) === token.address) {
setFromToken(toToken); // Swap them
}
setToToken(token);
}
// Reset amounts after any selection
setFromAmount('');
setToAmount('');
setIsModalOpen(false);
};
var handleCloseModal = function () {
setIsModalOpen(false);
};
var handleSwap = function () { return __awaiter(void 0, void 0, void 0, function () {
var switchNetwork, userBalance, swapAmount, hash, receipt, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!fromToken || !toToken || !fromAmount || parseFloat(fromAmount) <= 0) {
return [2 /*return*/];
}
// Check if wallet is connected
if (!isConnected || !address) {
alert('Please connect your wallet first');
return [2 /*return*/];
}
if (!isWrongNetwork) return [3 /*break*/, 2];
switchNetwork = window.confirm('You are on the wrong network. Switch to the correct network?');
if (!switchNetwork) return [3 /*break*/, 2];
return [4 /*yield*/, switchToCorrectNetwork()];
case 1:
_a.sent();
return [2 /*return*/];
case 2:
userBalance = parseFloat(fromTokenBalance || '0');
swapAmount = parseFloat(fromAmount);
if (swapAmount > userBalance) {
alert("Insufficient balance. You have ".concat(userBalance, " ").concat(fromToken.symbol));
return [2 /*return*/];
}
setIsLoading(true);
_a.label = 3;
case 3:
_a.trys.push([3, 13, 14, 15]);
hash = null;
if (!(fromToken.symbol === 'IP' && toToken.symbol === 'WIP')) return [3 /*break*/, 5];
return [4 /*yield*/, wrapIP(fromAmount)];
case 4:
hash = _a.sent();
return [3 /*break*/, 9];
case 5:
if (!(fromToken.symbol === 'WIP' && toToken.symbol === 'IP')) return [3 /*break*/, 7];
return [4 /*yield*/, unwrapWIP(fromAmount)];
case 6:
hash = _a.sent();
return [3 /*break*/, 9];
case 7: return [4 /*yield*/, executeSwap(fromToken, toToken, fromAmount, quoteData === null || quoteData === void 0 ? void 0 : quoteData.quote)];
case 8:
// Regular swap with quote data
hash = _a.sent();
_a.label = 9;
case 9:
if (!(hash && config)) return [3 /*break*/, 11];
return [4 /*yield*/, waitForTransactionReceipt(config, { hash: hash })];
case 10:
receipt = _a.sent();
if (receipt.status === 'success') {
toast.success('Swap successful');
setFromAmount('');
setToAmount('');
}
else {
toast.error('Swap failed');
}
return [3 /*break*/, 12];
case 11:
toast.error('Swap failed. Please try again.');
_a.label = 12;
case 12: return [3 /*break*/, 15];
case 13:
error_1 = _a.sent();
console.error('Swap failed:', error_1);
toast.error('Swap failed. Please check the console for details.');
return [3 /*break*/, 15];
case 14:
setIsLoading(false);
return [7 /*endfinally*/];
case 15: return [2 /*return*/];
}
});
}); };
var canSwap = fromToken && toToken && fromAmount && parseFloat(fromAmount) > 0 && !isLoading && isConnected && (isWrapOrUnwrap || !!(quoteData === null || quoteData === void 0 ? void 0 : quoteData.quote));
var formClasses = "bg-white rounded-2xl shadow-xl border border-gray-200 p-6 max-w-md mx-auto".trim();
return (_jsxs(_Fragment, { children: [_jsx(Toaster, {}), _jsxs("div", { className: formClasses, children: [_jsx("div", { className: "flex justify-end mb-4", children: _jsx("a", { href: "https://ag.mimboku.com/?tab=SWAP", target: "_blank", rel: "noopener noreferrer", className: "flex gap-2 items-center text-blue-600 hover:underline font-medium", children: "Mimboku" }) }), _jsx("div", { className: "", children: _jsx(TokenInput, { label: "From", backgroundColor: "#1F1F23", textColor: "white", symbol: (fromToken === null || fromToken === void 0 ? void 0 : fromToken.symbol) || 'Select Token', name: (fromToken === null || fromToken === void 0 ? void 0 : fromToken.name) || 'Choose a token', logoUrl: fromToken === null || fromToken === void 0 ? void 0 : fromToken.logoUrl, value: fromAmount, balance: fromTokenBalance || (fromToken === null || fromToken === void 0 ? void 0 : fromToken.balance), onChange: setFromAmount, onMaxClick: handleMaxClick, onTokenSelect: function () { return handleTokenSelect('from'); } }) }), _jsx("div", { className: "mb-6", children: _jsx(TokenInput, { label: "To", backgroundColor: "#D4D4D4", textColor: "black", symbol: (toToken === null || toToken === void 0 ? void 0 : toToken.symbol) || 'Select Token', name: (toToken === null || toToken === void 0 ? void 0 : toToken.name) || 'Choose a token', logoUrl: toToken === null || toToken === void 0 ? void 0 : toToken.logoUrl, value: toAmount, onChange: setToAmount, onTokenSelect: function () { return handleTokenSelect('to'); }, disabled: true }) }), exchangeRate && fromToken && toToken && (_jsxs("div", { className: "mb-4 p-3 bg-gray-50 rounded-lg", children: [_jsxs("div", { className: "flex justify-between items-center text-sm", children: [_jsxs("div", { className: "flex items-center", children: [_jsx("span", { className: "text-gray-600 mr-2", children: "Rate" }), _jsxs("span", { className: "font-medium", children: ["1 ", fromToken.symbol, " = ", exchangeRate.toFixed(6), " ", toToken.symbol] })] }), _jsxs("div", { className: "flex items-center", children: [_jsx("button", { onClick: handleReloadClick, className: "mr-2 p-1 hover:bg-gray-200 rounded-full", children: _jsx("svg", { className: "w-4 h-4 text-gray-600 ".concat(isQuoteLoading ? 'animate-spin' : ''), fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", d: "M4 4v5h5M20 20v-5h-5M4 4l16 16" }) }) }), _jsxs("span", { className: "text-xs text-gray-500", children: [reloadCountdown, "s"] })] })] }), _jsxs("div", { className: "flex justify-between items-center text-sm mt-1", children: [_jsx("span", { className: "text-gray-600", children: "Slippage Tolerance" }), _jsxs("span", { className: "font-medium", children: [slippageTolerance, "%"] })] }), priceImpactWarning && (_jsx("div", { className: "text-red-500 text-xs mt-1", children: "High price impact warning!" }))] })), _jsx(SwapButton, { variant: "primary", size: "medium", fullWidth: true, disabled: !canSwap, loading: isLoading || isWagmiLoading || isQuoteLoading, onClick: handleSwap, children: !isConnected
? 'Connect Wallet'
: isWrongNetwork
? 'Wrong Network'
: isQuoteLoading
? 'Fetching Quote...'
: isLoading || isWagmiLoading
? 'Processing...'
: (fromToken === null || fromToken === void 0 ? void 0 : fromToken.symbol) === 'IP' && (toToken === null || toToken === void 0 ? void 0 : toToken.symbol) === 'WIP'
? 'Wrap IP'
: (fromToken === null || fromToken === void 0 ? void 0 : fromToken.symbol) === 'WIP' && (toToken === null || toToken === void 0 ? void 0 : toToken.symbol) === 'IP'
? 'Unwrap WIP'
: 'Swap Tokens' }), _jsx(TokenSelectModal, { isOpen: isModalOpen, onClose: handleCloseModal, onSelectToken: handleModalTokenSelect, selectedToken: modalType === 'from' ? fromToken : toToken, excludeToken: modalType === 'from' ? toToken : fromToken })] })] }));
};
export default SwapForm;