UNPKG

@cks-systems/manifest-sdk

Version:
518 lines (517 loc) 19.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Market = void 0; const web3_js_1 = require("@solana/web3.js"); const beet_1 = require("./utils/beet"); const beet_solana_1 = require("@metaplex-foundation/beet-solana"); const redBlackTree_1 = require("./utils/redBlackTree"); const numbers_1 = require("./utils/numbers"); const constants_1 = require("./constants"); const manifest_1 = require("./manifest"); const market_1 = require("./utils/market"); const spl_token_1 = require("@solana/spl-token"); const bn_js_1 = __importDefault(require("bn.js")); /** * Market object used for reading data from a manifest market. */ class Market { /** Public key for the market account. */ address; /** Deserialized data. */ data; /** Last updated slot. */ slot; /** * Constructs a Market object. * * @param address The `PublicKey` of the market account * @param data Deserialized market data */ constructor({ address, data, slot, }) { this.address = address; this.data = data; this.slot = slot; } /** * Returns a `Market` for a given address, a data buffer * * @param marketAddress The `PublicKey` of the market account * @param buffer The buffer holding the market account data */ static loadFromBuffer({ address, buffer, slot, }) { const marketData = Market.deserializeMarketBuffer(buffer, slot ?? constants_1.NO_EXPIRATION_LAST_VALID_SLOT); // When we are not given a slot, pretend it is time zero to show everything. return new Market({ address, data: marketData, slot: slot ?? constants_1.NO_EXPIRATION_LAST_VALID_SLOT, }); } /** * Returns a `Market` for a given address, a data buffer * * @param connection The Solana `Connection` object * @param address The `PublicKey` of the market account */ static async loadFromAddress({ connection, address, }) { const [buffer, slot] = await connection .getAccountInfoAndContext(address) .then((getAccountInfoAndContext) => { return [ getAccountInfoAndContext.value?.data, getAccountInfoAndContext.context.slot, ]; }); if (buffer === undefined) { throw new Error(`Failed to load ${address}`); } return Market.loadFromBuffer({ address, buffer, slot }); } /** * Updates the data in a Market. * * @param connection The Solana `Connection` object */ async reload(connection) { const [buffer, slot] = await connection .getAccountInfoAndContext(this.address) .then((getAccountInfoAndContext) => { return [ getAccountInfoAndContext.value?.data, getAccountInfoAndContext.context.slot, ]; }); if (buffer === undefined) { throw new Error(`Failed to load ${this.address}`); } this.slot = slot; this.data = Market.deserializeMarketBuffer(buffer, slot); } /** * Get the amount in tokens of balance that is deposited on this market, does * not include tokens currently in open orders. * * @param trader PublicKey of the trader to check balance of * @param isBase boolean for whether this is checking base or quote * * @returns number in tokens */ getWithdrawableBalanceTokens(trader, isBase) { const filteredSeats = this.data.claimedSeats.filter((claimedSeat) => { return claimedSeat.publicKey.equals(trader); }); // No seat claimed. if (filteredSeats.length == 0) { return 0; } const seat = filteredSeats[0]; const withdrawableBalance = isBase ? (0, numbers_1.toNum)(seat.baseBalance) / 10 ** this.baseDecimals() : (0, numbers_1.toNum)(seat.quoteBalance) / 10 ** this.quoteDecimals(); return withdrawableBalance; } /** * Get the amount in tokens of balance that is deposited on this market, split * by base, quote, and whether in orders or not for the whole market. * * @returns { * baseWithdrawableBalanceAtoms: number, * quoteWithdrawableBalanceAtoms: number, * baseOpenOrdersBalanceAtoms: number, * quoteOpenOrdersBalanceAtoms: number * } */ getMarketBalances() { const asks = this.asks(); const bids = this.bids(); const quoteOpenOrdersBalanceAtoms = bids .filter((restingOrder) => { return restingOrder.orderType != manifest_1.OrderType.Global; }) .map((restingOrder) => { return Math.ceil(Number(restingOrder.numBaseTokens) * restingOrder.tokenPrice * 10 ** this.data.quoteMintDecimals - // Force float precision to not round up on an integer. 0.00001); }) .reduce((sum, current) => sum + current, 0); const baseOpenOrdersBalanceAtoms = asks .filter((restingOrder) => { return restingOrder.orderType != manifest_1.OrderType.Global; }) .map((restingOrder) => { return (Number(restingOrder.numBaseTokens) * 10 ** this.data.baseMintDecimals); }) .reduce((sum, current) => sum + current, 0); const quoteWithdrawableBalanceAtoms = this.data.claimedSeats .map((claimedSeat) => { return Number(claimedSeat.quoteBalance); }) .reduce((sum, current) => sum + current, 0); const baseWithdrawableBalanceAtoms = this.data.claimedSeats .map((claimedSeat) => { return Number(claimedSeat.baseBalance); }) .reduce((sum, current) => sum + current, 0); return { baseWithdrawableBalanceAtoms, quoteWithdrawableBalanceAtoms, baseOpenOrdersBalanceAtoms, quoteOpenOrdersBalanceAtoms, }; } /** * Get the amount in tokens of balance that is deposited on this market, split * by base, quote, and whether in orders or not. * * @param trader PublicKey of the trader to check balance of * * @returns { * baseWithdrawableBalanceTokens: number, * quoteWithdrawableBalanceTokens: number, * baseOpenOrdersBalanceTokens: number, * quoteOpenOrdersBalanceTokens: number * } */ getBalances(trader) { const filteredSeats = this.data.claimedSeats.filter((claimedSeat) => { return claimedSeat.publicKey.equals(trader); }); // No seat claimed. if (filteredSeats.length == 0) { return { baseWithdrawableBalanceTokens: 0, quoteWithdrawableBalanceTokens: 0, baseOpenOrdersBalanceTokens: 0, quoteOpenOrdersBalanceTokens: 0, }; } const seat = filteredSeats[0]; const asks = this.asks(); const bids = this.bids(); const baseOpenOrdersBalanceTokens = asks .filter((ask) => ask.trader.equals(trader)) .reduce((sum, ask) => sum + Number(ask.numBaseTokens), 0); const quoteOpenOrdersBalanceTokens = bids .filter((bid) => bid.trader.equals(trader)) .reduce((sum, bid) => sum + Number(bid.numBaseTokens) * Number(bid.tokenPrice), 0); const quoteWithdrawableBalanceTokens = (0, numbers_1.toNum)(seat.quoteBalance) / 10 ** this.quoteDecimals(); const baseWithdrawableBalanceTokens = (0, numbers_1.toNum)(seat.baseBalance) / 10 ** this.baseDecimals(); return { baseWithdrawableBalanceTokens, quoteWithdrawableBalanceTokens, baseOpenOrdersBalanceTokens, quoteOpenOrdersBalanceTokens, }; } /** * Gets the base mint of the market * * @returns PublicKey */ baseMint() { return this.data.baseMint; } /** * Gets the quote mint of the market * * @returns PublicKey */ quoteMint() { return this.data.quoteMint; } /** * Gets the base decimals of the market * * @returns number */ baseDecimals() { return this.data.baseMintDecimals; } /** * Gets the base decimals of the market * * @returns number */ quoteDecimals() { return this.data.quoteMintDecimals; } /** * Check whether a given public key has a claimed seat on the market * * @param trader PublicKey of the trader * * @returns boolean */ hasSeat(trader) { const filteredSeats = this.data.claimedSeats.filter((claimedSeat) => { return claimedSeat.publicKey.equals(trader); }); return filteredSeats.length > 0; } /** * Get all open bids on the market. * * @returns RestingOrder[] */ bids() { return this.data.bids; } /** * Get all open asks on the market. * * @returns RestingOrder[] */ asks() { return this.data.asks; } /** * Get the most competitive bid price * * @returns number | undefined */ bestBidPrice() { return this.data.bids[this.data.bids.length - 1]?.tokenPrice; } /** * Get the most competitive ask price. * * @returns number | undefined */ bestAskPrice() { return this.data.asks[this.data.asks.length - 1]?.tokenPrice; } /** * Get all open bids on the market ordered from most competitive to least. * * @returns RestingOrder[] */ bidsL2() { return this.data.bids.slice().reverse(); } /** * Get all open asks on the market ordered from most competitive to least. * * @returns RestingOrder[] */ asksL2() { return this.data.asks.slice().reverse(); } /** * Get all open orders on the market. * * @returns RestingOrder[] */ openOrders() { return [...this.data.bids, ...this.data.asks]; } /** * Gets the quote volume traded over the lifetime of the market. * * @returns bigint */ quoteVolume() { return this.data.quoteVolumeAtoms; } /** * Print all information loaded about the market in a human readable format. */ prettyPrint() { console.log(''); console.log(`Market: ${this.address}`); console.log(`========================`); console.log(`Version: ${this.data.version}`); console.log(`BaseMint: ${this.data.baseMint.toBase58()}`); console.log(`QuoteMint: ${this.data.quoteMint.toBase58()}`); console.log(`OrderSequenceNumber: ${this.data.orderSequenceNumber}`); console.log(`NumBytesAllocated: ${this.data.numBytesAllocated}`); console.log('Bids:'); this.data.bids.forEach((bid) => { console.log(`trader: ${bid.trader} numBaseTokens: ${bid.numBaseTokens} token price: ${bid.tokenPrice} lastValidSlot: ${bid.lastValidSlot} sequenceNumber: ${bid.sequenceNumber}`); }); console.log('Asks:'); this.data.asks.forEach((ask) => { console.log(`trader: ${ask.trader} numBaseTokens: ${ask.numBaseTokens} token price: ${ask.tokenPrice} lastValidSlot: ${ask.lastValidSlot} sequenceNumber: ${ask.sequenceNumber}`); }); console.log('ClaimedSeats:'); this.data.claimedSeats.forEach((claimedSeat) => { console.log(`publicKey: ${claimedSeat.publicKey.toBase58()} baseBalance: ${claimedSeat.baseBalance} quoteBalance: ${claimedSeat.quoteBalance}`); }); console.log(`========================`); } /** * Deserializes market data from a given buffer and returns a `Market` object * * This includes both the fixed and dynamic parts of the market. * https://github.com/CKS-Systems/manifest/blob/main/programs/manifest/src/state/market.rs * * @param data The data buffer to deserialize * @param currentSlot Number that is the cutoff for order expiration. */ static deserializeMarketBuffer(data, currentSlot) { let offset = 0; // Deserialize the market header const _discriminant = data.readBigUInt64LE(0); offset += 8; const version = data.readUInt8(offset); offset += 1; const baseMintDecimals = data.readUInt8(offset); offset += 1; const quoteMintDecimals = data.readUInt8(offset); offset += 1; const _baseVaultBump = data.readUInt8(offset); offset += 1; const _quoteVaultBump = data.readUInt8(offset); offset += 1; // 3 bytes of unused padding. offset += 3; const baseMint = beet_solana_1.publicKey.read(data, offset); offset += beet_solana_1.publicKey.byteSize; const quoteMint = beet_solana_1.publicKey.read(data, offset); offset += beet_solana_1.publicKey.byteSize; const _baseVault = beet_solana_1.publicKey.read(data, offset); offset += beet_solana_1.publicKey.byteSize; const _quoteVault = beet_solana_1.publicKey.read(data, offset); offset += beet_solana_1.publicKey.byteSize; const orderSequenceNumber = data.readBigUInt64LE(offset); offset += 8; const numBytesAllocated = data.readUInt32LE(offset); offset += 4; const bidsRootIndex = data.readUInt32LE(offset); offset += 4; const _bidsBestIndex = data.readUInt32LE(offset); offset += 4; const asksRootIndex = data.readUInt32LE(offset); offset += 4; const _askBestIndex = data.readUInt32LE(offset); offset += 4; const claimedSeatsRootIndex = data.readUInt32LE(offset); offset += 4; const _freeListHeadIndex = data.readUInt32LE(offset); offset += 4; const _padding2 = data.readUInt32LE(offset); offset += 4; const quoteVolumeAtoms = data.readBigUInt64LE(offset); offset += 8; // _padding3: [u64; 8], const bids = bidsRootIndex != constants_1.NIL ? (0, redBlackTree_1.deserializeRedBlackTree)(data.subarray(constants_1.FIXED_MANIFEST_HEADER_SIZE), bidsRootIndex, manifest_1.restingOrderBeet) .map((restingOrderInternal) => { return { trader: beet_1.publicKeyBeet.deserialize(data.subarray(Number(restingOrderInternal.traderIndex) + 16 + constants_1.FIXED_MANIFEST_HEADER_SIZE, Number(restingOrderInternal.traderIndex) + 48 + constants_1.FIXED_MANIFEST_HEADER_SIZE))[0].publicKey, numBaseTokens: (0, numbers_1.toNum)(restingOrderInternal.numBaseAtoms) / 10 ** baseMintDecimals, tokenPrice: (0, numbers_1.convertU128)(restingOrderInternal.price) * 10 ** (baseMintDecimals - quoteMintDecimals), ...restingOrderInternal, }; }) .filter((bid) => { return (bid.lastValidSlot == constants_1.NO_EXPIRATION_LAST_VALID_SLOT || Number(bid.lastValidSlot) > currentSlot); }) : []; const asks = asksRootIndex != constants_1.NIL ? (0, redBlackTree_1.deserializeRedBlackTree)(data.subarray(constants_1.FIXED_MANIFEST_HEADER_SIZE), asksRootIndex, manifest_1.restingOrderBeet) .map((restingOrderInternal) => { return { trader: beet_1.publicKeyBeet.deserialize(data.subarray(Number(restingOrderInternal.traderIndex) + 16 + constants_1.FIXED_MANIFEST_HEADER_SIZE, Number(restingOrderInternal.traderIndex) + 48 + constants_1.FIXED_MANIFEST_HEADER_SIZE))[0].publicKey, numBaseTokens: (0, numbers_1.toNum)(restingOrderInternal.numBaseAtoms) / 10 ** baseMintDecimals, tokenPrice: (0, numbers_1.convertU128)(restingOrderInternal.price) * 10 ** (baseMintDecimals - quoteMintDecimals), ...restingOrderInternal, }; }) .filter((ask) => { return (ask.lastValidSlot == constants_1.NO_EXPIRATION_LAST_VALID_SLOT || Number(ask.lastValidSlot) > currentSlot); }) : []; const claimedSeats = claimedSeatsRootIndex != constants_1.NIL ? (0, redBlackTree_1.deserializeRedBlackTree)(data.subarray(constants_1.FIXED_MANIFEST_HEADER_SIZE), claimedSeatsRootIndex, manifest_1.claimedSeatBeet).map((claimedSeatInternal) => { return { publicKey: claimedSeatInternal.trader, baseBalance: claimedSeatInternal.baseWithdrawableBalance, quoteBalance: claimedSeatInternal.quoteWithdrawableBalance, }; }) : []; return { version, baseMintDecimals, quoteMintDecimals, baseMint, quoteMint, orderSequenceNumber, numBytesAllocated, bids, asks, claimedSeats, quoteVolumeAtoms, }; } static async findByMints(connection, baseMint, quoteMint) { // Based on the MarketFixed struct const baseMintOffset = 16; const quoteMintOffset = 48; const filters = [ { memcmp: { offset: baseMintOffset, bytes: baseMint.toBase58(), }, }, { memcmp: { offset: quoteMintOffset, bytes: quoteMint.toBase58(), }, }, ]; const accounts = await connection.getProgramAccounts(manifest_1.PROGRAM_ID, { filters, }); return accounts .map(({ account, pubkey }) => Market.loadFromBuffer({ address: pubkey, buffer: account.data })) .sort((a, b) => new bn_js_1.default(b.quoteVolume().toString()) .sub(new bn_js_1.default(a.quoteVolume().toString())) .toNumber()); } static async setupIxs(connection, baseMint, quoteMint, payer) { const marketKeypair = web3_js_1.Keypair.generate(); const createAccountIx = web3_js_1.SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: marketKeypair.publicKey, space: constants_1.FIXED_MANIFEST_HEADER_SIZE, lamports: await connection.getMinimumBalanceForRentExemption(constants_1.FIXED_MANIFEST_HEADER_SIZE), programId: manifest_1.PROGRAM_ID, }); const market = marketKeypair.publicKey; const baseVault = (0, market_1.getVaultAddress)(market, baseMint); const quoteVault = (0, market_1.getVaultAddress)(market, quoteMint); const createMarketIx = (0, manifest_1.createCreateMarketInstruction)({ payer, baseMint, quoteMint, market, baseVault, quoteVault, tokenProgram22: spl_token_1.TOKEN_2022_PROGRAM_ID, }); return { ixs: [createAccountIx, createMarketIx], signers: [marketKeypair] }; } } exports.Market = Market;