UNPKG

@thespidercode/openbook-swap

Version:
221 lines (220 loc) 11.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Side = exports.newSwap = exports.getSwapTransaction = exports.getCloseOpenOrdersInstruction = void 0; const web3_js_1 = require("@solana/web3.js"); const dex_constant_1 = require("./constants/dex.constant"); const market_1 = require("./market"); const account_1 = require("./account"); const spl_token_1 = require("@solana/spl-token"); const bn_js_1 = __importDefault(require("bn.js")); const instructions_1 = require("./serum/instructions"); const market_2 = require("./serum/market"); const getCloseOpenOrdersInstruction = (openOrders, market, owner) => { // TODO: SHOULD WE LET THE USER CHOOSE THE PROGRAM ADDRESS? const programAddress = new web3_js_1.PublicKey(dex_constant_1.DEX_ADDRESS); const keys = [ { pubkey: openOrders, isSigner: false, isWritable: true }, { pubkey: owner, isSigner: true, isWritable: false }, { pubkey: owner, isSigner: false, isWritable: true }, { pubkey: market, isSigner: false, isWritable: false }, ]; return new web3_js_1.TransactionInstruction({ keys, programId: programAddress, data: (0, instructions_1.encodeInstruction)({ closeOpenOrders: {}, }), }); }; exports.getCloseOpenOrdersInstruction = getCloseOpenOrdersInstruction; const getSwapTransaction = async (owner, side, limit, size, marketDetails, connection, onchain) => { try { const transaction = new web3_js_1.Transaction(); const programAddress = new web3_js_1.PublicKey(dex_constant_1.DEX_ADDRESS); let marketInfo = null; const market = await market_2.Market.load(connection, marketDetails.address, {}, programAddress); // USING ONCHAIN DATA if (onchain) { marketInfo = (await (0, market_1.getMarketOrdersOnChain)(market.address, connection, market))?.market ?? null; if (!marketInfo?.lowestAsk || !marketInfo.highestBid) { throw ('Cannot get market information - please check your RPC and market address'); } } // USING API // TODO: FINISH THIS THING else { let getMarketOrdersResponse = await (0, market_1.getMarketOrders)(marketDetails.address); if (!getMarketOrdersResponse?.market) throw ('Cannot get market information from API'); marketInfo = getMarketOrdersResponse.market; } if (marketInfo === null) { throw ('Cannot get market information'); } // ALSO CHECK IF WALLET HAS ENOUGH FUNDS // SEE IF THIS 1.01 SLIPPAGE SHOULD BE VARIABLE let accountDetails = await (0, account_1.getAccountDetail)(marketDetails, market, transaction, owner, connection, side == Side.Buy ? limit * size * 1.01 * web3_js_1.LAMPORTS_PER_SOL : 0); if (!accountDetails || !accountDetails.baseTokenAccount || !accountDetails.quoteTokenAccount) { return 'Cannot get account information'; } // CHECK ALSO IF QUANTITY IS ENOUGH (FOR SOL SPECIFICALLY) BUT SHOULD BE DONE BEFORE IN THE UI // ADDING AN EXTRA 0.5% SO THE PHANTOM ORDER AS A LITTLE ROOM TO MOVE const margin = marketDetails.swapMargin + 0.005; if ((side == Side.Buy && (limit * 1.05) < marketInfo.lowestAsk) || (side == Side.Sell && (limit * 0.95) > marketInfo.highestBid)) { throw (`Oops quote changed, please refresh (old quote ${limit.toFixed(10)} and new quote ${side == Side.Buy ? marketInfo.lowestAsk.toFixed(10) : marketInfo.highestBid.toFixed(10)})`); } else { // MAKE THE CALCULATION MORE PRECISE const minimumReceive = +(side == Side.Buy ? +marketInfo.lowestAsk.toFixed(10) * (1 + margin) : +marketInfo.highestBid.toFixed(10) * (1 - margin)) * size; console.log('Going to process the order at rate', side == Side.Buy ? marketInfo.lowestAsk.toFixed(10) : marketInfo.highestBid.toFixed(10), '. Should pay/receive max/min', minimumReceive.toFixed(10), 'qty', size); } const orderTransaction = await getOrderTransaction(market, side, side == Side.Buy ? marketInfo.lowestAsk * (1 + margin) : marketInfo.highestBid * (1 - margin), size, accountDetails, owner, margin, connection); if (!orderTransaction) { throw ('Cannot create order instruction'); } transaction.add(orderTransaction.transaction); const settleIx = getSettleInstruction(market, marketDetails, accountDetails, owner); if (!settleIx) { throw ('Cannot create settle instruction'); } transaction.add(settleIx); if (marketDetails.base.mint.toString() == spl_token_1.NATIVE_MINT.toString()) { const closeAccountInstruction = (0, spl_token_1.createCloseAccountInstruction)(accountDetails.baseTokenAccount, owner, owner); transaction.add(closeAccountInstruction); } else if (marketDetails.quote.mint.toString() == spl_token_1.NATIVE_MINT.toString()) { const closeAccountInstruction = (0, spl_token_1.createCloseAccountInstruction)(accountDetails.quoteTokenAccount, owner, owner); transaction.add(closeAccountInstruction); } return { signers: (accountDetails.signers ? accountDetails.signers : []).concat(accountDetails.openOrders && accountDetails.openOrders.hasOwnProperty('_keypair') ? [accountDetails.openOrders] : []), transaction: transaction, isNewOpenOrders: (accountDetails.openOrders && accountDetails.openOrders.hasOwnProperty('_keypair')) ?? false }; } catch (error) { console.log(error); return error.toString(); } }; exports.getSwapTransaction = getSwapTransaction; const getOrderTransaction = async (market, side, price, size, accountDetails, owner, swapMargin, connection) => { try { if (!accountDetails.quoteTokenAccount || !accountDetails.baseTokenAccount) { return null; } const programAddress = new web3_js_1.PublicKey(dex_constant_1.DEX_ADDRESS); // TODO: WHY NOT USING THIS ONE: makeMatchOrdersTransaction ? Maybe the response to the partial fill // CHECK DIFFERENCE WITH THIS ONE: makeNewOrderV3Instruction if (accountDetails.openOrders?.hasOwnProperty('_keypair')) { return await market.makePlaceOrderTransaction(connection, { owner: owner, payer: side == Side.Buy ? accountDetails.quoteTokenAccount : accountDetails.baseTokenAccount, price: price * (side == Side.Buy ? (1 + swapMargin) : (1 - swapMargin)), side, size, orderType: 'ioc', selfTradeBehavior: "decrementTake", openOrdersAccount: new web3_js_1.Account(accountDetails.openOrders.secretKey), openOrdersAddressKey: accountDetails.openOrders.publicKey, programId: programAddress }); } else { return await market.makePlaceOrderTransaction(connection, { owner: owner, payer: side == Side.Buy ? accountDetails.quoteTokenAccount : accountDetails.baseTokenAccount, price: price * (side == Side.Buy ? (1 + swapMargin) : (1 - swapMargin)), side, size, orderType: 'ioc', selfTradeBehavior: "decrementTake", programId: programAddress }); } } catch (error) { console.log(error); return null; } }; const getSettleInstruction = (market, marketDetails, accountDetails, owner) => { try { if (!accountDetails.openOrders || !accountDetails.baseTokenAccount || !accountDetails.quoteTokenAccount) { return null; } const programAddress = new web3_js_1.PublicKey(dex_constant_1.DEX_ADDRESS); let vaultSigner; // SHOULD WE MANUALLY PUT THE VAULT SIGNER IN THE CONST? // OR FIND A WAY TO AUTO FIND THEM // MARKET INFO? if (marketDetails.quote.mint.toString() === spl_token_1.NATIVE_MINT.toString()) { vaultSigner = new web3_js_1.PublicKey('51Cdt3oASXuVD88tAqJEeR6XH3PjQQ3xb7Cd22KaW2GK'); } else { vaultSigner = web3_js_1.PublicKey.createProgramAddressSync([ market.address.toBuffer(), new bn_js_1.default(1).toArrayLike(Buffer, 'le', 8), // ?? Might be when no PDAs ], programAddress); } const keys = [ { pubkey: market.address, isSigner: false, isWritable: true }, { pubkey: accountDetails.openOrders.hasOwnProperty('_keypair') ? accountDetails.openOrders.publicKey : accountDetails.openOrders, isSigner: false, isWritable: true }, { pubkey: owner, isSigner: true, isWritable: false }, { pubkey: marketDetails.base.vault, isSigner: false, isWritable: true }, { pubkey: marketDetails.quote.vault, isSigner: false, isWritable: true }, { pubkey: accountDetails.baseTokenAccount, isSigner: false, isWritable: true }, { pubkey: accountDetails.quoteTokenAccount, isSigner: false, isWritable: true }, { pubkey: vaultSigner, isSigner: false, isWritable: false }, { pubkey: spl_token_1.TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, ]; return new web3_js_1.TransactionInstruction({ keys, programId: programAddress, data: (0, instructions_1.encodeInstruction)({ settleFunds: {}, }), }); } catch (error) { return null; } }; const newSwap = async (owner, swap, lowestAsk, highestBid, connection) => { try { const baseAmount = parseFloat(swap.inputAmounts.base) ?? 0; const quoteAmount = parseFloat(swap.inputAmounts.quote) ?? 0; if (swap.sell ? baseAmount == 0 : quoteAmount == 0) { return { error: `Amount incorrect` }; } if (swap.sell ? baseAmount < swap.market.minBase : swap.amounts.base < swap.market.minBase) { return { error: `Must swap at least ${swap.market.minBase} ${swap.market.base.name}` }; } if (!lowestAsk || !highestBid) { return { error: `Error getting market data` }; } const limit = swap.sell ? highestBid * (1 - swap.market.swapMargin) : lowestAsk * (1 + swap.market.swapMargin); const size = swap.sell ? baseAmount : swap.amounts.base; const side = swap.sell ? Side.Sell : Side.Buy; const onchain = false; const swapTransaction = await (0, exports.getSwapTransaction)(owner, side, limit, size, swap.market, connection, onchain); if (typeof swapTransaction == 'string') { return { error: `Swap error, ${swapTransaction}` }; } else { return { transaction: swapTransaction }; } } catch (error) { return { error: `Swap error ${error}` }; } }; exports.newSwap = newSwap; var Side; (function (Side) { Side["Buy"] = "buy"; Side["Sell"] = "sell"; })(Side = exports.Side || (exports.Side = {}));