UNPKG

@thespidercode/openbook-swap

Version:
364 lines (363 loc) 22.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SwapContainer = void 0; const react_1 = require("react"); const swap_1 = require("./swap"); const account_1 = require("./account"); const market_1 = require("./market"); const transaction_1 = require("./transaction"); const web3_js_1 = require("@solana/web3.js"); const lodash_debounce_1 = __importDefault(require("lodash.debounce")); const react_2 = __importDefault(require("react")); const token_1 = require("./token"); const token_input_1 = require("./token-input"); const token_quote_1 = require("./token-quote"); const icon_swap_svg_1 = __importDefault(require("@thespidercode/openbook-swap/src/images/icons/icon-swap.svg")); require("@thespidercode/openbook-swap/src/css/openbookswap.css"); const buffer = __importStar(require("buffer")); window.Buffer = buffer.Buffer; function SwapContainer(props) { const { title, markets, connection, wallet, onSwapLoading, onSwapError, onSwapSuccess, onSwap, colors, manualTransaction } = props; const style = { "--primary-color": colors?.primary || "grey", "--secondary-color": colors?.secondary || "#3a3a3a", "--background-color": colors?.background || "rgb(27, 23, 23)", "--text-color": colors?.text || "#fff", "--button-color": colors?.swapButton || "grey" }; const [swap, setSwap] = (0, react_1.useState)({ sell: false, market: markets[0], amounts: { base: 0, quote: 0 }, inputAmounts: { base: "", quote: "" }, slotConsumed: 0 }); const [userTokens, setUserTokens] = (0, react_1.useState)(); const [marketOrders, setMarketOrders] = (0, react_1.useState)(market_1.marketOrdersInit); const [loadingSwap, setLoadingSwap] = (0, react_1.useState)(false); const [alertMessages, setAlertMessages] = (0, react_1.useState)([]); const [priceAccuracy, setPriceAccuracy] = (0, react_1.useState)(0); const [counter, setCounter] = (0, react_1.useState)(0); const [prices, setPrices] = (0, react_1.useState)({}); let isOnChainData = false; let timeoutId = null; const processTransactionConfirmation = async (transactionConfirmation, orderSignature) => { if (transactionConfirmation.error) { onSwapError({ message: transactionConfirmation.message ?? "", signature: orderSignature }); } if (!transactionConfirmation.balances) { onSwapSuccess({ message: "Swap successfull", signature: orderSignature, differences: { base: transactionConfirmation.balances ? transactionConfirmation.balances[swap.market.base.mint.toString()] : null, quote: transactionConfirmation.balances ? transactionConfirmation.balances[swap.market.quote.mint.toString()] : null } }); } else if (transactionConfirmation.balances[swap.market.base.mint.toString()] == 0 && transactionConfirmation.balances[swap.market.quote.mint.toString()] == 0) { onSwapError({ message: "Exceeded Slippage Boundary", signature: orderSignature, }); } else { onSwapSuccess({ message: "Swap successfull", signature: orderSignature, differences: { base: transactionConfirmation.balances ? transactionConfirmation.balances[swap.market.base.mint.toString()] : null, quote: transactionConfirmation.balances ? transactionConfirmation.balances[swap.market.quote.mint.toString()] : null } }); } }; const processOrder = async () => { setLoadingSwap(true); try { if (!(wallet && wallet.publicKey) && !manualTransaction) { onSwapError({ message: "Cannot get wallet address" }); return; } if (!marketOrders) { onSwapError({ message: "Cannot get market information" }); return; } const swapResult = await (0, swap_1.newSwap)((wallet && wallet?.publicKey ? wallet.publicKey : new web3_js_1.PublicKey(manualTransaction?.toString())), swap, marketOrders.lowestAsk, marketOrders.highestBid, connection); if (swapResult.error || !swapResult.transaction) { onSwapError({ message: "Cannot get wallet and/or market information", }); setLoadingSwap(false); return; } if (manualTransaction && onSwap) { swapResult.market = swap.market; onSwap({ swapResult, setLoadingSwap, refreshUserBalances }); return; } if (!wallet || !wallet.publicKey) { onSwapError({ message: "Cannot get wallet", }); setLoadingSwap(false); return; } const orderSignature = await wallet.sendTransaction(swapResult.transaction.transaction, connection, { signers: swapResult.transaction.signers }); onSwapLoading({ message: "Waiting for the transaction to confirm...", }); const latestBlockHash = await connection.getLatestBlockhash(); const transactionConfirmation = await (0, transaction_1.confirmTransaction)(connection, orderSignature, wallet.publicKey, swap.market); await processTransactionConfirmation(transactionConfirmation, orderSignature); await connection.confirmTransaction({ signature: orderSignature, blockhash: latestBlockHash.blockhash, lastValidBlockHeight: latestBlockHash.lastValidBlockHeight }, 'finalized'); refreshUserBalances(wallet.publicKey); } catch (error) { if (error && (error.toString().includes('0x1') || error.toString().includes('0x22'))) { onSwapError({ message: "Insufficient funds.", }); } else { onSwapError({ message: error?.toString()?.replace('WalletSendTransactionError: ', ''), }); } } setLoadingSwap(false); }; const refreshQuotes = async () => { try { if (!marketOrders?.bids || !marketOrders?.asks) return; const computedQuotes = await (0, market_1.computeQuotes)(marketOrders, swap); if (!computedQuotes) return; const inputAmount = swap.sell ? +computedQuotes.inputAmounts.base : +computedQuotes.inputAmounts.quote; const quoteAmount = swap.sell ? computedQuotes.amounts.quote : computedQuotes.amounts.base; if (inputAmount && quoteAmount) { const valueInput = inputAmount * (swap.sell ? prices[swap.market.base.name] : prices[swap.market.quote.name]); const valueQuote = quoteAmount * (swap.sell ? prices[swap.market.quote.name] : prices[swap.market.base.name]); setPriceAccuracy(((valueInput / valueQuote) * 100) - 100); } setSwap(computedQuotes); } catch (error) { console.log(error); } }; const switchPair = () => { setSwap({ ...swap, sell: !swap.sell, inputAmounts: { ...swap.inputAmounts, base: (swap.sell ? swap.inputAmounts.base : (swap.amounts.base > 0 ? swap.amounts.base.toFixed(2) : '')), quote: (swap.sell ? (swap.amounts.quote > 0 ? swap.amounts.quote.toFixed(2) : '') : swap.inputAmounts.quote) } }); }; const refreshUserBalances = async (owner) => { try { const walletTokens = await (0, account_1.getWalletToken)(owner, swap.market, connection); setUserTokens(walletTokens); } catch (error) { setUserTokens(undefined); } }; const refreshMarketOrders = async (onchain = false, marketAddress = swap.market.address) => { try { let marketOrdersResponse; if (onchain && !isOnChainData) return; const marketQuery = await (0, market_1.getMarketData)(marketAddress, connection, onchain); if (!marketQuery) return; marketOrdersResponse = marketQuery.marketOrdersResponse; const tokens = markets .map((market) => [market.base.name, market.quote.name]) .flat() .filter((value, index, self) => { // Only keep the first occurrence of each value if (self.indexOf(value) !== index) { return false; } // Ensure that the value is not undefined, null, or an empty string return value != null && value.trim() !== ''; }); const tokenPrices = await (0, token_1.geTokenPrices)(tokens); var obj = {}; for (var token of tokens) { obj[token] = tokenPrices.data.tokenPrices[token] ?? 0; } setPrices(obj); isOnChainData = marketQuery.isOnChainData; const priceAlertMessage = 'Openbook is experiencing a price delay that can impact swaps'; if (Date.parse(marketOrdersResponse?.market['updatedAt']) + 3660000 < Date.now()) { if (!alertMessages.includes(priceAlertMessage)) { const newAlerts = [...alertMessages, priceAlertMessage]; setAlertMessages(newAlerts); } } else { const newAlerts = [...alertMessages.filter(((am) => am != priceAlertMessage))]; setAlertMessages(newAlerts); } (0, market_1.updateMarketOrders)(marketOrders, marketOrdersResponse, setMarketOrders, swap.market.base.name, prices[swap.market.base.name]); setCounter(counter + 1); } catch (error) { console.log(error); setCounter(counter + 1); } }; const debounceRefresh = (0, react_1.useMemo)(() => { if (isOnChainData) { return (0, lodash_debounce_1.default)(() => refreshMarketOrders(true), 50); } }, [(swap.sell ? swap.inputAmounts.base : swap.inputAmounts.quote), swap.sell]); (0, react_1.useEffect)(() => { clearTimeout(timeoutId ?? 0); timeoutId = window.setTimeout(() => debounceRefresh ? debounceRefresh() : null, 500); }, [(swap.sell ? swap.inputAmounts.base : swap.inputAmounts.quote), swap.sell]); (0, react_1.useEffect)(() => { refreshQuotes(); }, [(swap.sell ? swap.inputAmounts.base : swap.inputAmounts.quote), swap.sell, marketOrders]); (0, react_1.useEffect)(() => { if (manualTransaction && swap.market.address) { refreshUserBalances(manualTransaction); } else if (!wallet?.publicKey && swap.market.address) { setUserTokens(undefined); } }, [manualTransaction, swap.market.address]); (0, react_1.useEffect)(() => { if (wallet?.publicKey && swap.market.address) { refreshUserBalances(wallet.publicKey); } else if (!manualTransaction && swap.market.address) { setUserTokens(undefined); } }, [wallet?.publicKey, swap.market.address]); (0, react_1.useEffect)(() => { setTimeout(() => { refreshMarketOrders(); }, 2000); if (counter % 15 == 0) { refreshMarketOrders(true); } }, [counter]); return (react_2.default.createElement("div", { className: "openbookswap swap-container", style: style }, react_2.default.createElement("div", { className: `swap-container-box flex column between items-center` }, alertMessages.length > 0 ? (react_2.default.createElement("div", { className: "alert-container flex column" }, alertMessages.map((am) => react_2.default.createElement("span", null, am)))) : '', react_2.default.createElement("div", { className: "swap-container-content flex column items-center" }, react_2.default.createElement("div", { className: "flex w-100 center" }, react_2.default.createElement("h1", { className: "swap-title" }, title)), react_2.default.createElement("div", { className: "swap-container-content-pair flex column items-center" }, react_2.default.createElement("div", { className: "flex column mt-2 w-100" }, react_2.default.createElement("div", { className: "flex items-center between" }, react_2.default.createElement("h4", null, "From:"), (swap.sell ? userTokens?.amountBaseToken : userTokens?.amountQuoteToken) || (swap.sell ? userTokens?.amountBaseToken : userTokens?.amountQuoteToken) == 0 ? react_2.default.createElement("div", { className: "balance flex items-center" }, react_2.default.createElement("span", null, "Balance:"), " ", react_2.default.createElement("span", null, swap.sell ? (userTokens?.amountBaseToken && userTokens?.amountBaseToken > 10000 ? (userTokens?.amountBaseToken / 1000).toLocaleString() + 'K' : userTokens?.amountBaseToken.toLocaleString()) : (userTokens?.amountQuoteToken && userTokens?.amountQuoteToken > 10000 ? (userTokens?.amountQuoteToken / 1000).toLocaleString() + 'K' : userTokens?.amountQuoteToken.toLocaleString()))) : null), react_2.default.createElement(token_input_1.TokenInput, { markets: markets, pair: swap.market, amount: swap.sell ? swap.inputAmounts.base : swap.inputAmounts.quote, onValueChange: (value) => { const amount = value && value != '.' ? value : ""; setSwap({ ...swap, inputAmounts: { ...swap.inputAmounts, quote: swap.sell ? swap.inputAmounts.quote : amount, base: swap.sell ? amount : swap.inputAmounts.base } }); }, balance: swap.sell ? userTokens?.amountBaseToken : userTokens?.amountQuoteToken, setPair: (pair, newToken) => { if (swap.market.address != pair.address) { setMarketOrders(market_1.marketOrdersInit); } setSwap({ ...swap, market: pair, sell: pair.base.name == newToken ? true : false }); refreshMarketOrders(isOnChainData, pair.address); }, token: swap.sell ? { ...swap.market.base, price: prices[swap.market.base.name] } : { ...swap.market.quote, price: prices[swap.market.quote.name] } })), react_2.default.createElement("div", { className: "swap-switch mt-5 w-100" }, react_2.default.createElement("button", { className: "flex center items-center p-1", onClick: switchPair }, react_2.default.createElement("img", { src: icon_swap_svg_1.default, alt: 'logo' }))), react_2.default.createElement("div", { className: "flex column mt-3 w-100" }, react_2.default.createElement("div", { className: "flex items-center between" }, react_2.default.createElement("h4", null, "To:"), (!swap.sell ? userTokens?.amountBaseToken : userTokens?.amountQuoteToken) || (!swap.sell ? userTokens?.amountBaseToken : userTokens?.amountQuoteToken) == 0 ? react_2.default.createElement("div", { className: "balance flex items-center" }, react_2.default.createElement("span", null, "Balance:"), react_2.default.createElement("span", null, !swap.sell ? (userTokens?.amountBaseToken && userTokens?.amountBaseToken > 10000 ? (Number(userTokens?.amountBaseToken / 1000)).toLocaleString() + 'K' : userTokens?.amountBaseToken.toLocaleString()) : (userTokens?.amountQuoteToken && userTokens?.amountQuoteToken > 10000 ? (Number(userTokens?.amountQuoteToken / 1000)).toLocaleString() + 'K' : userTokens?.amountQuoteToken.toLocaleString()))) : null), react_2.default.createElement(token_quote_1.TokenQuote, { markets: markets, amountNumber: swap.sell ? swap.amounts.quote : swap.amounts.base, token: swap.sell ? { ...swap.market.quote, price: prices[swap.market.quote.name] } : { ...swap.market.base, price: prices[swap.market.base.name] }, setPair: (pair) => { if (swap.market.address != pair.address) { setMarketOrders(market_1.marketOrdersInit); } setSwap({ ...swap, market: pair }); refreshMarketOrders(isOnChainData, pair.address); }, pair: swap.market }))), react_2.default.createElement("div", { className: `price-analysis-container ${priceAccuracy == 0 ? 'no-display' : ''}` }, react_2.default.createElement("div", { className: 'flex start' }, react_2.default.createElement("div", { className: 'flex column mt-3 ' }, react_2.default.createElement("div", { className: `flex column ${priceAccuracy > 2 ? 'bad-price' : (priceAccuracy > 1 ? 'fair-price' : (priceAccuracy > 0 ? 'good-price' : 'great-price'))}` }, react_2.default.createElement("div", { className: 'flex gap-smaller items-center' }, react_2.default.createElement("span", null, priceAccuracy > 2 ? 'Bad' : (priceAccuracy > 1 ? 'Fair' : (priceAccuracy > 0 ? 'Good' : 'Great')), " Price: "), react_2.default.createElement("span", { className: '' }, priceAccuracy < 2 && priceAccuracy > 0 ? 'within' : '', " ", priceAccuracy > 0 ? priceAccuracy.toFixed(2) : (-1 * priceAccuracy).toFixed(2), "% ", priceAccuracy < 0 ? 'cheaper' : (priceAccuracy > 2 ? 'more expensive' : ''))), react_2.default.createElement("span", { className: 'small-text-detail' }, "(", swap.slotConsumed, " slot", swap.slotConsumed > 1 ? 's' : '', " consumed)")))))), react_2.default.createElement("div", { className: "mt-3 w-100" }, (wallet?.connected && wallet.publicKey) || (manualTransaction) ? react_2.default.createElement("button", { disabled: loadingSwap || swap.slotConsumed > 1 || (swap.sell && (userTokens?.amountBaseToken ?? 0) < (parseFloat(swap.inputAmounts.base) ?? 0)) || (!swap.sell && (userTokens?.amountQuoteToken ?? 0) < (parseFloat(swap.inputAmounts.quote) ?? 0)) || ((!swap.sell && swap.amounts.base < swap.market.minBase) || (swap.sell && parseFloat(swap.inputAmounts.base) < swap.market.minBase)) || ((swap.sell && parseFloat(swap.inputAmounts.base ? swap.inputAmounts.base : '0') == 0 || !swap.sell && parseFloat(swap.inputAmounts.quote ? swap.inputAmounts.quote : '0') == 0)), onClick: () => processOrder(), className: "wallet-adapter-button wallet-adapter-button-trigger" }, react_2.default.createElement("div", { className: "flex column" }, ((!swap.sell && swap.amounts.base < swap.market.minBase) || (swap.sell && parseFloat(swap.inputAmounts.base) < swap.market.minBase)) ? (`Minimum ${swap.market.minBase} ${swap.market.base.name}`) : (swap.slotConsumed > 1 || priceAccuracy > 2 ? 'Not enough liquidity ⚠️' : swap.sell ? ((parseFloat(swap.inputAmounts.base) > 0) ? ((userTokens?.amountBaseToken ?? 0) >= (parseFloat(swap.inputAmounts.base) ?? 0) ? 'Swap ' : 'Insufficent Balance') : 'Enter a Value') : ((parseFloat(swap.inputAmounts.quote) > 0) ? ((userTokens?.amountQuoteToken ?? 0) >= (parseFloat(swap.inputAmounts.quote) ?? 0) ? 'Swap' : 'Insufficent Balance') : 'Enter a Value')))) : react_2.default.createElement("button", { disabled: true, className: "wallet-adapter-button wallet-adapter-button-trigger" }, "Connect Wallet"))), react_2.default.createElement("a", { className: "powered-by-openbookswap", href: 'https://github.com/TheSpiderInc/openbook-swap', target: "_blank" }, "Powered by OpenBookSwap"))); } exports.SwapContainer = SwapContainer;