diff --git a/.github/workflows/ci-code-review-rust.yml b/.github/workflows/ci-code-review-rust.yml index a612a0b50..0d853e6e7 100644 --- a/.github/workflows/ci-code-review-rust.yml +++ b/.github/workflows/ci-code-review-rust.yml @@ -128,7 +128,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: '16' + node-version: '20' cache: 'yarn' - name: Install dependencies diff --git a/.github/workflows/ci-code-review-ts.yml b/.github/workflows/ci-code-review-ts.yml index 8ae536cb8..0020763de 100644 --- a/.github/workflows/ci-code-review-ts.yml +++ b/.github/workflows/ci-code-review-ts.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: '16' + node-version: '20' cache: 'yarn' - name: Install dependencies @@ -37,7 +37,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: '16' + node-version: '20' cache: 'yarn' - name: Install dependencies @@ -56,7 +56,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: '16' + node-version: '20' cache: 'yarn' - name: Duplicates check @@ -72,7 +72,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: '16' + node-version: '20' cache: 'yarn' - name: Install dependencies diff --git a/package.json b/package.json index 4ed4d94e4..ddbd6f36a 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@blockworks-foundation/mango-v4-settings": "0.14.15", "@blockworks-foundation/mangolana": "0.0.15", "@coral-xyz/anchor": "^0.29.0", - "@openbook-dex/openbook-v2": "git+https://github.com/openbook-dex/openbook-v2#pan/ts-client-anchor-0.29.0", + "@openbook-dex/openbook-v2": "git+https://github.com/openbook-dex/openbook-v2", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "~2.14.0", "@solana/spl-token": "0.3.7", diff --git a/ts/client/src/accounts/bookSide.ts b/ts/client/src/accounts/bookSide.ts new file mode 100644 index 000000000..ca551e693 --- /dev/null +++ b/ts/client/src/accounts/bookSide.ts @@ -0,0 +1,499 @@ +import BN from 'bn.js'; +import { MangoClient, PerpMarket, RUST_U64_MAX, U64_MAX_BN } from '..'; +import { PublicKey } from '@solana/web3.js'; + +interface BookSideAccount { + roots: OrderTreeRoot[]; + nodes: OrderTreeNodes; +} + +interface OrderTreeNodes { + bumpIndex: number; + freeListLen: number; + freeListHead: number; + nodes: AnyNode[]; +} + +interface AnyNode { + tag: number; + data?: number[]; + nodeData?: Buffer; +} + +interface OrderTreeRoot { + maybeNode: number; + leafCount: number; +} + +function decodeOrderTreeRootStruct(data: Buffer): OrderTreeRoot { + const maybeNode = data.readUInt32LE(0); + const leafCount = data.readUInt32LE(4); + return { maybeNode, leafCount }; +} + +export class BookSide { + private static INNER_NODE_TAG = 1; + private static LEAF_NODE_TAG = 2; + now: BN; + + static from( + client: MangoClient, + perpMarket: PerpMarket, + bookSideType: BookSideType, + account: BookSideAccount, + ): BookSide { + return new BookSide(client, perpMarket, bookSideType, account); + } + + static decodeAccountfromBuffer(data: Buffer): BookSideAccount { + // TODO: add discriminator parsing & check + const roots = [ + decodeOrderTreeRootStruct(data.subarray(8)), + decodeOrderTreeRootStruct(data.subarray(16)), + ]; + + // skip reserved + let offset = 56 + 256; + + const orderTreeType = data.readUInt8(offset); + const bumpIndex = data.readUInt32LE(offset + 4); + const freeListLen = data.readUInt32LE(offset + 8); + const freeListHead = data.readUInt32LE(offset + 12); + + // skip more reserved data + offset += 16 + 512; + + const nodes: { tag: number; nodeData: Buffer }[] = []; + for (let i = 0; i < 1024; ++i) { + const tag = data.readUInt8(offset); + const nodeData = data.subarray(offset, offset + 88); + nodes.push({ tag, nodeData }); + offset += 88; + } + + // this result has a slightly different layout than the regular account + // it doesn't include reserved data and it's AnyNodes don't have the field + // data: number[] (excluding the tag prefix byte) + // but nodeData: Buffer (including the tag prefix byte) + const result = { + roots, + nodes: { orderTreeType, bumpIndex, freeListLen, freeListHead, nodes }, + }; + + return result; + } + + constructor( + public client: MangoClient, + public perpMarket: PerpMarket, + public type: BookSideType, + public account: BookSideAccount, + maxBookDelay?: number, + ) { + // Determine the maxTimestamp found on the book to use for tif + // If maxBookDelay is not provided, use 3600 as a very large number + maxBookDelay = maxBookDelay === undefined ? 3600 : maxBookDelay; + let maxTimestamp = new BN(new Date().getTime() / 1000 - maxBookDelay); + + for (const node of account.nodes.nodes) { + if (node.tag !== BookSide.LEAF_NODE_TAG) { + continue; + } + + const leafNode = BookSide.toLeafNode(client, node); + if (leafNode.timestamp.gt(maxTimestamp)) { + maxTimestamp = leafNode.timestamp; + } + } + + this.now = maxTimestamp; + } + + static getPriceFromKey(key: BN): BN { + return key.ushrn(64); + } + + /** + * iterates over all orders + */ + public *items(): Generator { + function isBetter(type: BookSideType, a: PerpOrder, b: PerpOrder): boolean { + return a.priceLots.eq(b.priceLots) + ? a.seqNum.lt(b.seqNum) // if prices are equal prefer perp orders in the order they are placed + : type === BookSideType.bids // else compare the actual prices + ? a.priceLots.gt(b.priceLots) + : b.priceLots.gt(a.priceLots); + } + + const fGen = this.fixedItems(); + const oPegGen = this.oraclePeggedItems(); + + let fOrderRes = fGen.next(); + let oPegOrderRes = oPegGen.next(); + + while (true) { + if (fOrderRes.value && oPegOrderRes.value) { + if (isBetter(this.type, fOrderRes.value, oPegOrderRes.value)) { + yield fOrderRes.value; + fOrderRes = fGen.next(); + } else { + yield oPegOrderRes.value; + oPegOrderRes = oPegGen.next(); + } + } else if (fOrderRes.value && !oPegOrderRes.value) { + yield fOrderRes.value; + fOrderRes = fGen.next(); + } else if (!fOrderRes.value && oPegOrderRes.value) { + yield oPegOrderRes.value; + oPegOrderRes = oPegGen.next(); + } else if (!fOrderRes.value && !oPegOrderRes.value) { + break; + } + } + } + + /** + * iterates over all orders, + * skips oracle pegged orders which are invalid due to oracle price crossing the peg limit, + * skips tif orders which are invalid due to tif having elapsed, + */ + public *itemsValid(): Generator { + const itemsGen = this.items(); + let itemsRes = itemsGen.next(); + while (true) { + if (itemsRes.value) { + const val = itemsRes.value; + if ( + !val.isExpired && + (!val.isOraclePegged || + (val.isOraclePegged && !val.oraclePeggedProperties.isInvalid)) + ) { + yield val; + } + itemsRes = itemsGen.next(); + } else { + break; + } + } + } + + public *fixedItems(): Generator { + if (this.rootFixed.leafCount === 0) { + return; + } + const now = this.now; + const stack = [this.rootFixed.maybeNode]; + const [left, right] = this.type === BookSideType.bids ? [1, 0] : [0, 1]; + + while (stack.length > 0) { + const index = stack.pop()!; + const node = this.account.nodes.nodes[index]; + if (node.tag === BookSide.INNER_NODE_TAG) { + const innerNode = BookSide.toInnerNode(this.client, node); + stack.push(innerNode.children[right], innerNode.children[left]); + } else if (node.tag === BookSide.LEAF_NODE_TAG) { + const leafNode = BookSide.toLeafNode(this.client, node); + const expiryTimestamp = leafNode.timeInForce + ? leafNode.timestamp.add(new BN(leafNode.timeInForce)) + : U64_MAX_BN; + + yield PerpOrder.from( + this.perpMarket, + leafNode, + this.type, + now.gt(expiryTimestamp), + ); + } + } + } + + public *oraclePeggedItems(): Generator { + if (this.rootOraclePegged.leafCount === 0) { + return; + } + const now = this.now; + const stack = [this.rootOraclePegged.maybeNode]; + const [left, right] = this.type === BookSideType.bids ? [1, 0] : [0, 1]; + + while (stack.length > 0) { + const index = stack.pop()!; + const node = this.account.nodes.nodes[index]; + if (node.tag === BookSide.INNER_NODE_TAG) { + const innerNode = BookSide.toInnerNode(this.client, node); + stack.push(innerNode.children[right], innerNode.children[left]); + } else if (node.tag === BookSide.LEAF_NODE_TAG) { + const leafNode = BookSide.toLeafNode(this.client, node); + const expiryTimestamp = leafNode.timeInForce + ? leafNode.timestamp.add(new BN(leafNode.timeInForce)) + : U64_MAX_BN; + + yield PerpOrder.from( + this.perpMarket, + leafNode, + this.type, + now.gt(expiryTimestamp), + true, + ); + } + } + } + + public best(): PerpOrder | undefined { + return this.items().next().value; + } + + getImpactPriceUi(baseLots: BN): number | undefined { + const s = new BN(0); + for (const order of this.items()) { + s.iadd(order.sizeLots); + if (s.gte(baseLots)) { + return order.uiPrice; + } + } + return undefined; + } + + public getL2(depth: number): [number, number, BN, BN][] { + const levels: [BN, BN][] = []; + for (const { priceLots, sizeLots } of this.items()) { + if (levels.length > 0 && levels[levels.length - 1][0].eq(priceLots)) { + levels[levels.length - 1][1].iadd(sizeLots); + } else if (levels.length === depth) { + break; + } else { + levels.push([priceLots, sizeLots]); + } + } + return levels.map(([priceLots, sizeLots]) => [ + this.perpMarket.priceLotsToUi(priceLots), + this.perpMarket.baseLotsToUi(sizeLots), + priceLots, + sizeLots, + ]); + } + + public getL2Ui(depth: number): [number, number][] { + const levels: [number, number][] = []; + for (const { uiPrice: price, uiSize: size } of this.items()) { + if (levels.length > 0 && levels[levels.length - 1][0] === price) { + levels[levels.length - 1][1] += size; + } else if (levels.length === depth) { + break; + } else { + levels.push([price, size]); + } + } + return levels; + } + + get rootFixed(): OrderTreeRoot { + return this.account.roots[0]; + } + get rootOraclePegged(): OrderTreeRoot { + return this.account.roots[1]; + } + + static toInnerNode(client: MangoClient, node: AnyNode): InnerNode { + const layout = (client.program as any)._coder.types.typeLayouts.get( + 'InnerNode', + ); + if (node.nodeData) { + return layout.decode(node.nodeData); + } + return layout.decode( + Buffer.from([BookSide.INNER_NODE_TAG].concat(node.data!)), + ); + } + + static toLeafNode(client: MangoClient, node: AnyNode): LeafNode { + const layout = (client.program as any)._coder.types.typeLayouts.get( + 'LeafNode', + ); + if (node.nodeData) { + return layout.decode(node.nodeData); + } + return layout.decode( + Buffer.from([BookSide.LEAF_NODE_TAG].concat(node.data!)), + ); + } +} + +export type BookSideType = + | { bids: Record } + | { asks: Record }; +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace BookSideType { + export const bids = { bids: {} }; + export const asks = { asks: {} }; +} + +export class LeafNode { + static from(obj: { + ownerSlot: number; + orderType: PerpOrderType; + timeInForce: number; + key: BN; + owner: PublicKey; + quantity: BN; + timestamp: BN; + pegLimit: BN; + }): LeafNode { + return new LeafNode( + obj.ownerSlot, + obj.orderType, + obj.timeInForce, + obj.key, + obj.owner, + obj.quantity, + obj.timestamp, + obj.pegLimit, + ); + } + + constructor( + public ownerSlot: number, + public orderType: PerpOrderType, + public timeInForce: number, + public key: BN, + public owner: PublicKey, + public quantity: BN, + public timestamp: BN, + public pegLimit: BN, + ) {} +} +export class InnerNode { + static from(obj: { children: [number] }): InnerNode { + return new InnerNode(obj.children); + } + + constructor(public children: [number]) {} +} + +export type PerpSelfTradeBehavior = + | { decrementTake: Record } + | { cancelProvide: Record } + | { abortTransaction: Record }; +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PerpSelfTradeBehavior { + export const decrementTake = { decrementTake: {} }; + export const cancelProvide = { cancelProvide: {} }; + export const abortTransaction = { abortTransaction: {} }; +} + +export type PerpOrderSide = + | { bid: Record } + | { ask: Record }; +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PerpOrderSide { + export const bid = { bid: {} }; + export const ask = { ask: {} }; +} + +export type PerpOrderType = + | { limit: Record } + | { immediateOrCancel: Record } + | { postOnly: Record } + | { market: Record } + | { postOnlySlide: Record }; +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PerpOrderType { + export const limit = { limit: {} }; + export const immediateOrCancel = { immediateOrCancel: {} }; + export const postOnly = { postOnly: {} }; + export const market = { market: {} }; + export const postOnlySlide = { postOnlySlide: {} }; +} + +export class PerpOrder { + static from( + perpMarket: PerpMarket, + leafNode: LeafNode, + type: BookSideType, + isExpired = false, + isOraclePegged = false, + ): PerpOrder { + const side = + type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask; + let priceLots; + let oraclePeggedProperties; + if (isOraclePegged) { + const priceData = leafNode.key.ushrn(64); + const priceOffset = priceData.sub(new BN(1).ushln(63)); + priceLots = perpMarket.uiPriceToLots(perpMarket.uiPrice).add(priceOffset); + const isInvalid = + type === BookSideType.bids + ? priceLots.gt(leafNode.pegLimit) && !leafNode.pegLimit.eqn(-1) + : leafNode.pegLimit.gt(priceLots); + oraclePeggedProperties = { + isInvalid, + priceOffset, + uiPriceOffset: perpMarket.priceLotsToUi(priceOffset), + pegLimit: leafNode.pegLimit, + uiPegLimit: perpMarket.priceLotsToUi(leafNode.pegLimit), + } as OraclePeggedProperties; + } else { + priceLots = BookSide.getPriceFromKey(leafNode.key); + } + const expiryTimestamp = leafNode.timeInForce + ? leafNode.timestamp.add(new BN(leafNode.timeInForce)) + : U64_MAX_BN; + + return new PerpOrder( + type === BookSideType.bids + ? RUST_U64_MAX().sub(leafNode.key.maskn(64)) + : leafNode.key.maskn(64), + leafNode.key, + leafNode.owner, + leafNode.ownerSlot, + 0, + perpMarket.priceLotsToUi(priceLots), + priceLots, + perpMarket.baseLotsToUi(leafNode.quantity), + leafNode.quantity, + side, + leafNode.timestamp, + expiryTimestamp, + perpMarket.perpMarketIndex, + isExpired, + isOraclePegged, + leafNode.orderType, + oraclePeggedProperties, + ); + } + + constructor( + public seqNum: BN, + public orderId: BN, + public owner: PublicKey, + public openOrdersSlot: number, + public feeTier: 0, + public uiPrice: number, + public priceLots: BN, + public uiSize: number, + public sizeLots: BN, + public side: PerpOrderSide, + public timestamp: BN, + public expiryTimestamp: BN, + public perpMarketIndex: number, + public isExpired = false, + public isOraclePegged = false, + public orderType: PerpOrderType, + public oraclePeggedProperties?: OraclePeggedProperties, + ) {} + + get price(): number { + return this.uiPrice; + } + + get size(): number { + return this.uiSize; + } +} + +interface OraclePeggedProperties { + isInvalid: boolean; + priceOffset: BN; + uiPriceOffset: number; + pegLimit: BN; + uiPegLimit: number; +} diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 93151cfad..c3acae437 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -36,7 +36,7 @@ import { isSwitchboardOracle, parseSwitchboardOracle, } from './oracle'; -import { BookSide, PerpMarket, PerpMarketIndex } from './perp'; +import { BookSide, PerpMarket, PerpMarketIndex } from '..'; import { MarketIndex, Serum3Market } from './serum3'; export class Group { diff --git a/ts/client/src/accounts/healthCache.spec.ts b/ts/client/src/accounts/healthCache.spec.ts index 4c5bc5110..85aedc798 100644 --- a/ts/client/src/accounts/healthCache.spec.ts +++ b/ts/client/src/accounts/healthCache.spec.ts @@ -9,7 +9,7 @@ import { deepClone } from '../utils'; import { BankForHealth, StablePriceModel, TokenIndex } from './bank'; import { HealthCache, PerpInfo, SpotInfo, TokenInfo } from './healthCache'; import { HealthType, PerpPosition, Serum3Orders } from './mangoAccount'; -import { PerpMarket, PerpOrderSide } from './perp'; +import { PerpMarket, PerpOrderSide } from '..'; import { MarketIndex } from './serum3'; function mockBankAndOracle( diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 4711762c4..2cb1809e0 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -18,7 +18,7 @@ import { PerpPosition, Serum3Orders, } from './mangoAccount'; -import { PerpMarket, PerpMarketIndex, PerpOrderSide } from './perp'; +import { PerpMarket, PerpMarketIndex, PerpOrderSide } from '..'; import { MarketIndex, Serum3Side } from './serum3'; // ░░░░ diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 4070ab62b..289de3b67 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -26,7 +26,7 @@ import { MangoSignatureStatus } from '../utils/rpc'; import { Bank, TokenIndex } from './bank'; import { Group } from './group'; import { HealthCache } from './healthCache'; -import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from './perp'; +import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from '..'; import { MarketIndex, Serum3Side } from './serum3'; export class MangoAccount { public name: string; diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index a3c8217a0..7bec97679 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -3,16 +3,9 @@ import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; import Big from 'big.js'; import { MangoClient } from '../client'; -import { RUST_U64_MAX } from '../constants'; import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48'; import { Modify } from '../types'; -import { - As, - QUOTE_DECIMALS, - U64_MAX_BN, - toNative, - toUiDecimals, -} from '../utils'; +import { As, QUOTE_DECIMALS, toNative, toUiDecimals } from '../utils'; import { OracleConfig, OracleConfigDto, @@ -22,6 +15,12 @@ import { import { Group } from './group'; import { MangoAccount } from './mangoAccount'; import { OracleProvider, isOracleStaleOrUnconfident } from './oracle'; +import { + BookSide, + BookSideType, + PerpOrderSide, + PerpOrderType, +} from './bookSide'; export type PerpMarketIndex = number & As<'perp-market-index'>; @@ -327,7 +326,8 @@ export class PerpMarket { forceReload = false, ): Promise { if (forceReload || !this._asks) { - const asks = await client.program.account.bookSide.fetch(this.asks); + const askInfo = await client.connection.getAccountInfo(this.asks); + const asks = BookSide.decodeAccountfromBuffer(askInfo!.data); this._asks = BookSide.from(client, this, BookSideType.asks, asks as any); } return this._asks; @@ -338,7 +338,8 @@ export class PerpMarket { forceReload = false, ): Promise { if (forceReload || !this._bids) { - const bids = await client.program.account.bookSide.fetch(this.bids); + const bidInfo = await client.connection.getAccountInfo(this.bids); + const bids = BookSide.decodeAccountfromBuffer(bidInfo!.data); this._bids = BookSide.from(client, this, BookSideType.bids, bids as any); } return this._bids; @@ -601,439 +602,6 @@ export class PerpMarket { } } -interface OrderTreeNodes { - bumpIndex: number; - freeListLen: number; - freeListHead: number; - nodes: [any]; -} - -interface OrderTreeRoot { - maybeNode: number; - leafCount: number; -} - -export class BookSide { - private static INNER_NODE_TAG = 1; - private static LEAF_NODE_TAG = 2; - now: BN; - - static from( - client: MangoClient, - perpMarket: PerpMarket, - bookSideType: BookSideType, - obj: { - roots: OrderTreeRoot[]; - nodes: OrderTreeNodes; - }, - ): BookSide { - return new BookSide( - client, - perpMarket, - bookSideType, - obj.roots[0], - obj.roots[1], - obj.nodes, - ); - } - - constructor( - public client: MangoClient, - public perpMarket: PerpMarket, - public type: BookSideType, - public rootFixed: OrderTreeRoot, - public rootOraclePegged: OrderTreeRoot, - public orderTreeNodes: OrderTreeNodes, - maxBookDelay?: number, - ) { - // Determine the maxTimestamp found on the book to use for tif - // If maxBookDelay is not provided, use 3600 as a very large number - maxBookDelay = maxBookDelay === undefined ? 3600 : maxBookDelay; - let maxTimestamp = new BN(new Date().getTime() / 1000 - maxBookDelay); - for (const node of this.orderTreeNodes.nodes) { - if (node.tag !== BookSide.LEAF_NODE_TAG) { - continue; - } - - const leafNode = BookSide.toLeafNode(client, node.data); - if (leafNode.timestamp.gt(maxTimestamp)) { - maxTimestamp = leafNode.timestamp; - } - } - this.now = maxTimestamp; - } - - static getPriceFromKey(key: BN): BN { - return key.ushrn(64); - } - - /** - * iterates over all orders - */ - public *items(): Generator { - function isBetter(type: BookSideType, a: PerpOrder, b: PerpOrder): boolean { - return a.priceLots.eq(b.priceLots) - ? a.seqNum.lt(b.seqNum) // if prices are equal prefer perp orders in the order they are placed - : type === BookSideType.bids // else compare the actual prices - ? a.priceLots.gt(b.priceLots) - : b.priceLots.gt(a.priceLots); - } - - const fGen = this.fixedItems(); - const oPegGen = this.oraclePeggedItems(); - - let fOrderRes = fGen.next(); - let oPegOrderRes = oPegGen.next(); - - while (true) { - if (fOrderRes.value && oPegOrderRes.value) { - if (isBetter(this.type, fOrderRes.value, oPegOrderRes.value)) { - yield fOrderRes.value; - fOrderRes = fGen.next(); - } else { - yield oPegOrderRes.value; - oPegOrderRes = oPegGen.next(); - } - } else if (fOrderRes.value && !oPegOrderRes.value) { - yield fOrderRes.value; - fOrderRes = fGen.next(); - } else if (!fOrderRes.value && oPegOrderRes.value) { - yield oPegOrderRes.value; - oPegOrderRes = oPegGen.next(); - } else if (!fOrderRes.value && !oPegOrderRes.value) { - break; - } - } - } - - /** - * iterates over all orders, - * skips oracle pegged orders which are invalid due to oracle price crossing the peg limit, - * skips tif orders which are invalid due to tif having elapsed, - */ - public *itemsValid(): Generator { - const itemsGen = this.items(); - let itemsRes = itemsGen.next(); - while (true) { - if (itemsRes.value) { - const val = itemsRes.value; - if ( - !val.isExpired && - (!val.isOraclePegged || - (val.isOraclePegged && !val.oraclePeggedProperties.isInvalid)) - ) { - yield val; - } - itemsRes = itemsGen.next(); - } else { - break; - } - } - } - - public *fixedItems(): Generator { - if (this.rootFixed.leafCount === 0) { - return; - } - const now = this.now; - const stack = [this.rootFixed.maybeNode]; - const [left, right] = this.type === BookSideType.bids ? [1, 0] : [0, 1]; - - while (stack.length > 0) { - const index = stack.pop()!; - const node = this.orderTreeNodes.nodes[index]; - if (node.tag === BookSide.INNER_NODE_TAG) { - const innerNode = BookSide.toInnerNode(this.client, node.data); - stack.push(innerNode.children[right], innerNode.children[left]); - } else if (node.tag === BookSide.LEAF_NODE_TAG) { - const leafNode = BookSide.toLeafNode(this.client, node.data); - const expiryTimestamp = leafNode.timeInForce - ? leafNode.timestamp.add(new BN(leafNode.timeInForce)) - : U64_MAX_BN; - - yield PerpOrder.from( - this.perpMarket, - leafNode, - this.type, - now.gt(expiryTimestamp), - ); - } - } - } - - public *oraclePeggedItems(): Generator { - if (this.rootOraclePegged.leafCount === 0) { - return; - } - const now = this.now; - const stack = [this.rootOraclePegged.maybeNode]; - const [left, right] = this.type === BookSideType.bids ? [1, 0] : [0, 1]; - - while (stack.length > 0) { - const index = stack.pop()!; - const node = this.orderTreeNodes.nodes[index]; - if (node.tag === BookSide.INNER_NODE_TAG) { - const innerNode = BookSide.toInnerNode(this.client, node.data); - stack.push(innerNode.children[right], innerNode.children[left]); - } else if (node.tag === BookSide.LEAF_NODE_TAG) { - const leafNode = BookSide.toLeafNode(this.client, node.data); - const expiryTimestamp = leafNode.timeInForce - ? leafNode.timestamp.add(new BN(leafNode.timeInForce)) - : U64_MAX_BN; - - yield PerpOrder.from( - this.perpMarket, - leafNode, - this.type, - now.gt(expiryTimestamp), - true, - ); - } - } - } - - public best(): PerpOrder | undefined { - return this.items().next().value; - } - - getImpactPriceUi(baseLots: BN): number | undefined { - const s = new BN(0); - for (const order of this.items()) { - s.iadd(order.sizeLots); - if (s.gte(baseLots)) { - return order.uiPrice; - } - } - return undefined; - } - - public getL2(depth: number): [number, number, BN, BN][] { - const levels: [BN, BN][] = []; - for (const { priceLots, sizeLots } of this.items()) { - if (levels.length > 0 && levels[levels.length - 1][0].eq(priceLots)) { - levels[levels.length - 1][1].iadd(sizeLots); - } else if (levels.length === depth) { - break; - } else { - levels.push([priceLots, sizeLots]); - } - } - return levels.map(([priceLots, sizeLots]) => [ - this.perpMarket.priceLotsToUi(priceLots), - this.perpMarket.baseLotsToUi(sizeLots), - priceLots, - sizeLots, - ]); - } - - public getL2Ui(depth: number): [number, number][] { - const levels: [number, number][] = []; - for (const { uiPrice: price, uiSize: size } of this.items()) { - if (levels.length > 0 && levels[levels.length - 1][0] === price) { - levels[levels.length - 1][1] += size; - } else if (levels.length === depth) { - break; - } else { - levels.push([price, size]); - } - } - return levels; - } - - static toInnerNode(client: MangoClient, data: [number]): InnerNode { - return (client.program as any)._coder.types.typeLayouts - .get('InnerNode') - .decode(Buffer.from([BookSide.INNER_NODE_TAG].concat(data))); - } - static toLeafNode(client: MangoClient, data: [number]): LeafNode { - return LeafNode.from( - (client.program as any)._coder.types.typeLayouts - .get('LeafNode') - .decode(Buffer.from([BookSide.LEAF_NODE_TAG].concat(data))), - ); - } -} - -export type BookSideType = - | { bids: Record } - | { asks: Record }; -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace BookSideType { - export const bids = { bids: {} }; - export const asks = { asks: {} }; -} - -export class LeafNode { - static from(obj: { - ownerSlot: number; - orderType: PerpOrderType; - timeInForce: number; - key: BN; - owner: PublicKey; - quantity: BN; - timestamp: BN; - pegLimit: BN; - }): LeafNode { - return new LeafNode( - obj.ownerSlot, - obj.orderType, - obj.timeInForce, - obj.key, - obj.owner, - obj.quantity, - obj.timestamp, - obj.pegLimit, - ); - } - - constructor( - public ownerSlot: number, - public orderType: PerpOrderType, - public timeInForce: number, - public key: BN, - public owner: PublicKey, - public quantity: BN, - public timestamp: BN, - public pegLimit: BN, - ) {} -} -export class InnerNode { - static from(obj: { children: [number] }): InnerNode { - return new InnerNode(obj.children); - } - - constructor(public children: [number]) {} -} - -export type PerpSelfTradeBehavior = - | { decrementTake: Record } - | { cancelProvide: Record } - | { abortTransaction: Record }; -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace PerpSelfTradeBehavior { - export const decrementTake = { decrementTake: {} }; - export const cancelProvide = { cancelProvide: {} }; - export const abortTransaction = { abortTransaction: {} }; -} - -export type PerpOrderSide = - | { bid: Record } - | { ask: Record }; -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace PerpOrderSide { - export const bid = { bid: {} }; - export const ask = { ask: {} }; -} - -export type PerpOrderType = - | { limit: Record } - | { immediateOrCancel: Record } - | { postOnly: Record } - | { market: Record } - | { postOnlySlide: Record }; -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace PerpOrderType { - export const limit = { limit: {} }; - export const immediateOrCancel = { immediateOrCancel: {} }; - export const postOnly = { postOnly: {} }; - export const market = { market: {} }; - export const postOnlySlide = { postOnlySlide: {} }; -} - -export class PerpOrder { - static from( - perpMarket: PerpMarket, - leafNode: LeafNode, - type: BookSideType, - isExpired = false, - isOraclePegged = false, - ): PerpOrder { - const side = - type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask; - let priceLots; - let oraclePeggedProperties; - if (isOraclePegged) { - const priceData = leafNode.key.ushrn(64); - const priceOffset = priceData.sub(new BN(1).ushln(63)); - priceLots = perpMarket.uiPriceToLots(perpMarket.uiPrice).add(priceOffset); - const isInvalid = - type === BookSideType.bids - ? priceLots.gt(leafNode.pegLimit) && !leafNode.pegLimit.eqn(-1) - : leafNode.pegLimit.gt(priceLots); - oraclePeggedProperties = { - isInvalid, - priceOffset, - uiPriceOffset: perpMarket.priceLotsToUi(priceOffset), - pegLimit: leafNode.pegLimit, - uiPegLimit: perpMarket.priceLotsToUi(leafNode.pegLimit), - } as OraclePeggedProperties; - } else { - priceLots = BookSide.getPriceFromKey(leafNode.key); - } - const expiryTimestamp = leafNode.timeInForce - ? leafNode.timestamp.add(new BN(leafNode.timeInForce)) - : U64_MAX_BN; - - return new PerpOrder( - type === BookSideType.bids - ? RUST_U64_MAX().sub(leafNode.key.maskn(64)) - : leafNode.key.maskn(64), - leafNode.key, - leafNode.owner, - leafNode.ownerSlot, - 0, - perpMarket.priceLotsToUi(priceLots), - priceLots, - perpMarket.baseLotsToUi(leafNode.quantity), - leafNode.quantity, - side, - leafNode.timestamp, - expiryTimestamp, - perpMarket.perpMarketIndex, - isExpired, - isOraclePegged, - leafNode.orderType, - oraclePeggedProperties, - ); - } - - constructor( - public seqNum: BN, - public orderId: BN, - public owner: PublicKey, - public openOrdersSlot: number, - public feeTier: 0, - public uiPrice: number, - public priceLots: BN, - public uiSize: number, - public sizeLots: BN, - public side: PerpOrderSide, - public timestamp: BN, - public expiryTimestamp: BN, - public perpMarketIndex: number, - public isExpired = false, - public isOraclePegged = false, - public orderType: PerpOrderType, - public oraclePeggedProperties?: OraclePeggedProperties, - ) {} - - get price(): number { - return this.uiPrice; - } - - get size(): number { - return this.uiSize; - } -} - -interface OraclePeggedProperties { - isInvalid: boolean; - priceOffset: BN; - uiPriceOffset: number; - pegLimit: BN; - uiPegLimit: number; -} - export class PerpEventQueue { static FILL_EVENT_TYPE = 0; static OUT_EVENT_TYPE = 1; diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 88c8b15d4..21c00266d 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -66,9 +66,6 @@ import { PerpEventQueue, PerpMarket, PerpMarketIndex, - PerpOrderSide, - PerpOrderType, - PerpSelfTradeBehavior, } from './accounts/perp'; import { MarketIndex, @@ -111,6 +108,11 @@ import { sendTransaction, } from './utils/rpc'; import { NATIVE_MINT, TOKEN_PROGRAM_ID } from './utils/spl'; +import { + PerpOrderSide, + PerpOrderType, + PerpSelfTradeBehavior, +} from './accounts/bookSide'; export const DEFAULT_TOKEN_CONDITIONAL_SWAP_COUNT = 8; export const PERP_SETTLE_PNL_CU_LIMIT = 400000; diff --git a/ts/client/src/index.ts b/ts/client/src/index.ts index 23519da47..77572ef5a 100644 --- a/ts/client/src/index.ts +++ b/ts/client/src/index.ts @@ -7,6 +7,7 @@ export * from './accounts/bank'; export * from './accounts/mangoAccount'; export * from './accounts/oracle'; export * from './accounts/perp'; +export * from './accounts/bookSide'; export * from './accounts/openbookV2'; export { Serum3Market, diff --git a/yarn.lock b/yarn.lock index c5c942403..b13b02c7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -199,9 +199,9 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@openbook-dex/openbook-v2@git+https://github.com/openbook-dex/openbook-v2#pan/ts-client-anchor-0.29.0": +"@openbook-dex/openbook-v2@git+https://github.com/openbook-dex/openbook-v2": version "0.2.7" - resolved "git+https://github.com/openbook-dex/openbook-v2#380d2a8a8d9b3ba6fe360cc894e60282dfbf9812" + resolved "git+https://github.com/openbook-dex/openbook-v2#c0cf159271544f5e10136bc94d1b634fbf48ee3a" dependencies: "@coral-xyz/anchor" "^0.29.0" "@solana/spl-token" "^0.4.0"