UNPKG

@cks-systems/manifest-sdk

Version:
1,159 lines 50.6 kB
import { PublicKey, Keypair, SystemProgram, Transaction, sendAndConfirmTransaction, } from '@solana/web3.js'; import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, unpackMint, } from '@solana/spl-token'; import { createCreateMarketInstruction, createGlobalAddTraderInstruction, createGlobalCreateInstruction, createGlobalDepositInstruction, createGlobalWithdrawInstruction, createSwapInstruction, createBatchUpdateInstruction as createBatchUpdateCoreInstruction, } from './manifest/instructions'; import { OrderType } from './manifest/types'; import { Market } from './market'; import { Wrapper } from './wrapperObj'; import { PROGRAM_ID as MANIFEST_PROGRAM_ID, PROGRAM_ID } from './manifest'; import { PROGRAM_ID as WRAPPER_PROGRAM_ID, createBatchUpdateBaseGlobalInstruction, createBatchUpdateInstruction, createBatchUpdateQuoteGlobalInstruction, createClaimSeatInstruction, createCreateWrapperInstruction, createDepositInstruction, createWithdrawInstruction, } from './wrapper'; import { FIXED_WRAPPER_HEADER_SIZE, NO_EXPIRATION_LAST_VALID_SLOT, } from './constants'; import { getVaultAddress } from './utils/market'; import { genAccDiscriminator } from './utils/discriminator'; import { getGlobalAddress, getGlobalVaultAddress } from './utils/global'; import { Global } from './global'; const marketDiscriminator = genAccDiscriminator('manifest::state::market::MarketFixed'); export 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_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(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 PublicKey(ticker.ticker_id); }); return tickers; } const accounts = await connection.getProgramAccounts(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(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.loadFromAddress({ connection: connection, address: marketPk, }); const baseMintPk = marketObject.baseMint(); const quoteMintPk = marketObject.quoteMint(); const baseMintAccountInfo = (await connection.getAccountInfo(baseMintPk)); const baseMint = unpackMint(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner); const quoteMintAccountInfo = (await connection.getAccountInfo(quoteMintPk)); const quoteMint = unpackMint(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner); const baseGlobal = await Global.loadFromAddress({ connection, address: getGlobalAddress(baseMint.address), }); const quoteGlobal = await Global.loadFromAddress({ connection, address: getGlobalAddress(quoteMint.address), }); const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, payerKeypair.publicKey); const transaction = new Transaction(); if (!userWrapper) { const wrapperKeypair = Keypair.generate(); const createAccountIx = SystemProgram.createAccount({ fromPubkey: payerKeypair.publicKey, newAccountPubkey: wrapperKeypair.publicKey, space: FIXED_WRAPPER_HEADER_SIZE, lamports: await connection.getMinimumBalanceForRentExemption(FIXED_WRAPPER_HEADER_SIZE), programId: WRAPPER_PROGRAM_ID, }); const createWrapperIx = createCreateWrapperInstruction({ owner: payerKeypair.publicKey, wrapperState: wrapperKeypair.publicKey, }); const claimSeatIx = createClaimSeatInstruction({ manifestProgram: MANIFEST_PROGRAM_ID, owner: payerKeypair.publicKey, market: marketPk, wrapperState: wrapperKeypair.publicKey, }); transaction.add(createAccountIx); transaction.add(createWrapperIx); transaction.add(claimSeatIx); await sendAndConfirmTransaction(connection, transaction, [ payerKeypair, wrapperKeypair, ]); const wrapper = await 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 = Wrapper.deserializeWrapperBuffer(userWrapper.account.data); const existingMarketInfos = wrapperData.marketInfos.filter((marketInfo) => { return marketInfo.market.toBase58() == marketPk.toBase58(); }); if (existingMarketInfos.length > 0) { const wrapper = await 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 = createClaimSeatInstruction({ manifestProgram: MANIFEST_PROGRAM_ID, owner: payerKeypair.publicKey, market: marketPk, wrapperState: userWrapper.pubkey, }); transaction.add(claimSeatIx); await sendAndConfirmTransaction(connection, transaction, [payerKeypair]); const wrapper = await 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 = Keypair.generate(); setupData.wrapperKeypair = wrapperKeypair; const createAccountIx = SystemProgram.createAccount({ fromPubkey: trader, newAccountPubkey: wrapperKeypair.publicKey, space: FIXED_WRAPPER_HEADER_SIZE, lamports: await connection.getMinimumBalanceForRentExemption(FIXED_WRAPPER_HEADER_SIZE), programId: WRAPPER_PROGRAM_ID, }); setupData.instructions.push(createAccountIx); const createWrapperIx = createCreateWrapperInstruction({ owner: trader, wrapperState: wrapperKeypair.publicKey, }); setupData.instructions.push(createWrapperIx); const claimSeatIx = createClaimSeatInstruction({ manifestProgram: MANIFEST_PROGRAM_ID, owner: trader, market: marketPk, wrapperState: wrapperKeypair.publicKey, }); setupData.instructions.push(claimSeatIx); return setupData; } const wrapperData = 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 = createClaimSeatInstruction({ manifestProgram: MANIFEST_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.loadFromAddress({ connection: connection, address: marketPk, }); const baseMintPk = marketObject.baseMint(); const quoteMintPk = marketObject.quoteMint(); const baseMintAccountInfo = (await connection.getAccountInfo(baseMintPk)); const baseMint = unpackMint(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner); const quoteMintAccountInfo = (await connection.getAccountInfo(quoteMintPk)); const quoteMint = 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 Wrapper.loadFromAddress({ connection, address: userWrapper.pubkey, }); const baseGlobal = await Global.loadFromAddress({ connection, address: getGlobalAddress(baseMint.address), }); const quoteGlobal = await Global.loadFromAddress({ connection, address: 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.loadFromAddress({ connection: connection, address: marketPk, }); const baseMintPk = marketObject.baseMint(); const quoteMintPk = marketObject.quoteMint(); const baseGlobalPk = getGlobalAddress(baseMintPk); const quoteGlobalPk = getGlobalAddress(quoteMintPk); const [baseMintAccountInfo, quoteMintAccountInfo, baseGlobalAccountInfo, quoteGlobalAccountInfo,] = await connection.getMultipleAccountsInfo([ baseMintPk, quoteMintPk, baseGlobalPk, quoteGlobalPk, ]); const baseMint = unpackMint(baseMintPk, baseMintAccountInfo, baseMintAccountInfo.owner); const quoteMint = unpackMint(quoteMintPk, quoteMintAccountInfo, quoteMintAccountInfo.owner); // Global accounts are optional const baseGlobal = baseGlobalAccountInfo && Global.loadFromBuffer({ address: baseGlobalPk, buffer: baseGlobalAccountInfo.data, }); const quoteGlobal = quoteGlobalAccountInfo && Global.loadFromBuffer({ address: quoteGlobalPk, buffer: quoteGlobalAccountInfo.data, }); let wrapper = null; if (trader != null) { const userWrapper = await ManifestClient.fetchFirstUserWrapper(connection, trader); if (userWrapper) { wrapper = 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(PROGRAM_ID, { filters: [ { memcmp: { offset: 0, bytes: marketDiscriminator.toString('base64'), encoding: 'base64', }, }, ], withContext: true, }); const markets = marketAccountResponse.value.map((m) => 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) => getGlobalAddress(m.baseMint()).toString()); const quoteGlobalPks = marketsForTrader.map((m) => 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 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 = 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 = unpackMint(m.baseMint(), baseMintAccountInfo, baseMintAccountInfo.owner); const quoteMint = unpackMint(m.quoteMint(), quoteMintAccountInfo, quoteMintAccountInfo.owner); // Global accounts are optional const baseGlobal = baseGlobalAccountInfo && Global.loadFromBuffer({ address: new PublicKey(baseGlobalPks[i]), buffer: baseGlobalAccountInfo.data, }); const quoteGlobal = quoteGlobalAccountInfo && Global.loadFromBuffer({ address: new 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 = getVaultAddress(market, baseMint); const quoteVault = getVaultAddress(market, quoteMint); return createCreateMarketInstruction({ payer, market, baseVault, quoteVault, baseMint, quoteMint, tokenProgram22: 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 = getVaultAddress(this.market.address, mint); const is22 = (mint.equals(this.baseMint.address) && this.isBase22) || (mint.equals(this.quoteMint.address) && this.isQuote22); const traderTokenAccount = getAssociatedTokenAddressSync(mint, payer, true, is22 ? TOKEN_2022_PROGRAM_ID : 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 createDepositInstruction({ market: this.market.address, traderTokenAccount, vault, manifestProgram: MANIFEST_PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, mint, tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : 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 = getVaultAddress(this.market.address, mint); const is22 = (mint.equals(this.baseMint.address) && this.isBase22) || (mint.equals(this.quoteMint.address) && this.isQuote22); const traderTokenAccount = getAssociatedTokenAddressSync(mint, payer, true, is22 ? TOKEN_2022_PROGRAM_ID : 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 createWithdrawInstruction({ market: this.market.address, traderTokenAccount, vault, manifestProgram: MANIFEST_PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, mint, tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : 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 != OrderType.Global) { return createBatchUpdateInstruction({ market: this.market.address, manifestProgram: MANIFEST_PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, }, { params: { cancels: [], cancelAll: false, orders: [toWrapperPlaceOrderParams(this.market, params)], }, }); } if (params.isBid) { const global = getGlobalAddress(this.quoteMint.address); const globalVault = getGlobalVaultAddress(this.quoteMint.address); const vault = getVaultAddress(this.market.address, this.quoteMint.address); return createBatchUpdateQuoteGlobalInstruction({ market: this.market.address, manifestProgram: MANIFEST_PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, quoteMint: this.quoteMint.address, quoteGlobal: global, quoteGlobalVault: globalVault, quoteMarketVault: vault, quoteTokenProgram: this.isQuote22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, }, { params: { cancels: [], cancelAll: false, orders: [toWrapperPlaceOrderParams(this.market, params)], }, }); } else { const global = getGlobalAddress(this.baseMint.address); const globalVault = getGlobalVaultAddress(this.baseMint.address); const vault = getVaultAddress(this.market.address, this.baseMint.address); return createBatchUpdateBaseGlobalInstruction({ market: this.market.address, manifestProgram: MANIFEST_PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, baseMint: this.baseMint.address, baseGlobal: global, baseGlobalVault: globalVault, baseMarketVault: vault, baseTokenProgram: this.isBase22 ? TOKEN_2022_PROGRAM_ID : 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 != 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 = getAssociatedTokenAddressSync(this.baseMint.address, payer, true, this.isBase22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID); const traderQuote = getAssociatedTokenAddressSync(this.quoteMint.address, payer, true, this.isQuote22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID); const baseVault = getVaultAddress(this.market.address, this.baseMint.address); const quoteVault = getVaultAddress(this.market.address, this.quoteMint.address); const global = getGlobalAddress(params.isBaseIn ? this.quoteMint.address : this.baseMint.address); const globalVault = 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 createSwapInstruction({ payer, market: this.market.address, traderBase, traderQuote, baseVault, quoteVault, tokenProgramBase: this.isBase22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, baseMint: this.baseMint.address, tokenProgramQuote: this.isQuote22 ? TOKEN_2022_PROGRAM_ID : 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 createBatchUpdateInstruction({ market: this.market.address, manifestProgram: MANIFEST_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 == OrderType.Global; }); const quoteGlobalRequired = placeParams.some((placeParams) => { return placeParams.isBid && placeParams.orderType == OrderType.Global; }); if (!baseGlobalRequired && !quoteGlobalRequired) { return createBatchUpdateInstruction({ market: this.market.address, manifestProgram: MANIFEST_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 = getGlobalAddress(this.quoteMint.address); const globalVault = getGlobalVaultAddress(this.quoteMint.address); const vault = getVaultAddress(this.market.address, this.quoteMint.address); return createBatchUpdateQuoteGlobalInstruction({ market: this.market.address, manifestProgram: MANIFEST_PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, quoteMint: this.quoteMint.address, quoteGlobal: global, quoteGlobalVault: globalVault, quoteTokenProgram: this.isQuote22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, quoteMarketVault: vault, }, { params: { cancels: cancelParams, cancelAll, orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)), }, }); } if (baseGlobalRequired && !quoteGlobalRequired) { const global = getGlobalAddress(this.baseMint.address); const globalVault = getGlobalVaultAddress(this.baseMint.address); const vault = getVaultAddress(this.market.address, this.baseMint.address); return createBatchUpdateBaseGlobalInstruction({ market: this.market.address, manifestProgram: MANIFEST_PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, baseMint: this.baseMint.address, baseGlobal: global, baseGlobalVault: globalVault, baseTokenProgram: this.isBase22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, baseMarketVault: vault, }, { params: { cancels: cancelParams, cancelAll, orders: placeParams.map((params) => toWrapperPlaceOrderParams(this.market, params)), }, }); } const baseGlobal = getGlobalAddress(this.baseMint.address); const baseGlobalVault = getGlobalVaultAddress(this.baseMint.address); const baseMarketVault = getVaultAddress(this.market.address, this.baseMint.address); const quoteGlobal = getGlobalAddress(this.quoteMint.address); const quoteGlobalVault = getGlobalVaultAddress(this.quoteMint.address); const quoteMarketVault = getVaultAddress(this.market.address, this.quoteMint.address); return createBatchUpdateInstruction({ market: this.market.address, manifestProgram: MANIFEST_PROGRAM_ID, owner: this.payer, wrapperState: this.wrapper.address, baseMint: this.baseMint.address, baseGlobal, baseGlobalVault, baseTokenProgram: this.isBase22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, baseMarketVault, quoteMint: this.quoteMint.address, quoteGlobal, quoteGlobalVault, quoteTokenProgram: this.isQuote22 ? TOKEN_2022_PROGRAM_ID : 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 createBatchUpdateInstruction({ market: this.market.address, manifestProgram: MANIFEST_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 = createBatchUpdateCoreInstruction({ 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 Transaction(); const cancelAllSig = await 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 Transaction(); const withdrawAllSig = await 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 = getGlobalAddress(globalMint); const globalVault = getGlobalVaultAddress(globalMint); const globalMintAccountInfo = (await connection.getAccountInfo(globalMint)); const mint = unpackMint(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner); const is22 = mint.tlvData.length > 0; return createGlobalCreateInstruction({ payer, global, mint: globalMint, globalVault, tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : 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 = getGlobalAddress(globalMint); return 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 = getGlobalAddress(globalMint); const globalVault = getGlobalVaultAddress(globalMint); const globalMintAccountInfo = (await connection.getAccountInfo(globalMint)); const mint = unpackMint(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner); const is22 = mint.tlvData.length > 0; const traderTokenAccount = getAssociatedTokenAddressSync(globalMint, payer, true, is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID); const mintDecimals = mint.decimals; const amountAtoms = Math.ceil(amountTokens * 10 ** mintDecimals); return createGlobalDepositInstruction({ payer: payer, global: globalAddress, mint: globalMint, globalVault: globalVault, traderToken: traderTokenAccount, tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : 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 = getGlobalAddress(globalMint); const globalVault = getGlobalVaultAddress(globalMint); const globalMintAccountInfo = (await connection.getAccountInfo(globalMint)); const mint = unpackMint(globalMint, globalMintAccountInfo, globalMintAccountInfo.owner); const is22 = mint.tlvData.length > 0; const traderTokenAccount = getAssociatedTokenAddressSync(globalMint, payer, true, is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID); const mintDecimals = mint.decimals; const amountAtoms = Math.ceil(amountTokens * 10 ** mintDecimals); return createGlobalWithdrawInstruction({ payer: payer, global: globalAddress, mint: globalMint, globalVault: globalVault, traderToken: traderTokenAccount, tokenProgram: is22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, }, { params: { amountAtoms, }, }); } } function toWrapperPlaceOrderParams(market, wrapperPlaceOrderParamsExternal) { // Convert spread bps to 10^-5. if ('spreadBps' in wrapperPlaceOrderParamsExternal) { wrapperPlaceOrderParamsExternal['lastValidSlot'] = Math.floor(wrapperPlaceOrderParamsExternal['spreadBps'] * 10); } else if (wrapperPlaceOrderParamsExternal['lastValidSlot'] < 100_000 && wrapperPlaceOrderParamsExternal['lastValidSlot'] != NO_EXPIRATION_LAST_VALID_SLOT) { // 100_000 is way earlier than the current slot. This check ensures that // users are intentionally choosing the right type. throw new Error('Last valid slot on order not valid'); } const quoteAtomsPerToken = 10 ** market.quoteDecimals(); const baseAtomsPerToken = 10 ** market.baseDecimals(); // Converts token price to atom price since not always equal // Ex. BONK/USDC = 0.00001854 USDC tokens/BONK tokens -> 0.0001854 USDC Atoms/BONK Atoms const priceQuoteAtomsPerBaseAtoms = wrapperPlaceOrderParamsExternal.tokenPrice * (quoteAtomsPerToken / baseAtomsPerToken); const { priceMantissa, priceExponent } = toMantissaAndExponent(priceQuoteAtomsPerBaseAtoms); const numBaseAtoms = Mat