UNPKG

@cks-systems/manifest-sdk

Version:
1,129 lines 52.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ManifestClient = void 0; exports.toMantissaAndExponent = toMantissaAndExponent; const web3_js_1 = require("@solana/web3.js"); const spl_token_1 = require("@solana/spl-token"); const instructions_1 = require("./manifest/instructions"); const types_1 = require("./manifest/types"); const market_1 = require("./market"); const wrapperObj_1 = require("./wrapperObj"); const manifest_1 = require("./manifest"); const wrapper_1 = require("./wrapper"); const constants_1 = require("./constants"); const market_2 = require("./utils/market"); const discriminator_1 = require("./utils/discriminator"); const global_1 = require("./utils/global"); const global_2 = require("./global"); const marketDiscriminator = (0, discriminator_1.genAccDiscriminator)('manifest::state::market::MarketFixed'); class ManifestClient { connection; wrapper; market; payer; baseMint; quoteMint; baseGlobal; quoteGlobal; isBase22; isQuote22; constructor(connection, wrapper, market, payer, baseMint, quoteMint, // Globals are public. The expectation is that users will directly access // them, similar to the market. baseGlobal, quoteGlobal) { this.connection = connection; this.wrapper = wrapper; this.market = market; this.payer = payer; this.baseMint = baseMint; this.quoteMint = quoteMint; this.baseGlobal = baseGlobal; this.quoteGlobal = quoteGlobal; // If no extension data then the mint is not Token2022 this.isBase22 = baseMint.tlvData.length > 0; this.isQuote22 = quoteMint.tlvData.length > 0; } /** * fetches all user wrapper accounts and returns the first or null if none are found * * @param connection Connection * @param payerPub PublicKey of the trader * * @returns Promise<GetProgramAccountsResponse> */ static async fetchFirstUserWrapper(connection, payerPub) { const existingWrappers = await connection.getProgramAccounts(wrapper_1.PROGRAM_ID, { filters: [ // Dont check discriminant since there is only one type of account. { memcmp: { offset: 8, encoding: 'base58', bytes: payerPub.toBase58(), }, }, ], }); return existingWrappers.length > 0 ? existingWrappers[0] : null; } /** * list all Manifest markets using getProgramAccounts. caution: this is a heavy call. * * @param connection Connection * @returns PublicKey[] */ static async listMarketPublicKeys(connection) { const accounts = await connection.getProgramAccounts(manifest_1.PROGRAM_ID, { dataSlice: { offset: 0, length: 0 }, filters: [ { memcmp: { offset: 0, bytes: marketDiscriminator.toString('base64'), encoding: 'base64', }, }, ], }); return accounts.map((a) => a.pubkey); } /** * List all Manifest markets that match base and quote mint. If useApi, then * this call uses the manifest stats server instead of the heavy * getProgramAccounts RPC call. * * @param connection Connection * @param baseMint PublicKey * @param quoteMint PublicKey * @param useApi boolean * @returns PublicKey[] */ static async listMarketsForMints(connection, baseMint, quoteMint, useApi) { if (useApi) { const responseJson = await (await fetch('https://mfx-stats-mainnet.fly.dev/tickers')).json(); const tickers = responseJson .filter((ticker) => { return (ticker.base_currency == baseMint.toBase58() && ticker.target_currency == quoteMint.toBase58()); }) .map((ticker) => { return new web3_js_1.PublicKey(ticker.ticker_id); }); return tickers; } const accounts = await connection.getProgramAccounts(manifest_1.PROGRAM_ID, { dataSlice: { offset: 0, length: 0 }, filters: [ { memcmp: { offset: 0, bytes: marketDiscriminator.toString('base64'), encoding: 'base64', }, }, { memcmp: { offset: 16, bytes: baseMint.toBase58(), encoding: 'base58', }, }, { memcmp: { offset: 48, bytes: quoteMint.toBase58(), encoding: 'base58', }, }, ], }); return accounts.map((a) => a.pubkey); } /** * Get all market program accounts. This is expensive RPC load.. * * @param connection Connection * @returns GetProgramAccountsResponse */ static async getMarketProgramAccounts(connection) { const accounts = await connection.getProgramAccounts(manifest_1.PROGRAM_ID, { filters: [ { memcmp: { offset: 0, bytes: marketDiscriminator.toString('base64'), encoding: 'base64', }, }, ], }); return accounts; } /** * Create a new client which creates a wrapper and claims seat if needed. * * @param connection Connection * @param marketPk PublicKey of the market * @param payerKeypair Keypair of the trader * * @returns ManifestClient */ static async getClientForMarket(connection, marketPk, payerKeypair) { const marketObject = await market_1.Market.loadFromAddress({ connection: connection, address: marketPk, }); const baseMintPk = marketObject.baseMint(); const quoteMintPk = marketObject.quoteMint(); const baseMintAccountInfo = (await connection.getAccountInfo(baseMintPk)); const baseMint = (0, spl_token_1.unpackMint)(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner); const quoteMintAccountInfo = (await connection.getAccountInfo(quoteMintPk)); const quoteMint = (0, spl_token_1.unpackMint)(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner); const baseGlobal = await global_2.Global.loadFromAddress({ connection, address: (0, global_1.getGlobalAddress)(baseMint.address), }); const quoteGlobal = await global_2.Global.loadFromAddress({ connection, address: (0, global_1.getGlobalAddress)(quoteMint.address), }); const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, payerKeypair.publicKey); const transaction = new web3_js_1.Transaction(); if (!userWrapper) { const wrapperKeypair = web3_js_1.Keypair.generate(); const createAccountIx = web3_js_1.SystemProgram.createAccount({ fromPubkey: payerKeypair.publicKey, newAccountPubkey: wrapperKeypair.publicKey, space: constants_1.FIXED_WRAPPER_HEADER_SIZE, lamports: await connection.getMinimumBalanceForRentExemption(constants_1.FIXED_WRAPPER_HEADER_SIZE), programId: wrapper_1.PROGRAM_ID, }); const createWrapperIx = (0, wrapper_1.createCreateWrapperInstruction)({ owner: payerKeypair.publicKey, wrapperState: wrapperKeypair.publicKey, }); const claimSeatIx = (0, wrapper_1.createClaimSeatInstruction)({ manifestProgram: manifest_1.PROGRAM_ID, owner: payerKeypair.publicKey, market: marketPk, wrapperState: wrapperKeypair.publicKey, }); transaction.add(createAccountIx); transaction.add(createWrapperIx); transaction.add(claimSeatIx); await (0, web3_js_1.sendAndConfirmTransaction)(connection, transaction, [ payerKeypair, wrapperKeypair, ]); const wrapper = await wrapperObj_1.Wrapper.loadFromAddress({ connection, address: wrapperKeypair.publicKey, }); return new ManifestClient(connection, wrapper, marketObject, payerKeypair.publicKey, baseMint, quoteMint, baseGlobal, quoteGlobal); } // Otherwise there is an existing wrapper const wrapperData = wrapperObj_1.Wrapper.deserializeWrapperBuffer(userWrapper.account.data); const existingMarketInfos = wrapperData.marketInfos.filter((marketInfo) => { return marketInfo.market.toBase58() == marketPk.toBase58(); }); if (existingMarketInfos.length > 0) { const wrapper = await wrapperObj_1.Wrapper.loadFromAddress({ connection, address: userWrapper.pubkey, }); return new ManifestClient(connection, wrapper, marketObject, payerKeypair.publicKey, baseMint, quoteMint, baseGlobal, quoteGlobal); } // There is a wrapper, but need to claim a seat. const claimSeatIx = (0, wrapper_1.createClaimSeatInstruction)({ manifestProgram: manifest_1.PROGRAM_ID, owner: payerKeypair.publicKey, market: marketPk, wrapperState: userWrapper.pubkey, }); transaction.add(claimSeatIx); await (0, web3_js_1.sendAndConfirmTransaction)(connection, transaction, [payerKeypair]); const wrapper = await wrapperObj_1.Wrapper.loadFromAddress({ connection, address: userWrapper.pubkey, }); return new ManifestClient(connection, wrapper, marketObject, payerKeypair.publicKey, baseMint, quoteMint, baseGlobal, quoteGlobal); } /** * generate ixs which need to be executed in order to run a manifest client for a given market. `{ setupNeeded: false }` means all good. * this function should be used before getClientForMarketNoPrivateKey for UI cases where `Keypair`s cannot be directly passed in. * * @param connection Connection * @param marketPk PublicKey of the market * @param trader PublicKey of the trader * * @returns Promise<SetupData> */ static async getSetupIxs(connection, marketPk, trader) { const setupData = { setupNeeded: true, instructions: [], wrapperKeypair: null, }; const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader); if (!userWrapper) { const wrapperKeypair = web3_js_1.Keypair.generate(); setupData.wrapperKeypair = wrapperKeypair; const createAccountIx = web3_js_1.SystemProgram.createAccount({ fromPubkey: trader, newAccountPubkey: wrapperKeypair.publicKey, space: constants_1.FIXED_WRAPPER_HEADER_SIZE, lamports: await connection.getMinimumBalanceForRentExemption(constants_1.FIXED_WRAPPER_HEADER_SIZE), programId: wrapper_1.PROGRAM_ID, }); setupData.instructions.push(createAccountIx); const createWrapperIx = (0, wrapper_1.createCreateWrapperInstruction)({ owner: trader, wrapperState: wrapperKeypair.publicKey, }); setupData.instructions.push(createWrapperIx); const claimSeatIx = (0, wrapper_1.createClaimSeatInstruction)({ manifestProgram: manifest_1.PROGRAM_ID, owner: trader, market: marketPk, wrapperState: wrapperKeypair.publicKey, }); setupData.instructions.push(claimSeatIx); return setupData; } const wrapperData = wrapperObj_1.Wrapper.deserializeWrapperBuffer(userWrapper.account.data); const existingMarketInfos = wrapperData.marketInfos.filter((marketInfo) => { return marketInfo.market.toBase58() == marketPk.toBase58(); }); if (existingMarketInfos.length > 0) { setupData.setupNeeded = false; return setupData; } // There is a wrapper, but need to claim a seat. const claimSeatIx = (0, wrapper_1.createClaimSeatInstruction)({ manifestProgram: manifest_1.PROGRAM_ID, owner: trader, market: marketPk, wrapperState: userWrapper.pubkey, }); setupData.instructions.push(claimSeatIx); return setupData; } /** * Create a new client. throws if setup ixs are needed. Call ManifestClient.getSetupIxs to check if ixs are needed. * This is the way to create a client without directly passing in `Keypair` types (for example when building a UI). * * @param connection Connection * @param marketPk PublicKey of the market * @param trader PublicKey of the trader * * @returns ManifestClient */ static async getClientForMarketNoPrivateKey(connection, marketPk, trader) { const { setupNeeded } = await this.getSetupIxs(connection, marketPk, trader); if (setupNeeded) { throw new Error('setup ixs need to be executed first'); } const marketObject = await market_1.Market.loadFromAddress({ connection: connection, address: marketPk, }); const baseMintPk = marketObject.baseMint(); const quoteMintPk = marketObject.quoteMint(); const baseMintAccountInfo = (await connection.getAccountInfo(baseMintPk)); const baseMint = (0, spl_token_1.unpackMint)(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner); const quoteMintAccountInfo = (await connection.getAccountInfo(quoteMintPk)); const quoteMint = (0, spl_token_1.unpackMint)(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner); const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader); if (!userWrapper) { throw new Error('userWrapper is null even though setupNeeded is false. This should never happen.'); } const wrapper = await wrapperObj_1.Wrapper.loadFromAddress({ connection, address: userWrapper.pubkey, }); const baseGlobal = await global_2.Global.loadFromAddress({ connection, address: (0, global_1.getGlobalAddress)(baseMint.address), }); const quoteGlobal = await global_2.Global.loadFromAddress({ connection, address: (0, global_1.getGlobalAddress)(quoteMint.address), }); return new ManifestClient(connection, wrapper, marketObject, trader, baseMint, quoteMint, baseGlobal, quoteGlobal); } /** * Create a new client that is read only. Cannot send transactions or generate instructions. * * @param connection Connection * @param marketPk PublicKey of the market * @param trader PublicKey for trader whose wrapper to fetch * * @returns ManifestClient */ static async getClientReadOnly(connection, marketPk, trader) { const marketObject = await market_1.Market.loadFromAddress({ connection: connection, address: marketPk, }); const baseMintPk = marketObject.baseMint(); const quoteMintPk = marketObject.quoteMint(); const baseGlobalPk = (0, global_1.getGlobalAddress)(baseMintPk); const quoteGlobalPk = (0, global_1.getGlobalAddress)(quoteMintPk); const [baseMintAccountInfo, quoteMintAccountInfo, baseGlobalAccountInfo, quoteGlobalAccountInfo,] = await connection.getMultipleAccountsInfo([ baseMintPk, quoteMintPk, baseGlobalPk, quoteGlobalPk, ]); const baseMint = (0, spl_token_1.unpackMint)(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner); const quoteMint = (0, spl_token_1.unpackMint)(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner); // Global accounts are optional const baseGlobal = baseGlobalAccountInfo && global_2.Global.loadFromBuffer({ address: baseGlobalPk, buffer: baseGlobalAccountInfo.data, }); const quoteGlobal = quoteGlobalAccountInfo && global_2.Global.loadFromBuffer({ address: quoteGlobalPk, buffer: quoteGlobalAccountInfo.data, }); let wrapper = null; if (trader != null) { const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader); if (userWrapper) { wrapper = wrapperObj_1.Wrapper.loadFromBuffer({ address: userWrapper.pubkey, buffer: userWrapper.account.data, }); } } return new ManifestClient(connection, wrapper, marketObject, null, baseMint, quoteMint, baseGlobal, quoteGlobal); } /** * Initializes a ReadOnlyClient for each Market the trader has a seat on. * This has been optimized to be as light on the RPC as possible but it is * still using getProgramAccounts. caution: this is a heavy call. * * @param connection Connection * @param trader PublicKey * @returns ManifestClient[] */ static async getClientsReadOnlyForAllTraderSeats(connection, trader) { const marketAccountResponse = await connection.getProgramAccounts(manifest_1.PROGRAM_ID, { filters: [ { memcmp: { offset: 0, bytes: marketDiscriminator.toString('base64'), encoding: 'base64', }, }, ], withContext: true, }); const markets = marketAccountResponse.value.map((m) => market_1.Market.loadFromBuffer({ address: m.pubkey, buffer: m.account.data, slot: marketAccountResponse.context.slot, })); const marketsForTrader = markets.filter((m) => m.hasSeat(trader)); const baseMintPks = marketsForTrader.map((m) => m.baseMint().toString()); const quoteMintPks = marketsForTrader.map((m) => m.quoteMint().toString()); const baseGlobalPks = marketsForTrader.map((m) => (0, global_1.getGlobalAddress)(m.baseMint()).toString()); const quoteGlobalPks = marketsForTrader.map((m) => (0, global_1.getGlobalAddress)(m.quoteMint()).toString()); // ensure every account is only fetched once const allAisFetched = {}; const allPksToFetch = [ ...new Set([ ...baseMintPks, ...quoteMintPks, ...baseGlobalPks, ...quoteGlobalPks, ]), ]; const mutableCopy = Array.from(allPksToFetch); while (mutableCopy.length > 0) { const batchPks = mutableCopy.splice(0, 100); const batchAis = await connection.getMultipleAccountsInfoAndContext(batchPks.map((a) => new web3_js_1.PublicKey(a))); batchAis.value.forEach((ai, i) => (allAisFetched[batchPks[i]] = ai)); } let wrapper = null; if (trader != null) { const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader); if (userWrapper) { wrapper = wrapperObj_1.Wrapper.loadFromBuffer({ address: userWrapper.pubkey, buffer: userWrapper.account.data, }); } } return marketsForTrader.map((m, i) => { const baseMintAccountInfo = allAisFetched[baseMintPks[i]]; const quoteMintAccountInfo = allAisFetched[quoteMintPks[i]]; const baseGlobalAccountInfo = allAisFetched[baseGlobalPks[i]]; const quoteGlobalAccountInfo = allAisFetched[quoteGlobalPks[i]]; const baseMint = (0, spl_token_1.unpackMint)(m.baseMint(), baseMintAccountInfo, baseMintAccountInfo.owner); const quoteMint = (0, spl_token_1.unpackMint)(m.quoteMint(), quoteMintAccountInfo, quoteMintAccountInfo.owner); // Global accounts are optional const baseGlobal = baseGlobalAccountInfo && global_2.Global.loadFromBuffer({ address: new web3_js_1.PublicKey(baseGlobalPks[i]), buffer: baseGlobalAccountInfo.data, }); const quoteGlobal = quoteGlobalAccountInfo && global_2.Global.loadFromBuffer({ address: new web3_js_1.PublicKey(quoteGlobalPks[i]), buffer: quoteGlobalAccountInfo.data, }); return new ManifestClient(connection, wrapper, m, null, baseMint, quoteMint, baseGlobal, quoteGlobal); }); } /** * Reload the market and wrapper and global objects. */ async reload() { await Promise.all([ () => { if (this.wrapper) { return this.wrapper.reload(this.connection); } }, () => { if (this.baseGlobal) { return this.baseGlobal.reload(this.connection); } }, () => { if (this.quoteGlobal) { return this.quoteGlobal.reload(this.connection); } }, this.market.reload(this.connection), ]); } /** * CreateMarket instruction. Assumes the account is already funded onchain. * * @param payer PublicKey of the trader * @param baseMint PublicKey of the baseMint * @param quoteMint PublicKey of the quoteMint * @param market PublicKey of the market that will be created. Private key * will need to be a signer. * * @returns TransactionInstruction */ static createMarketIx(payer, baseMint, quoteMint, market) { const baseVault = (0, market_2.getVaultAddress)(market, baseMint); const quoteVault = (0, market_2.getVaultAddress)(market, quoteMint); return (0, instructions_1.createCreateMarketInstruction)({ payer, market, baseVault, quoteVault, baseMint, quoteMint, tokenProgram22: spl_token_1.TOKEN_2022_PROGRAM_ID, }); } /** * Deposit instruction * * @param payer PublicKey of the trader * @param mint PublicKey for deposit mint. Must be either the base or quote * @param amountTokens Number of tokens to deposit. * * @returns TransactionInstruction */ depositIx(payer, mint, amountTokens) { if (!this.wrapper || !this.payer) { throw new Error('Read only'); } const vault = (0, market_2.getVaultAddress)(this.market.address, mint); const is22 = (mint.equals(this.baseMint.address) && this.isBase22) || (mint.equals(this.quoteMint.address) && this.isQuote22); const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, payer, true, is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID); const mintDecimals = this.market.quoteMint().toBase58() === mint.toBase58() ? this.market.quoteDecimals() : this.market.baseDecimals(); const amountAtoms = Math.ceil(amountTokens * 10 ** mintDecimals); return (0, wrapper_1.createDepositInstruction)({ market: this.market.address, traderTokenAccount, vault, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, mint, tokenProgram: is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, }, { params: { amountAtoms, }, }); } /** * Withdraw instruction * * @param payer PublicKey of the trader * @param mint PublicKey for withdraw mint. Must be either the base or quote * @param amountTokens Number of tokens to withdraw. * * @returns TransactionInstruction */ withdrawIx(payer, mint, amountTokens) { if (!this.wrapper || !this.payer) { throw new Error('Read only'); } const vault = (0, market_2.getVaultAddress)(this.market.address, mint); const is22 = (mint.equals(this.baseMint.address) && this.isBase22) || (mint.equals(this.quoteMint.address) && this.isQuote22); const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, payer, true, is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID); const mintDecimals = this.market.quoteMint().toBase58() === mint.toBase58() ? this.market.quoteDecimals() : this.market.baseDecimals(); const amountAtoms = Math.floor(amountTokens * 10 ** mintDecimals); return (0, wrapper_1.createWithdrawInstruction)({ market: this.market.address, traderTokenAccount, vault, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, mint, tokenProgram: is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, }, { params: { amountAtoms, }, }); } /** * Withdraw All instruction. Withdraws all available base and quote tokens * * @returns TransactionInstruction[] */ withdrawAllIx() { if (!this.wrapper || !this.payer) { throw new Error('Read only'); } const withdrawInstructions = []; const baseBalance = this.market.getWithdrawableBalanceTokens(this.payer, true); if (baseBalance > 0) { const baseWithdrawIx = this.withdrawIx(this.payer, this.market.baseMint(), baseBalance); withdrawInstructions.push(baseWithdrawIx); } const quoteBalance = this.market.getWithdrawableBalanceTokens(this.payer, false); if (quoteBalance > 0) { const quoteWithdrawIx = this.withdrawIx(this.payer, this.market.quoteMint(), quoteBalance); withdrawInstructions.push(quoteWithdrawIx); } return withdrawInstructions; } /** * PlaceOrder instruction * * @param params WrapperPlaceOrderParamsExternal | WrapperPlaceOrderReverseParamsExternal * including all the information for placing an order like amount, price, * ordertype, ... This is called external because to avoid conflicts with the * autogenerated version which has problems with expressing some of the * parameters. The reverse type has a spreadBps field instead of lastValidSlot. * * @returns TransactionInstruction */ placeOrderIx(params) { if (!this.wrapper || !this.payer) { throw new Error('Read only'); } if (params.orderType != types_1.OrderType.Global) { return (0, wrapper_1.createBatchUpdateInstruction)({ market: this.market.address, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, }, { params: { cancels: [], cancelAll: false, orders: [toWrapperPlaceOrderParams(this.market, params)], }, }); } if (params.isBid) { const global = (0, global_1.getGlobalAddress)(this.quoteMint.address); const globalVault = (0, global_1.getGlobalVaultAddress)(this.quoteMint.address); const vault = (0, market_2.getVaultAddress)(this.market.address, this.quoteMint.address); return (0, wrapper_1.createBatchUpdateQuoteGlobalInstruction)({ market: this.market.address, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, quoteMint: this.quoteMint.address, quoteGlobal: global, quoteGlobalVault: globalVault, quoteMarketVault: vault, quoteTokenProgram: this.isQuote22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, }, { params: { cancels: [], cancelAll: false, orders: [toWrapperPlaceOrderParams(this.market, params)], }, }); } else { const global = (0, global_1.getGlobalAddress)(this.baseMint.address); const globalVault = (0, global_1.getGlobalVaultAddress)(this.baseMint.address); const vault = (0, market_2.getVaultAddress)(this.market.address, this.baseMint.address); return (0, wrapper_1.createBatchUpdateBaseGlobalInstruction)({ market: this.market.address, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, baseMint: this.baseMint.address, baseGlobal: global, baseGlobalVault: globalVault, baseMarketVault: vault, baseTokenProgram: this.isBase22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, }, { params: { cancels: [], cancelAll: false, orders: [toWrapperPlaceOrderParams(this.market, params)], }, }); } } /** * PlaceOrderWithRequiredDeposit instruction. Only deposits the appropriate base * or quote tokens if not in the withdrawable balances. * * @param payer PublicKey of the trader * @param params WrapperPlaceOrderParamsExternal | WrapperPlaceOrderReverseParamsExternal * including all the information for placing an order like amount, price, * ordertype, ... This is called external because to avoid conflicts with the * autogenerated version which has problems with expressing some of the * parameters. The reverse type has a spreadBps field instead of lastValidSlot. * * @returns TransactionInstruction[] */ async placeOrderWithRequiredDepositIxs(payer, params) { const placeOrderIx = this.placeOrderIx(params); if (params.orderType != types_1.OrderType.Global) { const currentBalanceTokens = this.market.getWithdrawableBalanceTokens(payer, !params.isBid); let depositMint; let depositAmountTokens = 0; if (params.isBid) { depositMint = this.market.quoteMint(); depositAmountTokens = params.numBaseTokens * params.tokenPrice - currentBalanceTokens; } else { depositMint = this.market.baseMint(); depositAmountTokens = params.numBaseTokens - currentBalanceTokens; } if (depositAmountTokens <= 0) { return [placeOrderIx]; } const depositIx = this.depositIx(payer, depositMint, depositAmountTokens); return [depositIx, placeOrderIx]; } else { const global = (params.isBid ? this.quoteGlobal : this.baseGlobal); const currentBalanceTokens = await global.getGlobalBalanceTokens(this.connection, payer); let depositMint; let depositAmountTokens = 0; if (params.isBid) { depositMint = this.market.quoteMint(); depositAmountTokens = params.numBaseTokens * params.tokenPrice - currentBalanceTokens; } else { depositMint = this.market.baseMint(); depositAmountTokens = params.numBaseTokens - currentBalanceTokens; } if (depositAmountTokens <= 0) { return [placeOrderIx]; } const depositIx = await ManifestClient.globalDepositIx(this.connection, payer, depositMint, depositAmountTokens); return [depositIx, placeOrderIx]; } } /** * Swap instruction * * Optimized swap for routers and arb bots. Normal traders should compose * depost/withdraw/placeOrder to get limit orders. Does not go through the * wrapper. * * @param payer PublicKey of the trader * @param params SwapParams * * @returns TransactionInstruction */ swapIx(payer, params) { const traderBase = (0, spl_token_1.getAssociatedTokenAddressSync)(this.baseMint.address, payer, true, this.isBase22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID); const traderQuote = (0, spl_token_1.getAssociatedTokenAddressSync)(this.quoteMint.address, payer, true, this.isQuote22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID); const baseVault = (0, market_2.getVaultAddress)(this.market.address, this.baseMint.address); const quoteVault = (0, market_2.getVaultAddress)(this.market.address, this.quoteMint.address); const global = (0, global_1.getGlobalAddress)(params.isBaseIn ? this.quoteMint.address : this.baseMint.address); const globalVault = (0, global_1.getGlobalVaultAddress)(params.isBaseIn ? this.quoteMint.address : this.baseMint.address); // Assumes just normal token program for now. // No Token22 support here in sdk yet, but includes programs and mints as // though it was. // No support for the case where global are not needed. That is an // optimization that needs to be made when looking at the orderbook and // deciding if it is worthwhile to lock the accounts. return (0, instructions_1.createSwapInstruction)({ payer, market: this.market.address, traderBase, traderQuote, baseVault, quoteVault, tokenProgramBase: this.isBase22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, baseMint: this.baseMint.address, tokenProgramQuote: this.isQuote22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, quoteMint: this.quoteMint.address, global, globalVault, }, { params, }); } /** * CancelOrder instruction * * @param params WrapperCancelOrderParams includes the clientOrderId of the * order to cancel. * * @returns TransactionInstruction */ cancelOrderIx(params) { if (!this.wrapper || !this.payer) { throw new Error('Read only'); } // Global not required for cancels. If we do cancel a global, then our gas // prepayment is abandoned. return (0, wrapper_1.createBatchUpdateInstruction)({ market: this.market.address, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, }, { params: { cancels: [params], cancelAll: false, orders: [], }, }); } /** * BatchUpdate instruction * * @param placeParams (WrapperPlaceOrderParamsExternal | WrapperPlaceOrderReverseParamsExternal)[] * including all the information for placing an order like amount, price, * ordertype, ... This is called external because to avoid conflicts with the * autogenerated version which has problems with expressing some of the * parameters. The reverse type has a spreadBps field instead of lastValidSlot. * @param params WrapperCancelOrderParams[] includes the clientOrderId of the * order to cancel. * * @returns TransactionInstruction */ batchUpdateIx(placeParams, cancelParams, cancelAll) { if (!this.wrapper || !this.payer) { throw new Error('Read only'); } const baseGlobalRequired = placeParams.some((placeParams) => { return !placeParams.isBid && placeParams.orderType == types_1.OrderType.Global; }); const quoteGlobalRequired = placeParams.some((placeParams) => { return placeParams.isBid && placeParams.orderType == types_1.OrderType.Global; }); if (!baseGlobalRequired && !quoteGlobalRequired) { return (0, wrapper_1.createBatchUpdateInstruction)({ market: this.market.address, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, }, { params: { cancels: cancelParams, cancelAll, orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)), }, }); } if (!baseGlobalRequired && quoteGlobalRequired) { const global = (0, global_1.getGlobalAddress)(this.quoteMint.address); const globalVault = (0, global_1.getGlobalVaultAddress)(this.quoteMint.address); const vault = (0, market_2.getVaultAddress)(this.market.address, this.quoteMint.address); return (0, wrapper_1.createBatchUpdateQuoteGlobalInstruction)({ market: this.market.address, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, quoteMint: this.quoteMint.address, quoteGlobal: global, quoteGlobalVault: globalVault, quoteTokenProgram: this.isQuote22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, quoteMarketVault: vault, }, { params: { cancels: cancelParams, cancelAll, orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)), }, }); } if (baseGlobalRequired && !quoteGlobalRequired) { const global = (0, global_1.getGlobalAddress)(this.baseMint.address); const globalVault = (0, global_1.getGlobalVaultAddress)(this.baseMint.address); const vault = (0, market_2.getVaultAddress)(this.market.address, this.baseMint.address); return (0, wrapper_1.createBatchUpdateBaseGlobalInstruction)({ market: this.market.address, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, baseMint: this.baseMint.address, baseGlobal: global, baseGlobalVault: globalVault, baseTokenProgram: this.isBase22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, baseMarketVault: vault, }, { params: { cancels: cancelParams, cancelAll, orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)), }, }); } const baseGlobal = (0, global_1.getGlobalAddress)(this.baseMint.address); const baseGlobalVault = (0, global_1.getGlobalVaultAddress)(this.baseMint.address); const baseMarketVault = (0, market_2.getVaultAddress)(this.market.address, this.baseMint.address); const quoteGlobal = (0, global_1.getGlobalAddress)(this.quoteMint.address); const quoteGlobalVault = (0, global_1.getGlobalVaultAddress)(this.quoteMint.address); const quoteMarketVault = (0, market_2.getVaultAddress)(this.market.address, this.quoteMint.address); return (0, wrapper_1.createBatchUpdateInstruction)({ market: this.market.address, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, baseMint: this.baseMint.address, baseGlobal, baseGlobalVault, baseTokenProgram: this.isBase22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, baseMarketVault, quoteMint: this.quoteMint.address, quoteGlobal, quoteGlobalVault, quoteTokenProgram: this.isQuote22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, quoteMarketVault, }, { params: { cancels: cancelParams, cancelAll, orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)), }, }); } /** * CancelAll instruction. Cancels all orders on a market. This is discouraged * outside of circuit breaker usage because it is less efficient and does not * cancel global cleanly. Use batchUpdate instead. This also does not cancel * any orders not placed through the wrapper, which includes reverse orders * that were reversed. * * @returns TransactionInstruction */ cancelAllIx() { if (!this.wrapper || !this.payer) { throw new Error('Read only'); } // Global not required for cancelAll. If we do cancel a global, then our gas // prepayment is abandoned. return (0, wrapper_1.createBatchUpdateInstruction)({ market: this.market.address, manifestProgram: manifest_1.PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, }, { params: { cancels: [], cancelAll: true, orders: [], }, }); } /** * CancelAllOnCore instruction. Cancels all orders on a market directly on the core program, * including reverse orders and global orders with rent prepayment. * * @returns TransactionInstruction[] */ async cancelAllOnCoreIx() { if (!this.payer) { throw new Error('Read only'); } const openOrders = this.market.openOrders(); const ordersToCancel = []; for (const openOrder of openOrders) { if (openOrder.trader.toBase58() === this.payer.toBase58()) { const seqNum = openOrder.sequenceNumber; ordersToCancel.push({ orderSequenceNumber: seqNum, orderIndexHint: null, }); } } const MAX_CANCELS_PER_BATCH = 25; const cancelInstructions = []; for (let i = 0; i < ordersToCancel.length; i += MAX_CANCELS_PER_BATCH) { const batchOfCancels = ordersToCancel.slice(i, i + MAX_CANCELS_PER_BATCH); const batchedCancelInstruction = (0, instructions_1.createBatchUpdateInstruction)({ payer: this.payer, market: this.market.address, }, { params: { cancels: batchOfCancels, orders: [], traderIndexHint: null, }, }); cancelInstructions.push(batchedCancelInstruction); } return cancelInstructions; } /** * killSwitchMarket transactions. Pulls all orders * and withdraws all balances from the market in two transactions * * @param payer PublicKey of the trader * * @returns TransactionSignatures[] */ async killSwitchMarket(payerKeypair) { await this.market.reload(this.connection); const cancelAllIx = this.cancelAllIx(); const cancelAllTx = new web3_js_1.Transaction(); const cancelAllSig = await (0, web3_js_1.sendAndConfirmTransaction)(this.connection, cancelAllTx.add(cancelAllIx), [payerKeypair], { skipPreflight: true, commitment: 'confirmed', }); // TOOD: Merge this into one transaction await this.market.reload(this.connection); const withdrawAllIx = this.withdrawAllIx(); const withdrawAllTx = new web3_js_1.Transaction(); const withdrawAllSig = await (0, web3_js_1.sendAndConfirmTransaction)(this.connection, withdrawAllTx.add(...withdrawAllIx), [payerKeypair], { skipPreflight: true, commitment: 'confirmed', }); return [cancelAllSig, withdrawAllSig]; } /** * CreateGlobalCreate instruction. Creates the global account. Should be used only once per mint. * * @param connection Connection to pull mint info * @param payer PublicKey of the trader * @param globalMint PublicKey of the globalMint * * @returns Promise<TransactionInstruction> */ static async createGlobalCreateIx(connection, payer, globalMint) { const global = (0, global_1.getGlobalAddress)(globalMint); const globalVault = (0, global_1.getGlobalVaultAddress)(globalMint); const globalMintAccountInfo = (await connection.getAccountInfo(globalMint)); const mint = (0, spl_token_1.unpackMint)(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner); const is22 = mint.tlvData.length > 0; return (0, instructions_1.createGlobalCreateInstruction)({ payer, global, mint: globalMint, globalVault, tokenProgram: is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, }); } /** * CreateGlobalAddTrader instruction. Adds a new trader to the global account. * Static because it does not require a wrapper. * * @param payer PublicKey of the trader * @param globalMint PublicKey of the globalMint * * @returns TransactionInstruction */ static createGlobalAddTraderIx(payer, globalMint) { const global = (0, global_1.getGlobalAddress)(globalMint); return (0, instructions_1.createGlobalAddTraderInstruction)({ payer, global, }); } /** * Global deposit instruction. Static because it does not require a wrapper. * * @param connection Connection to pull mint info * @param payer PublicKey of the trader * @param globalMint PublicKey for global mint deposit. * @param amountTokens Number of tokens to deposit. * * @returns Promise<TransactionInstruction> */ static async globalDepositIx(connection, payer, globalMint, amountTokens) { const globalAddress = (0, global_1.getGlobalAddress)(globalMint); const globalVault = (0, global_1.getGlobalVaultAddress)(globalMint); const globalMintAccountInfo = (await connection.getAccountInfo(globalMint)); const mint = (0, spl_token_1.unpackMint)(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner); const is22 = mint.tlvData.length > 0; const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(globalMint, payer, true, is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID); const mintDecimals = mint.decimals; const amountAtoms = Math.ceil(amountTokens * 10 ** mintDecimals); return (0, instructions_1.createGlobalDepositInstruction)({ payer: payer, global: globalAddress, mint: globalMint, globalVault: globalVault, traderToken: traderTokenAccount, tokenProgram: is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID, }, { params: { amountAtoms, }, }); } /** * Global withdraw instruction. Static because it does not require a wrapper. * * @param connection Connection to pull mint info * @param payer PublicKey of the trader * @param globalMint PublicKey for global mint withdraw. * @param amountTokens Number of tokens to withdraw. * * @returns Promise<TransactionInstruction> */ static async globalWithdrawIx(connection, payer, globalMint, amountTokens) { const globalAddress = (0, global_1.getGlobalAddress)(globalMint); const globalVault = (0, global_1.getGlobalVaultAddress)(globalMint); const globalMintAccountInfo = (await connection.getAccountInfo(globalMint)); const mint = (0, spl_token_1.unpackMint)(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner); const is22 = mint.tlvData.length > 0; const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(globalMint, payer, true, is22 ? spl_token_1.TOKEN_2022_PROGRAM_ID : spl_token_1.TOKEN_PROGRAM_ID); const mintDecimals = mint.decimals; const amountAto