UNPKG

@cks-systems/manifest-sdk

Version:
411 lines (410 loc) 17.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UiWrapper = void 0; const beet_solana_1 = require("@metaplex-foundation/beet-solana"); const web3_js_1 = require("@solana/web3.js"); const ui_wrapper_1 = require("./ui_wrapper"); const redBlackTree_1 = require("./utils/redBlackTree"); const constants_1 = require("./constants"); const manifest_1 = require("./manifest"); const market_1 = require("./market"); const market_2 = require("./utils/market"); const spl_token_1 = require("@solana/spl-token"); const numbers_1 = require("./utils/numbers"); const bn_js_1 = require("bn.js"); const global_1 = require("./utils/global"); const types_1 = require("./ui_wrapper/types"); /** * Wrapper object used for reading data from a wrapper for manifest markets. */ class UiWrapper { /** Public key for the market account. */ address; /** Deserialized data. */ data; /** * Constructs a Wrapper object. * * @param address The `PublicKey` of the wrapper account * @param data Deserialized wrapper data */ constructor({ address, data, }) { this.address = address; this.data = data; } /** * Returns a `Wrapper` for a given address, a data buffer * * @param marketAddress The `PublicKey` of the wrapper account * @param buffer The buffer holding the wrapper account data */ static loadFromBuffer({ address, buffer, }) { const wrapperData = UiWrapper.deserializeWrapperBuffer(buffer); return new UiWrapper({ address, data: wrapperData }); } /** * Updates the data in a Wrapper. * * @param connection The Solana `Connection` object */ async reload(connection) { const buffer = await connection .getAccountInfo(this.address) .then((accountInfo) => accountInfo?.data); if (buffer === undefined) { throw new Error(`Failed to load ${this.address}`); } this.data = UiWrapper.deserializeWrapperBuffer(buffer); } /** * Get the parsed market info from the wrapper. * * @param marketPk PublicKey for the market * * @return MarketInfoParsed */ marketInfoForMarket(marketPk) { const filtered = this.data.marketInfos.filter((marketInfo) => { return marketInfo.market.equals(marketPk); }); if (filtered.length == 0) { return null; } return filtered[0]; } /** * Get the open orders from the wrapper. * * @param marketPk PublicKey for the market * * @return OpenOrder[] */ openOrdersForMarket(marketPk) { const filtered = this.data.marketInfos.filter((marketInfo) => { return marketInfo.market.equals(marketPk); }); if (filtered.length == 0) { return null; } return filtered[0].orders; } activeMarkets() { return this.data.marketInfos.map((mi) => mi.market); } unsettledBalances(markets) { const { owner } = this.data; return markets.map((market) => { const numBaseTokens = market.getWithdrawableBalanceTokens(owner, true); const numQuoteTokens = market.getWithdrawableBalanceTokens(owner, false); return { market, numBaseTokens, numQuoteTokens }; }); } settleIx(market, accounts, params) { const { owner } = this.data; const mintBase = market.baseMint(); const mintQuote = market.quoteMint(); const traderTokenAccountBase = (0, spl_token_1.getAssociatedTokenAddressSync)(mintBase, owner); const traderTokenAccountQuote = (0, spl_token_1.getAssociatedTokenAddressSync)(mintQuote, owner); const vaultBase = (0, market_2.getVaultAddress)(market.address, mintBase); const vaultQuote = (0, market_2.getVaultAddress)(market.address, mintQuote); return (0, ui_wrapper_1.createSettleFundsInstruction)({ wrapperState: this.address, owner, traderTokenAccountBase, traderTokenAccountQuote, market: market.address, vaultBase, vaultQuote, mintBase, mintQuote, tokenProgramBase: accounts.baseTokenProgram || spl_token_1.TOKEN_PROGRAM_ID, tokenProgramQuote: accounts.quoteTokenProgram || spl_token_1.TOKEN_PROGRAM_ID, manifestProgram: manifest_1.PROGRAM_ID, platformTokenAccount: accounts.platformTokenAccount, referrerTokenAccount: accounts.referrerTokenAccount, }, params); } // Do not include getters for the balances because those can be retrieved from // the market and that will be fresher data or the same always. /** * Print all information loaded about the wrapper in a human readable format. */ prettyPrint() { console.log(''); console.log(`Wrapper: ${this.address.toBase58()}`); console.log(`========================`); console.log(`Owner: ${this.data.owner.toBase58()}`); this.data.marketInfos.forEach((marketInfo) => { console.log(`------------------------`); console.log(`Market: ${marketInfo.market}`); console.log(`Last updated slot: ${marketInfo.lastUpdatedSlot}`); console.log(`BaseAtoms: ${marketInfo.baseBalanceAtoms} QuoteAtoms: ${marketInfo.quoteBalanceAtoms}`); marketInfo.orders.forEach((order) => { console.log(`OpenOrder: ClientOrderId: ${order.clientOrderId} ${order.numBaseAtoms}@${order.price} SeqNum: ${order.orderSequenceNumber} LastValidSlot: ${order.lastValidSlot} IsBid: ${order.isBid}`); }); }); console.log(`------------------------`); } /** * Deserializes wrapper data from a given buffer and returns a `Wrapper` object * * This includes both the fixed and dynamic parts of the market. * https://github.com/CKS-Systems/manifest/blob/main/programs/wrapper/src/wrapper_state.rs * * @param data The data buffer to deserialize * * @returns WrapperData */ static deserializeWrapperBuffer(data) { let offset = 0; // Deserialize the market header const _discriminant = data.readBigUInt64LE(0); offset += 8; const owner = beet_solana_1.publicKey.read(data, offset); offset += beet_solana_1.publicKey.byteSize; const _numBytesAllocated = data.readUInt32LE(offset); offset += 4; const _freeListHeadIndex = data.readUInt32LE(offset); offset += 4; const marketInfosRootIndex = data.readUInt32LE(offset); offset += 4; const _padding = data.readUInt32LE(offset); offset += 12; const marketInfos = marketInfosRootIndex != constants_1.NIL ? (0, redBlackTree_1.deserializeRedBlackTree)(data.subarray(constants_1.FIXED_WRAPPER_HEADER_SIZE), marketInfosRootIndex, types_1.marketInfoBeet) : []; const parsedMarketInfos = marketInfos.map((marketInfoRaw) => { const rootIndex = marketInfoRaw.ordersRootIndex; const rawOpenOrders = rootIndex != constants_1.NIL ? (0, redBlackTree_1.deserializeRedBlackTree)(data.subarray(constants_1.FIXED_WRAPPER_HEADER_SIZE), rootIndex, ui_wrapper_1.wrapperOpenOrderBeet) : []; const parsedOpenOrdersWithPrice = rawOpenOrders.map((openOrder) => { return { ...openOrder, dataIndex: openOrder.marketDataIndex, price: (0, numbers_1.convertU128)(new bn_js_1.BN(openOrder.price, 10, 'le')), }; }); return { market: marketInfoRaw.market, baseBalanceAtoms: marketInfoRaw.baseBalance, quoteBalanceAtoms: marketInfoRaw.quoteBalance, orders: parsedOpenOrdersWithPrice, lastUpdatedSlot: marketInfoRaw.lastUpdatedSlot, }; }); return { owner, marketInfos: parsedMarketInfos, }; } placeOrderIx(market, accounts, args) { const { owner } = this.data; const payer = accounts.payer ?? owner; const { isBid } = args; const mint = isBid ? market.quoteMint() : market.baseMint(); const traderTokenProgram = isBid ? accounts.quoteTokenProgram : accounts.baseTokenProgram; const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, owner, true, traderTokenProgram); const vault = (0, market_2.getVaultAddress)(market.address, mint); const clientOrderId = args.orderId ?? Date.now(); const baseAtoms = Math.round(args.amount * 10 ** market.baseDecimals()); let priceMantissa = args.price; let priceExponent = market.quoteDecimals() - market.baseDecimals(); while (priceMantissa < constants_1.U32_MAX / 10 && priceExponent > constants_1.PRICE_MIN_EXP && Math.round(priceMantissa) != priceMantissa) { priceMantissa *= 10; priceExponent -= 1; } while (priceMantissa > constants_1.U32_MAX && priceExponent < constants_1.PRICE_MAX_EXP) { priceMantissa = priceMantissa / 10; priceExponent += 1; } priceMantissa = Math.round(priceMantissa); const baseGlobal = (0, global_1.getGlobalAddress)(market.baseMint()); const quoteGlobal = (0, global_1.getGlobalAddress)(market.quoteMint()); const baseGlobalVault = (0, global_1.getGlobalVaultAddress)(market.baseMint()); const quoteGlobalVault = (0, global_1.getGlobalVaultAddress)(market.quoteMint()); return (0, ui_wrapper_1.createPlaceOrderInstruction)({ wrapperState: this.address, owner, traderTokenAccount, market: market.address, vault, mint, manifestProgram: manifest_1.PROGRAM_ID, payer, tokenProgram: traderTokenProgram, baseMint: market.baseMint(), baseGlobal, baseGlobalVault, baseMarketVault: (0, market_2.getVaultAddress)(market.address, market.baseMint()), baseTokenProgram: accounts.baseTokenProgram || spl_token_1.TOKEN_PROGRAM_ID, quoteMint: market.quoteMint(), quoteGlobal, quoteGlobalVault, quoteMarketVault: (0, market_2.getVaultAddress)(market.address, market.quoteMint()), quoteTokenProgram: accounts.quoteTokenProgram || spl_token_1.TOKEN_PROGRAM_ID, }, { params: { clientOrderId, baseAtoms, priceMantissa, priceExponent, isBid, lastValidSlot: constants_1.NO_EXPIRATION_LAST_VALID_SLOT, orderType: ui_wrapper_1.OrderType.Limit, }, }); } static async fetchFirstUserWrapper(connection, payer) { const existingWrappers = await connection.getProgramAccounts(ui_wrapper_1.PROGRAM_ID, { filters: [ // Dont check discriminant since there is only one type of account. { memcmp: { offset: 8, encoding: 'base58', bytes: payer.toBase58(), }, }, ], }); return existingWrappers.length > 0 ? existingWrappers[0] : null; } static async placeOrderCreateIfNotExistsIxs(connection, baseMint, baseDecimals, quoteMint, quoteDecimals, owner, payer, args, baseTokenProgram = spl_token_1.TOKEN_PROGRAM_ID, quoteTokenProgram = spl_token_1.TOKEN_PROGRAM_ID) { const ixs = []; const signers = []; const [markets, wrapper] = await Promise.all([ market_1.Market.findByMints(connection, baseMint, quoteMint), UiWrapper.fetchFirstUserWrapper(connection, owner), ]); let market = markets.length > 0 ? markets[0] : null; let wrapperPk = wrapper?.pubkey; if (!market) { const marketIxs = await market_1.Market.setupIxs(connection, baseMint, quoteMint, payer); market = { address: marketIxs.signers[0].publicKey, baseMint: () => baseMint, quoteMint: () => quoteMint, baseDecimals: () => baseDecimals, quoteDecimals: () => quoteDecimals, }; ixs.push(...marketIxs.ixs); signers.push(...marketIxs.signers); } if (!wrapper) { const setup = await this.setupIxs(connection, owner, payer); wrapperPk = setup.signers[0].publicKey; ixs.push(...setup.ixs); signers.push(...setup.signers); } if (wrapper) { const wrapperParsed = UiWrapper.loadFromBuffer({ address: wrapper.pubkey, buffer: wrapper.account.data, }); const placeIx = wrapperParsed.placeOrderIx(market, { payer, baseTokenProgram, quoteTokenProgram }, args); ixs.push(placeIx); } else { const placeIx = await this.placeIx_(market, { wrapper: wrapperPk, owner, payer, baseTokenProgram, quoteTokenProgram, }, args); ixs.push(...placeIx.ixs); signers.push(...placeIx.signers); } return { ixs, signers, }; } static async setupIxs(connection, owner, payer) { const wrapperKeypair = web3_js_1.Keypair.generate(); const createAccountIx = web3_js_1.SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: wrapperKeypair.publicKey, space: constants_1.FIXED_WRAPPER_HEADER_SIZE, lamports: await connection.getMinimumBalanceForRentExemption(constants_1.FIXED_WRAPPER_HEADER_SIZE), programId: ui_wrapper_1.PROGRAM_ID, }); const createWrapperIx = (0, ui_wrapper_1.createCreateWrapperInstruction)({ payer, owner, wrapperState: wrapperKeypair.publicKey, }); return { ixs: [createAccountIx, createWrapperIx], signers: [wrapperKeypair], }; } static placeIx_(market, accounts, args) { const { isBid } = args; const mint = isBid ? market.quoteMint() : market.baseMint(); const traderTokenProgram = isBid ? accounts.quoteTokenProgram : accounts.baseTokenProgram; const traderTokenAccount = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, accounts.owner, true, traderTokenProgram); const vault = (0, market_2.getVaultAddress)(market.address, mint); const clientOrderId = args.orderId ?? Date.now(); const baseAtoms = Math.round(args.amount * 10 ** market.baseDecimals()); let priceMantissa = args.price; let priceExponent = market.quoteDecimals() - market.baseDecimals(); while (priceMantissa < constants_1.U32_MAX / 10 && priceExponent > constants_1.PRICE_MIN_EXP && Math.round(priceMantissa) != priceMantissa) { priceMantissa *= 10; priceExponent -= 1; } while (priceMantissa > constants_1.U32_MAX && priceExponent < constants_1.PRICE_MAX_EXP) { priceMantissa = priceMantissa / 10; priceExponent += 1; } priceMantissa = Math.round(priceMantissa); const baseMarketVault = (0, market_2.getVaultAddress)(market.address, market.baseMint()); const quoteMarketVault = (0, market_2.getVaultAddress)(market.address, market.quoteMint()); const baseGlobal = (0, global_1.getGlobalAddress)(market.baseMint()); const quoteGlobal = (0, global_1.getGlobalAddress)(market.quoteMint()); const baseGlobalVault = (0, global_1.getGlobalVaultAddress)(market.baseMint()); const quoteGlobalVault = (0, global_1.getGlobalVaultAddress)(market.quoteMint()); const placeIx = (0, ui_wrapper_1.createPlaceOrderInstruction)({ wrapperState: accounts.wrapper, owner: accounts.owner, traderTokenAccount, market: market.address, vault, mint, manifestProgram: manifest_1.PROGRAM_ID, payer: accounts.payer, baseMint: market.baseMint(), baseGlobal, baseGlobalVault, baseMarketVault, tokenProgram: traderTokenProgram, baseTokenProgram: accounts.baseTokenProgram || spl_token_1.TOKEN_PROGRAM_ID, quoteMint: market.quoteMint(), quoteGlobal, quoteGlobalVault, quoteMarketVault, quoteTokenProgram: accounts.quoteTokenProgram || spl_token_1.TOKEN_PROGRAM_ID, }, { params: { clientOrderId, baseAtoms, priceMantissa, priceExponent, isBid, lastValidSlot: constants_1.NO_EXPIRATION_LAST_VALID_SLOT, orderType: ui_wrapper_1.OrderType.Limit, }, }); return { ixs: [placeIx], signers: [] }; } } exports.UiWrapper = UiWrapper;