@thespidercode/openbook-swap
Version:
Ready-to-use swap tool using Openbook DEX
364 lines (363 loc) • 22.7 kB
JavaScript
"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;