@cks-systems/manifest-sdk
Version:
TypeScript SDK for Manifest
518 lines (517 loc) • 19.9 kB
JavaScript
;
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;