diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 9a4a9f58e..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - }, - extends: ['standard-with-typescript', 'prettier'], - overrides: [ - { - env: { - node: true, - }, - files: ['.eslintrc.{js,cjs}'], - parserOptions: { - sourceType: 'script', - }, - }, - ], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['tsconfig.json'], - }, - rules: {}, -}; diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..ba35b46de --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,34 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "linebreak-style": [ + "error", + "unix" + ], + "semi": [ + "error", + "always" + ], + "@typescript-eslint/no-non-null-assertion": 0, + "@typescript-eslint/ban-ts-comment": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-function-return-type": "warn" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 7f13e3761..e489af74f 100644 --- a/README.md +++ b/README.md @@ -103,3 +103,12 @@ just test-dev ```bash yarn build ``` + +### TS Testing + +```bash +export SOL_RPC_URL=https://a.b.c +export KEYPAIR="[1,2,3,4,...]" +yarn ts/client/src/test/market.ts +yarn ts/client/src/test/openOrders.ts +``` \ No newline at end of file diff --git a/package.json b/package.json index 0b7757e02..3df8277e9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@coral-xyz/anchor": "^0.28.1-beta.2", - "@solana/spl-token": "0.3.8", + "@solana/spl-token": "^0.4.0", "@solana/web3.js": "^1.77.3", "big.js": "^6.2.1" }, @@ -51,6 +51,7 @@ "mocha": "^9.0.3", "prettier": "^2.6.2", "ts-mocha": "^10.0.0", + "ts-node": "^10.9.2", "typescript": "*" }, "license": "MIT" diff --git a/ts/client/src/accounts/bookSide.ts b/ts/client/src/accounts/bookSide.ts new file mode 100644 index 000000000..b3e2c125c --- /dev/null +++ b/ts/client/src/accounts/bookSide.ts @@ -0,0 +1,166 @@ +import { PublicKey } from '@solana/web3.js'; +import { + Market, + BookSideAccount, + SideUtils, + Side, + OpenBookV2Client, + LeafNode, + InnerNode, + U64_MAX_BN, +} from '..'; +import { BN } from '@coral-xyz/anchor'; +import { Order } from '../structs/order'; + +export class BookSide { + public clusterTime: BN; + + constructor( + public market: Market, + public pubkey: PublicKey, + public account: BookSideAccount, + public side: Side, + ) { + this.clusterTime = new BN(0); + } + + public *items(): Generator { + const fGen = this.fixedItems(); + const oPegGen = this.oraclePeggedItems(); + + let fOrderRes = fGen.next(); + let oPegOrderRes = oPegGen.next(); + + while (true) { + if (fOrderRes.value && oPegOrderRes.value) { + if (this.compareOrders(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; + } + } + } + + get rootFixed() { + return this.account.roots[0]; + } + + get rootOraclePegged() { + return this.account.roots[1]; + } + + public *fixedItems(): Generator { + if (this.rootFixed.leafCount === 0) { + return; + } + const stack = [this.rootFixed.maybeNode]; + const [left, right] = this.side === SideUtils.Bid ? [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 = this.toInnerNode(node.data); + stack.push(innerNode.children[right], innerNode.children[left]); + } else if (node.tag === BookSide.LEAF_NODE_TAG) { + const leafNode = this.toLeafNode(node.data); + const expiryTimestamp = leafNode.timeInForce + ? leafNode.timestamp.add(new BN(leafNode.timeInForce)) + : U64_MAX_BN; + + yield new Order( + this.market, + leafNode, + this.side, + this.clusterTime.gt(expiryTimestamp), + ); + } + } + } + + public *oraclePeggedItems(): Generator { + if (this.rootOraclePegged.leafCount === 0) { + return; + } + const stack = [this.rootOraclePegged.maybeNode]; + const [left, right] = this.side === SideUtils.Bid ? [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 = this.toInnerNode(node.data); + stack.push(innerNode.children[right], innerNode.children[left]); + } else if (node.tag === BookSide.LEAF_NODE_TAG) { + const leafNode = this.toLeafNode(node.data); + const expiryTimestamp = leafNode.timeInForce + ? leafNode.timestamp.add(new BN(leafNode.timeInForce)) + : U64_MAX_BN; + + yield new Order( + this.market, + leafNode, + this.side, + this.clusterTime.gt(expiryTimestamp), + true, + ); + } + } + } + + public compareOrders(a: Order, b: Order): boolean { + return a.priceLots.eq(b.priceLots) + ? a.seqNum.lt(b.seqNum) // if prices are equal prefer orders in the order they are placed + : this.side === SideUtils.Bid // else compare the actual prices + ? a.priceLots.gt(b.priceLots) + : b.priceLots.gt(a.priceLots); + } + + public best(): Order | undefined { + return this.items().next().value; + } + + 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.market.priceLotsToUi(priceLots), + this.market.baseLotsToUi(sizeLots), + priceLots, + sizeLots, + ]); + } + + private static INNER_NODE_TAG = 1; + private static LEAF_NODE_TAG = 2; + + private toInnerNode(data: number[]): InnerNode { + return (this.market.client.program as any)._coder.types.typeLayouts + .get('InnerNode') + .decode(Buffer.from([BookSide.INNER_NODE_TAG].concat(data))); + } + private toLeafNode(data: number[]): LeafNode { + return (this.market.client.program as any)._coder.types.typeLayouts + .get('LeafNode') + .decode(Buffer.from([BookSide.LEAF_NODE_TAG].concat(data))); + } +} diff --git a/ts/client/src/accounts/market.ts b/ts/client/src/accounts/market.ts new file mode 100644 index 000000000..849a6a19c --- /dev/null +++ b/ts/client/src/accounts/market.ts @@ -0,0 +1,157 @@ +import Big from 'big.js'; +import { BN } from '@coral-xyz/anchor'; +import { PublicKey } from '@solana/web3.js'; +import { + toNative, + MarketAccount, + OpenBookV2Client, + BookSideAccount, + BookSide, + SideUtils, + nameToString, +} from '..'; + +export class Market { + public minOrderSize: number; + public tickSize: number; + public quoteLotFactor: number; + + /** + * use async loadBids() or loadOrderBook() to populate bids + */ + public bids: BookSide | undefined; + + /** + * use async loadAsks() or loadOrderBook() to populate asks + */ + public asks: BookSide | undefined; + + constructor( + public client: OpenBookV2Client, + public pubkey: PublicKey, + public account: MarketAccount, + ) { + this.minOrderSize = new Big(10) + .pow(account.baseDecimals - account.quoteDecimals) + .mul(new Big(account.quoteLotSize.toString())) + .div(new Big(account.baseLotSize.toString())) + .toNumber(); + this.quoteLotFactor = new Big(account.quoteLotSize.toString()) + .div(new Big(10).pow(account.quoteDecimals)) + .toNumber(); + this.tickSize = new Big(10) + .pow(account.baseDecimals - account.quoteDecimals) + .mul(new Big(account.quoteLotSize.toString())) + .div(new Big(account.baseLotSize.toString())) + .toNumber(); + } + + public static async load( + client: OpenBookV2Client, + pubkey: PublicKey, + ): Promise { + const account = await client.program.account.market.fetch(pubkey); + return new Market(client, pubkey, account); + } + + public baseLotsToUi(lots: BN): number { + return Number(lots.toString()) * this.minOrderSize; + } + public quoteLotsToUi(lots: BN): number { + return Number(lots.toString()) * this.quoteLotFactor; + } + public priceLotsToUi(lots: BN): number { + return Number(lots.toString()) * this.tickSize; + } + + public baseUiToLots(uiAmount: number): BN { + return toNative(uiAmount, this.account.baseDecimals).div( + this.account.baseLotSize, + ); + } + public quoteUiToLots(uiAmount: number): BN { + return toNative(uiAmount, this.account.quoteDecimals).div( + this.account.quoteLotSize, + ); + } + public priceUiToLots(uiAmount: number): BN { + return toNative(uiAmount, this.account.quoteDecimals) + .imul(this.account.baseLotSize) + .div( + new BN(Math.pow(10, this.account.baseDecimals)).imul( + this.account.quoteLotSize, + ), + ); + } + + public async loadBids(): Promise { + const bidSide = (await this.client.program.account.bookSide.fetch( + this.account.bids, + )) as BookSideAccount; + this.bids = new BookSide(this, this.account.bids, bidSide, SideUtils.Bid); + return this.bids; + } + + public async loadAsks(): Promise { + const askSide = (await this.client.program.account.bookSide.fetch( + this.account.asks, + )) as BookSideAccount; + this.asks = new BookSide(this, this.account.asks, askSide, SideUtils.Ask); + return this.asks; + } + + public async loadOrderBook(): Promise { + await Promise.all([this.loadBids(), this.loadAsks()]); + return this; + } + + public toPrettyString(): string { + const mkt = this.account; + let debug = `Market: ${nameToString(mkt.name)}\n`; + debug += ` authority: ${mkt.marketAuthority.toBase58()}\n`; + debug += ` collectFeeAdmin: ${mkt.collectFeeAdmin.toBase58()}\n`; + if (!mkt.openOrdersAdmin.key.equals(PublicKey.default)) + debug += ` openOrdersAdmin: ${mkt.openOrdersAdmin.key.toBase58()}\n`; + if (!mkt.consumeEventsAdmin.key.equals(PublicKey.default)) + debug += ` consumeEventsAdmin: ${mkt.consumeEventsAdmin.key.toBase58()}\n`; + if (!mkt.closeMarketAdmin.key.equals(PublicKey.default)) + debug += ` closeMarketAdmin: ${mkt.closeMarketAdmin.key.toBase58()}\n`; + + debug += ` baseMint: ${mkt.baseMint.toBase58()}\n`; + debug += ` quoteMint: ${mkt.quoteMint.toBase58()}\n`; + debug += ` marketBaseVault: ${mkt.marketBaseVault.toBase58()}\n`; + debug += ` marketQuoteVault: ${mkt.marketQuoteVault.toBase58()}\n`; + + if (!mkt.oracleA.key.equals(PublicKey.default)) { + debug += ` oracleConfig: { confFilter: ${ + mkt.oracleConfig.confFilter + }, maxStalenessSlots: ${mkt.oracleConfig.maxStalenessSlots.toString()} }\n`; + debug += ` oracleA: ${mkt.oracleA.key.toBase58()}\n`; + } + if (!mkt.oracleB.key.equals(PublicKey.default)) + debug += ` oracleB: ${mkt.oracleB.key.toBase58()}\n`; + + debug += ` bids: ${mkt.bids.toBase58()}\n`; + const bb = this.bids?.best(); + if (bb) { + debug += ` best: ${bb.price} ${ + bb.size + } ${bb.leafNode.owner.toBase58()}\n`; + } + + debug += ` asks: ${mkt.asks.toBase58()}\n`; + const ba = this.asks?.best(); + if (ba) { + debug += ` best: ${ba.price} ${ + ba.size + } ${ba.leafNode.owner.toBase58()}\n`; + } + + debug += ` eventHeap: ${mkt.eventHeap.toBase58()}\n`; + + debug += ` minOrderSize: ${this.minOrderSize}\n`; + debug += ` tickSize: ${this.tickSize}\n`; + + return debug; + } +} diff --git a/ts/client/src/accounts/openOrders.ts b/ts/client/src/accounts/openOrders.ts new file mode 100644 index 000000000..2dd46318c --- /dev/null +++ b/ts/client/src/accounts/openOrders.ts @@ -0,0 +1,416 @@ +import { + Keypair, + PublicKey, + Signer, + Transaction, + TransactionInstruction, +} from '@solana/web3.js'; +import { + OpenBookV2Client, + OpenOrdersAccount, + PlaceOrderType, + SelfTradeBehavior as SelfTradeBehaviorType, + Side, + nameToString, +} from '../client'; +import { Market } from './market'; +import { + I64_MAX_BN, + PlaceOrderTypeUtils, + SelfTradeBehaviorUtils, + SideUtils, + U64_MAX_BN, + getAssociatedTokenAddress, +} from '../utils/utils'; +import { BN } from '@coral-xyz/anchor'; +import { OpenOrdersIndexer } from './openOrdersIndexer'; +import { Order } from '../structs/order'; +import { createAssociatedTokenAccountIdempotentInstruction } from '@solana/spl-token'; + +export interface OrderToPlace { + side: Side; + price: number; + size: number; + quoteLimit?: number; + clientOrderId?: number; + orderType?: PlaceOrderType; + expiryTimestamp?: number; + selfTradeBehavior?: SelfTradeBehaviorType; + matchLoopLimit?: number; +} + +export class OpenOrders { + public delegate: Keypair | undefined; + + constructor( + public pubkey: PublicKey, + public account: OpenOrdersAccount, + public market: Market, + ) {} + + /// high-level API + + public static async load( + pubkey: PublicKey, + market?: Market, + client?: OpenBookV2Client, + ): Promise { + client ??= market?.client; + if (!client) throw new Error('provide either market or client'); + + const account = await client.program.account.openOrdersAccount.fetch( + pubkey, + ); + + if (!market) { + market = await Market.load(client, account.market); + } + + return new OpenOrders(pubkey, account, market); + } + + /** + * Try loading the OpenOrders account associated with a Market + * @param market + * @param owner optional if configured already on the Market's client + * @param indexer optional, pass in to speed up fetch + * @returns null if the user does not have an OpenOrders account for this market + */ + public static async loadNullableForMarketAndOwner( + market: Market, + owner?: PublicKey, + indexer?: OpenOrdersIndexer | null, + ): Promise { + indexer ??= await OpenOrdersIndexer.loadNullable(market.client, owner); + if (!indexer) return null; + const ooPks = indexer.account.addresses; + const ooAccs = + await market.client.program.account.openOrdersAccount.fetchMultiple( + ooPks, + ); + const ooIndex = ooAccs.findIndex((o) => o?.market.equals(market.pubkey)); + if (ooIndex == -1) return null; + const ooPk = ooPks[ooIndex]; + const ooAcc = ooAccs[ooIndex]; + // note: ooPk & ooAcc most certainly will always be defined here, due to the index check + return ooPk && ooAcc && new OpenOrders(ooPk, ooAcc, market); + } + + public async reload(): Promise { + this.account = + await this.market.client.program.account.openOrdersAccount.fetch( + this.pubkey, + ); + return this; + } + + public setDelegate(delegate: Keypair): this { + this.delegate = delegate; + return this; + } + + public async placeOrder(order: OrderToPlace): Promise { + // derive token account + const mint = + order.side === SideUtils.Bid + ? this.market.account.quoteMint + : this.market.account.baseMint; + const userTokenAccount = await getAssociatedTokenAddress( + mint, + this.market.client.walletPk, + ); + + // TODO: derive wrap sol instruction + + const remainingAccounts = new Set(); + const bookSide = + order.side === SideUtils.Bid ? this.market.asks : this.market.bids; + if ( + bookSide && + order.orderType !== PlaceOrderTypeUtils.PostOnly && + order.orderType !== PlaceOrderTypeUtils.PostOnlySlide + ) { + for (const order of bookSide.items()) { + remainingAccounts.add(order.leafNode.owner.toBase58()); + if (remainingAccounts.size >= 3) break; + } + } + + const [placeIx] = await this.placeOrderIx( + order, + userTokenAccount, + [...remainingAccounts].map((a) => new PublicKey(a)), + ); + + const additionalSigners = this.delegate ? [this.delegate] : []; + + return this.market.client.sendAndConfirmTransaction([placeIx], { + additionalSigners, + }); + } + + public async cancelOrder( + order: Order | { clientOrderId: number }, + ): Promise { + const ixs: TransactionInstruction[] = []; + if ('clientOrderId' in order) { + const id = new BN(order.clientOrderId); + const [ix] = await this.cancelOrderByClientIdIx(id); + ixs.push(ix); + } else { + const id = order.leafNode.key; + const [ix] = await this.cancelOrderByIdIx(id); + ixs.push(ix); + } + + const additionalSigners = this.delegate ? [this.delegate] : []; + + return this.market.client.sendAndConfirmTransaction(ixs, { + additionalSigners, + }); + } + + public async cancelAllOrders(side: Side | null): Promise { + const [cancelIx] = await this.cancelAllOrdersIx(side); + + const { baseMint, quoteMint } = this.market.account; + const owner = this.market.client.walletPk; + const payer = this.delegate?.publicKey ?? owner; + + const ataIxs: TransactionInstruction[] = []; + const baseATA = await getAssociatedTokenAddress(baseMint, owner); + ataIxs.push( + createAssociatedTokenAccountIdempotentInstruction( + payer, + baseATA, + owner, + baseMint, + ), + ); + + const quoteATA = await getAssociatedTokenAddress(quoteMint, owner); + ataIxs.push( + createAssociatedTokenAccountIdempotentInstruction( + payer, + quoteATA, + owner, + quoteMint, + ), + ); + + const referrer = this.market.client.referrerWallet; + let referrerATA: PublicKey | null = null; + if (referrer) { + referrerATA = await getAssociatedTokenAddress(quoteMint, referrer); + ataIxs.push( + createAssociatedTokenAccountIdempotentInstruction( + payer, + referrerATA, + referrer, + quoteMint, + ), + ); + } + + const [settleIx] = await this.settleFundsIx( + baseATA, + quoteATA, + referrerATA, + payer, + ); + + // TODO: derive unwrap sol instruction + + const additionalSigners = this.delegate ? [this.delegate] : []; + + return this.market.client.sendAndConfirmTransaction( + [cancelIx, ...ataIxs, settleIx], + { additionalSigners }, + ); + } + + public *items(): Generator { + const { bids, asks } = this.market; + if (!bids || !asks) + throw new Error('requires OrderBook of Market to be loaded'); + + for (const slot of this.account.openOrders) { + if (slot.isFree) continue; + + let gen; + switch (slot.sideAndTree) { + case 0: + gen = bids.fixedItems(); + break; + case 1: + gen = asks.fixedItems(); + break; + case 2: + gen = bids.oraclePeggedItems(); + break; + case 3: + gen = asks.oraclePeggedItems(); + break; + } + + inner: for (const order of gen as Generator) { + if (order.leafNode.key.eq(slot.id)) { + yield order; + break inner; + } + } + } + } + + public toPrettyString(): string { + const oo = this.account; + let debug = `OO: ${this.pubkey.toBase58()}\n`; + debug += ` owner: ${oo.owner.toBase58()}\n`; + debug += ` market: ${oo.market.toBase58()} (${nameToString( + this.market.account.name, + )})\n`; + if (!oo.delegate.key.equals(PublicKey.default)) + debug += ` delegate: ${oo.delegate.key.toBase58()}\n`; + + debug += ` accountNum: ${oo.accountNum}\n`; + debug += ` version: ${oo.version}\n`; + debug += ` bidsBaseLots: ${oo.position.bidsBaseLots.toString()}\n`; + debug += ` bidsQuoteLots: ${oo.position.bidsQuoteLots.toString()}\n`; + debug += ` asksBaseLots: ${oo.position.asksBaseLots.toString()}\n`; + debug += ` baseFreeNative: ${oo.position.baseFreeNative.toString()}\n`; + debug += ` quoteFreeNative: ${oo.position.quoteFreeNative.toString()}\n`; + debug += ` lockedMakerFees: ${oo.position.lockedMakerFees.toString()}\n`; + debug += ` referrerRebatesAvailable: ${oo.position.referrerRebatesAvailable.toString()}\n`; + debug += ` penaltyHeapCount: ${oo.position.penaltyHeapCount.toString()}\n`; + debug += ` makerVolume: ${oo.position.makerVolume.toString()}\n`; + debug += ` takerVolume: ${oo.position.takerVolume.toString()}\n`; + + debug += ` orders:\n`; + for (const order of this.items()) { + debug += ` ${order.toPrettyString()}\n`; + } + + return debug; + } + + getBaseBalanceNative(): BN { + return this.account.position.asksBaseLots + .mul(this.market.account.baseLotSize) + .iadd(this.account.position.baseFreeNative); + } + + getQuoteBalanceNative(): BN { + return this.account.position.bidsQuoteLots + .mul(this.market.account.quoteLotSize) + .iadd(this.account.position.quoteFreeNative) + .iadd(this.account.position.lockedMakerFees); + } + + getBaseBalanceUi(): number { + return ( + Number(this.getBaseBalanceNative().toString()) / + 10 ** this.market.account.baseDecimals + ); + } + + getQuoteBalanceUi(): number { + return ( + Number(this.getQuoteBalanceNative().toString()) / + 10 ** this.market.account.quoteDecimals + ); + } + + /// low-level API + + public async placeOrderIx( + order: OrderToPlace, + userTokenAccount: PublicKey, + remainingAccounts: PublicKey[] = [], + ): Promise<[TransactionInstruction, Signer[]]> { + const priceLots = this.market.priceUiToLots(order.price); + const maxBaseLots = this.market.baseUiToLots(order.size); + const maxQuoteLotsIncludingFees = order.quoteLimit + ? new BN(order.quoteLimit) + : I64_MAX_BN; + const clientOrderId = new BN(order.clientOrderId || Date.now()); + const orderType = order.orderType || PlaceOrderTypeUtils.Limit; + const expiryTimestamp = new BN(order.expiryTimestamp || 0); + const selfTradeBehavior = + order.selfTradeBehavior || SelfTradeBehaviorUtils.DecrementTake; + const limit = order.matchLoopLimit || 16; + + const args = { + side: order.side, + priceLots, + maxBaseLots, + maxQuoteLotsIncludingFees, + clientOrderId, + orderType, + expiryTimestamp, + selfTradeBehavior, + limit, + }; + + return await this.market.client.placeOrderIx( + this.pubkey, + this.market.pubkey, + this.market.account, + userTokenAccount, + args, + remainingAccounts, + this.delegate, + ); + } + + public async cancelAllOrdersIx( + side: Side | null, + ): Promise<[TransactionInstruction, Signer[]]> { + return this.market.client.cancelAllOrdersIx( + this.pubkey, + this.account, + this.market.account, + 24, + side, + this.delegate, + ); + } + + public async cancelOrderByIdIx(id: BN) { + return this.market.client.cancelOrderByIdIx( + this.pubkey, + this.account, + this.market.account, + id, + this.delegate, + ); + } + + public async cancelOrderByClientIdIx(id: BN) { + return this.market.client.cancelOrderByClientIdIx( + this.pubkey, + this.account, + this.market.account, + id, + this.delegate, + ); + } + + public async settleFundsIx( + userBaseAccount: PublicKey, + userQuoteAccount: PublicKey, + referrerAccount: PublicKey | null, + penaltyPayer: PublicKey, + ): Promise<[TransactionInstruction, Signer[]]> { + return this.market.client.settleFundsIx( + this.pubkey, + this.account, + this.market.pubkey, + this.market.account, + userBaseAccount, + userQuoteAccount, + referrerAccount, + penaltyPayer, + this.delegate, + ); + } +} diff --git a/ts/client/src/accounts/openOrdersIndexer.ts b/ts/client/src/accounts/openOrdersIndexer.ts new file mode 100644 index 000000000..f146d65c8 --- /dev/null +++ b/ts/client/src/accounts/openOrdersIndexer.ts @@ -0,0 +1,31 @@ +import { PublicKey } from '@solana/web3.js'; +import { OpenBookV2Client, OpenOrdersIndexerAccount } from '../client'; + +export class OpenOrdersIndexer { + constructor( + public client: OpenBookV2Client, + public pubkey: PublicKey, + public account: OpenOrdersIndexerAccount, + ) {} + + public static async load( + client: OpenBookV2Client, + owner?: PublicKey, + ): Promise { + const pubkey = client.findOpenOrdersIndexer(owner); + const account = await client.program.account.openOrdersIndexer.fetch( + pubkey, + ); + return new OpenOrdersIndexer(client, pubkey, account); + } + + public static async loadNullable( + client: OpenBookV2Client, + owner?: PublicKey, + ): Promise { + const pubkey = client.findOpenOrdersIndexer(owner); + const account = + await client.program.account.openOrdersIndexer.fetchNullable(pubkey); + return account && new OpenOrdersIndexer(client, pubkey, account); + } +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 06cd22bd8..433892100 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -35,6 +35,7 @@ export type IdsSource = 'api' | 'static' | 'get-program-accounts'; export type PlaceOrderArgs = IdlTypes['PlaceOrderArgs']; export type PlaceOrderType = IdlTypes['PlaceOrderType']; export type Side = IdlTypes['Side']; +export type SelfTradeBehavior = IdlTypes['SelfTradeBehavior']; export type PlaceOrderPeggedArgs = IdlTypes['PlaceOrderPeggedArgs']; export type PlaceMultipleOrdersArgs = IdlTypes['PlaceMultipleOrdersArgs']; @@ -47,6 +48,7 @@ export type OpenOrdersIndexerAccount = export type EventHeapAccount = IdlAccounts['eventHeap']; export type BookSideAccount = IdlAccounts['bookSide']; export type LeafNode = IdlTypes['LeafNode']; +export type InnerNode = IdlTypes['InnerNode']; export type AnyNode = IdlTypes['AnyNode']; export type FillEvent = IdlTypes['FillEvent']; export type OutEvent = IdlTypes['OutEvent']; @@ -56,6 +58,7 @@ export interface OpenBookClientOptions { postSendTxCallback?: ({ txid }: { txid: string }) => void; prioritizationFee?: number; txConfirmationCommitment?: Commitment; + referrerWallet?: PublicKey; } export function nameToString(name: number[]): string { @@ -71,6 +74,7 @@ export const OPENBOOK_PROGRAM_ID = new PublicKey( export class OpenBookV2Client { public program: Program; + public referrerWallet: PublicKey | undefined; private readonly idsSource: IdsSource; private readonly postSendTxCallback?: ({ txid }) => void; @@ -92,6 +96,7 @@ export class OpenBookV2Client { ? (this.program.provider as AnchorProvider).opts.commitment : undefined) ?? 'processed'; + this.referrerWallet = opts.referrerWallet; // TODO: evil side effect, but limited backtraces are a nightmare Error.stackTraceLimit = 1000; } @@ -171,17 +176,6 @@ export class OpenBookV2Client { return [ix, address]; } - // Get the MarketAccount from the market publicKey - public async deserializeMarketAccount( - publicKey: PublicKey, - ): Promise { - try { - return await this.program.account.market.fetch(publicKey); - } catch { - return null; - } - } - public async deserializeOpenOrderAccount( publicKey: PublicKey, ): Promise { @@ -212,37 +206,6 @@ export class OpenBookV2Client { } } - public async deserializeBookSide( - publicKey: PublicKey, - ): Promise { - try { - return await this.program.account.bookSide.fetch(publicKey); - } catch { - return null; - } - } - - public priceData(key: BN): number { - const shiftedValue = key.shrn(64); // Shift right by 64 bits - return shiftedValue.toNumber(); // Convert BN to a regular number - } - - // Get bids or asks from a bookside account - public getLeafNodes(bookside: BookSideAccount): LeafNode[] { - const leafNodesData = bookside.nodes.nodes.filter( - (x: AnyNode) => x.tag === 2, - ); - const leafNodes: LeafNode[] = []; - for (const x of leafNodesData) { - const leafNode: LeafNode = this.program.coder.types.decode( - 'LeafNode', - Buffer.from([0, ...x.data]), - ); - leafNodes.push(leafNode); - } - return leafNodes; - } - public async createMarketIx( payer: PublicKey, name: string, @@ -622,7 +585,6 @@ export class OpenBookV2Client { marketPublicKey: PublicKey, market: MarketAccount, userTokenAccount: PublicKey, - openOrdersAdmin: PublicKey | null, args: PlaceOrderArgs, remainingAccounts: PublicKey[], openOrdersDelegate?: Keypair, @@ -637,13 +599,14 @@ export class OpenBookV2Client { isWritable: true, })); + const openOrdersAdmin = market.openOrdersAdmin.key.equals(PublicKey.default) + ? null + : market.openOrdersAdmin.key; + const ix = await this.program.methods .placeOrder(args) .accounts({ - signer: - openOrdersDelegate != null - ? openOrdersDelegate.publicKey - : this.walletPk, + signer: openOrdersDelegate?.publicKey ?? this.walletPk, asks: market.asks, bids: market.bids, marketVault, @@ -771,7 +734,7 @@ export class OpenBookV2Client { orderType: PlaceOrderType, bids: PlaceMultipleOrdersArgs[], asks: PlaceMultipleOrdersArgs[], - limit: number = 12, + limit = 12, openOrdersDelegate?: Keypair, ): Promise<[TransactionInstruction, Signer[]]> { const ix = await this.program.methods @@ -814,7 +777,7 @@ export class OpenBookV2Client { orderType: PlaceOrderType, bids: PlaceMultipleOrdersArgs[], asks: PlaceMultipleOrdersArgs[], - limit: number = 12, + limit = 12, openOrdersDelegate?: Keypair, ): Promise<[TransactionInstruction, Signer[]]> { const ix = await this.program.methods @@ -846,7 +809,7 @@ export class OpenBookV2Client { return [ix, signers]; } - public async cancelOrderById( + public async cancelOrderByIdIx( openOrdersPublicKey: PublicKey, openOrdersAccount: OpenOrdersAccount, market: MarketAccount, @@ -870,7 +833,7 @@ export class OpenBookV2Client { return [ix, signers]; } - public async cancelOrderByClientId( + public async cancelOrderByClientIdIx( openOrdersPublicKey: PublicKey, openOrdersAccount: OpenOrdersAccount, market: MarketAccount, @@ -894,7 +857,7 @@ export class OpenBookV2Client { return [ix, signers]; } - public async cancelAllOrders( + public async cancelAllOrdersIx( openOrdersPublicKey: PublicKey, openOrdersAccount: OpenOrdersAccount, market: MarketAccount, diff --git a/ts/client/src/index.ts b/ts/client/src/index.ts index a561c8dd6..5c6e325c1 100644 --- a/ts/client/src/index.ts +++ b/ts/client/src/index.ts @@ -1,7 +1,11 @@ import { IDL, type OpenbookV2 } from './openbook_v2'; export * from './client'; +export * from './accounts/bookSide'; +export * from './accounts/market'; +export * from './accounts/openOrders'; export * from './market'; export * from './utils/utils'; +export * from './utils/watcher'; export { IDL, type OpenbookV2 }; diff --git a/ts/client/src/market.ts b/ts/client/src/market.ts index 35fc62aaa..d9ea16d85 100644 --- a/ts/client/src/market.ts +++ b/ts/client/src/market.ts @@ -8,6 +8,7 @@ import { type MarketAccount, OPENBOOK_PROGRAM_ID, getFilteredProgramAccounts, + nameToString, } from './client'; import { utils, diff --git a/ts/client/src/structs/order.ts b/ts/client/src/structs/order.ts new file mode 100644 index 000000000..491e0aca4 --- /dev/null +++ b/ts/client/src/structs/order.ts @@ -0,0 +1,48 @@ +import { BN } from '@coral-xyz/anchor'; +import { LeafNode, Market, Side, SideUtils, U64_MAX_BN } from '..'; + +export class Order { + public seqNum: BN; + public priceLots: BN; + + constructor( + public market: Market, + public leafNode: LeafNode, + public side: Side, + public isExpired = false, + public isOraclePegged = false, + ) { + this.seqNum = + this.side === SideUtils.Bid + ? U64_MAX_BN.sub(this.leafNode.key.maskn(64)) + : this.leafNode.key.maskn(64); + const priceData = this.leafNode.key.ushrn(64); + if (this.isOraclePegged) { + const priceOffset = priceData.sub(new BN(1).ushln(63)); + throw new Error('Not implemented yet'); + // TODO: add oracle price logic to Market + } else { + this.priceLots = priceData; + } + } + + public get price(): number { + return this.market.priceLotsToUi(this.priceLots); + } + + public get size(): number { + return this.market.baseLotsToUi(this.leafNode.quantity); + } + + public get sizeLots(): BN { + return this.leafNode.quantity; + } + + public toPrettyString(): string { + return `side:${this.side === SideUtils.Bid ? 'bid' : 'ask'} price:${ + this.price + } size:${this.size} seqNum:${this.seqNum.toString()} expired:${ + this.isExpired + }`; + } +} diff --git a/ts/client/src/test/market.ts b/ts/client/src/test/market.ts new file mode 100644 index 000000000..2857063ce --- /dev/null +++ b/ts/client/src/test/market.ts @@ -0,0 +1,65 @@ +import { PublicKey } from '@solana/web3.js'; +import { + Market, + OPENBOOK_PROGRAM_ID, + findAccountsByMints, + findAllMarkets, + initReadOnlyOpenbookClient, + Watcher, + sleep, +} from '..'; + +async function testFindAccountsByMints(): Promise { + const client = initReadOnlyOpenbookClient(); + const accounts = await findAccountsByMints( + client.connection, + new PublicKey('So11111111111111111111111111111111111111112'), + new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), + OPENBOOK_PROGRAM_ID, + ); + console.log(accounts.map((a) => a.publicKey.toBase58())); +} + +async function testFindAllMarkets(): Promise { + const client = initReadOnlyOpenbookClient(); + const markets = await findAllMarkets( + client.connection, + OPENBOOK_PROGRAM_ID, + client.provider, + ); + console.log('markets', markets); +} + +async function testDecodeMarket(): Promise { + const client = initReadOnlyOpenbookClient(); + const marketPk = new PublicKey( + 'CFSMrBssNG8Ud1edW59jNLnq2cwrQ9uY5cM3wXmqRJj3', + ); + const market = await Market.load(client, marketPk); + await market.loadOrderBook(); + + console.log(market.toPrettyString()); +} + +async function testWatchMarket(): Promise { + const client = initReadOnlyOpenbookClient(); + const marketPk = new PublicKey( + 'CFSMrBssNG8Ud1edW59jNLnq2cwrQ9uY5cM3wXmqRJj3', + ); + const market = await Market.load(client, marketPk); + await market.loadOrderBook(); + + console.log('bids before sub', market.bids?.getL2(2)); + + const w = new Watcher(client.connection); + w.addMarket(market); + + await sleep(5_000); + + console.log('bids after sub', market.bids?.getL2(2)); +} + +// void testFindAccountsByMints(); +// void testFindAllMarkets(); +// void testDecodeMarket(); +void testWatchMarket(); diff --git a/ts/client/src/test/openOrders.ts b/ts/client/src/test/openOrders.ts new file mode 100644 index 000000000..f8bd395f2 --- /dev/null +++ b/ts/client/src/test/openOrders.ts @@ -0,0 +1,95 @@ +import { PublicKey } from '@solana/web3.js'; +import { + Market, + OpenOrders, + SideUtils, + initOpenbookClient, + initReadOnlyOpenbookClient, +} from '..'; +import { OpenOrdersIndexer } from '../accounts/openOrdersIndexer'; + +async function testLoadIndexerNonExistent(): Promise { + const client = initReadOnlyOpenbookClient(); + try { + const indexer = await OpenOrdersIndexer.load(client); + console.error('should not find', indexer); + process.exit(-1); + } catch (e) { + console.log('expected failure'); + } +} + +async function testLoadOOForMarket(): Promise { + const client = initOpenbookClient(); + const marketPk = new PublicKey( + 'CFSMrBssNG8Ud1edW59jNLnq2cwrQ9uY5cM3wXmqRJj3', + ); + const market = await Market.load(client, marketPk); + const [oo] = await Promise.all([ + OpenOrders.loadNullableForMarketAndOwner(market), + market.loadOrderBook(), + ]); + console.log(oo?.toPrettyString()); +} + +async function testPlaceAndCancelOrder(): Promise { + const client = initOpenbookClient(); + const marketPk = new PublicKey( + 'CFSMrBssNG8Ud1edW59jNLnq2cwrQ9uY5cM3wXmqRJj3', + ); + const market = await Market.load(client, marketPk); + + console.log(market.toPrettyString()); + const [oo] = await Promise.all([ + OpenOrders.loadNullableForMarketAndOwner(market), + market.loadOrderBook(), + ]); + + const sigPlace = await oo?.placeOrder({ + side: SideUtils.Bid, + price: market.tickSize, + size: market.minOrderSize, + }); + + console.log('placed order', sigPlace); + + await Promise.all([oo?.reload(), market.loadBids()]); + + console.log(oo?.toPrettyString()); + + const sigCancel = await oo?.cancelOrder(oo.items().next().value); + + console.log('cancelled order', sigCancel); +} + +async function testPlaceAndCancelOrderByClientId(): Promise { + const client = initOpenbookClient(); + const marketPk = new PublicKey( + 'CFSMrBssNG8Ud1edW59jNLnq2cwrQ9uY5cM3wXmqRJj3', + ); + const market = await Market.load(client, marketPk); + + console.log(market.toPrettyString()); + const [oo] = await Promise.all([ + OpenOrders.loadNullableForMarketAndOwner(market), + market.loadOrderBook(), + ]); + + const sigPlace = await oo?.placeOrder({ + side: SideUtils.Bid, + price: market.tickSize, + size: market.minOrderSize, + clientOrderId: 9999, + }); + + console.log('placed order', sigPlace); + + const sigCancel = await oo?.cancelOrder({ clientOrderId: 9999 }); + + console.log('cancelled order', sigCancel); +} + +// testLoadIndexerNonExistent(); +void testLoadOOForMarket(); +// testPlaceAndCancelOrder(); +// testPlaceAndCancelOrderByClientId(); diff --git a/ts/client/src/utils/rpc.ts b/ts/client/src/utils/rpc.ts index a97c0d917..d9dc69941 100644 --- a/ts/client/src/utils/rpc.ts +++ b/ts/client/src/utils/rpc.ts @@ -17,6 +17,8 @@ export async function sendTransaction( opts: any = {}, ): Promise { const connection = provider.connection; + const additionalSigners = opts?.additionalSigners || []; + if ((connection as any).banksClient !== undefined) { const tx = new Transaction(); for (const ix of ixs) { @@ -27,13 +29,14 @@ export async function sendTransaction( connection as any ).banksClient.getLatestBlockhash(); - for (const signer of opts?.additionalSigners) { + for (const signer of additionalSigners) { tx.partialSign(signer); } await (connection as any).banksClient.processTransaction(tx); return ''; } + const latestBlockhash = opts?.latestBlockhash ?? (await connection.getLatestBlockhash( @@ -49,18 +52,15 @@ export async function sendTransaction( } const message = MessageV0.compile({ - payerKey: provider.wallet.publicKey, + payerKey: payer.publicKey, instructions: ixs, recentBlockhash: latestBlockhash.blockhash, addressLookupTableAccounts: alts, }); let vtx = new VersionedTransaction(message); - if ( - opts?.additionalSigners !== undefined && - opts?.additionalSigners.length !== 0 - ) { - vtx.sign([...opts?.additionalSigners]); + if (additionalSigners !== undefined && additionalSigners.length !== 0) { + vtx.sign([...additionalSigners]); } if ( @@ -79,12 +79,7 @@ export async function sendTransaction( skipPreflight: true, // mergedOpts.skipPreflight, }); - // const signature = await connection.sendTransactionss( - // vtx as any as VersionedTransaction, - // { - // skipPreflight: true, - // }, - // ); + // console.log(`sent tx base64=${Buffer.from(vtx.serialize()).toString('base64')}`); if ( opts?.postSendTxCallback !== undefined && @@ -99,12 +94,12 @@ export async function sendTransaction( const txConfirmationCommitment = opts?.txConfirmationCommitment ?? 'processed'; - let status: any; + let result: any; if ( latestBlockhash.blockhash != null && latestBlockhash.lastValidBlockHeight != null ) { - status = ( + result = ( await connection.confirmTransaction( { signature: signature, @@ -115,15 +110,15 @@ export async function sendTransaction( ) ).value; } else { - status = ( + result = ( await connection.confirmTransaction(signature, txConfirmationCommitment) ).value; } - if (status.err !== '' && status.err !== null) { - console.warn('Tx status: ', status); + if (result.err !== '' && result.err !== null) { + console.warn('Tx failed result: ', result); throw new OpenBookError({ txid: signature, - message: `${JSON.stringify(status)}`, + message: `${JSON.stringify(result)}`, }); } diff --git a/ts/client/src/utils/utils.ts b/ts/client/src/utils/utils.ts index 2dbfa2290..06ba12620 100644 --- a/ts/client/src/utils/utils.ts +++ b/ts/client/src/utils/utils.ts @@ -1,4 +1,6 @@ import { + Connection, + Keypair, PublicKey, SystemProgram, TransactionInstruction, @@ -8,13 +10,15 @@ import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, } from '@solana/spl-token'; +import { OpenBookV2Client } from '..'; +import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; export const SideUtils = { Bid: { bid: {} }, Ask: { ask: {} }, }; -export const OrderType = { +export const PlaceOrderTypeUtils = { Limit: { limit: {} }, ImmediateOrCancel: { immediateOrCancel: {} }, PostOnly: { postOnly: {} }, @@ -22,7 +26,7 @@ export const OrderType = { PostOnlySlide: { postOnlySlide: {} }, }; -export const SelfTradeBehavior = { +export const SelfTradeBehaviorUtils = { DecrementTake: { decrementTake: {} }, CancelProvide: { cancelProvide: {} }, AbortTransaction: { abortTransaction: {} }, @@ -33,6 +37,7 @@ export const SelfTradeBehavior = { /// export const U64_MAX_BN = new BN('18446744073709551615'); export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64); +export const ORDER_FEE_UNIT: BN = new BN(1e6); export function bpsToDecimal(bps: number): number { return bps / 10000; @@ -43,7 +48,7 @@ export function percentageToDecimal(percentage: number): number { } export function toNative(uiAmount: number, decimals: number): BN { - return new BN((uiAmount * Math.pow(10, decimals)).toFixed(0)); + return new BN(Math.round(uiAmount * Math.pow(10, decimals))); } export function toUiDecimals(nativeAmount: number, decimals: number): number { @@ -108,3 +113,25 @@ export async function createAssociatedTokenAccountIdempotentInstruction( data: Buffer.from([0x1]), }); } + +export function initReadOnlyOpenbookClient(): OpenBookV2Client { + const conn = new Connection(process.env.SOL_RPC_URL!); + const stubWallet = new Wallet(Keypair.generate()); + const provider = new AnchorProvider(conn, stubWallet, {}); + return new OpenBookV2Client(provider); +} + +export function initOpenbookClient(): OpenBookV2Client { + const conn = new Connection(process.env.SOL_RPC_URL!, 'processed'); + const wallet = new Wallet( + Keypair.fromSecretKey(Uint8Array.from(JSON.parse(process.env.KEYPAIR!))), + ); + const provider = new AnchorProvider(conn, wallet, {}); + return new OpenBookV2Client(provider, undefined, { + prioritizationFee: 10_000, + }); +} + +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/ts/client/src/utils/watcher.ts b/ts/client/src/utils/watcher.ts new file mode 100644 index 000000000..9ae5a5947 --- /dev/null +++ b/ts/client/src/utils/watcher.ts @@ -0,0 +1,65 @@ +import { Connection } from '@solana/web3.js'; +import { BookSide, Market, OpenOrders } from '..'; + +export class Watcher { + accountSubs: { [pk: string]: number } = {}; + + constructor(public connection: Connection) {} + + addMarket(market: Market, includeBook = true): this { + const { client, asks, bids, pubkey } = market; + + this.accountSubs[pubkey.toBase58()] = this.connection.onAccountChange( + pubkey, + (ai) => { + market.account = client.program.coder.accounts.decode( + 'market', + ai.data, + ); + }, + ); + + if (includeBook && asks) { + this.addBookSide(asks); + } + if (includeBook && bids) { + this.addBookSide(bids); + } + return this; + } + + addBookSide(bookSide: BookSide): this { + const { market, pubkey } = bookSide; + this.accountSubs[pubkey.toBase58()] = this.connection.onAccountChange( + pubkey, + (ai) => { + bookSide.account = market.client.program.coder.accounts.decode( + 'bookSide', + ai.data, + ); + }, + ); + return this; + } + + addOpenOrders(openOrders: OpenOrders): this { + const { market, pubkey } = openOrders; + this.accountSubs[pubkey.toBase58()] = this.connection.onAccountChange( + pubkey, + (ai) => { + openOrders.account = market.client.program.coder.accounts.decode( + 'OpenOrders', + ai.data, + ); + }, + ); + return this; + } + + clear(): this { + for (const [_pk, sub] of Object.entries(this.accountSubs)) { + this.connection.removeAccountChangeListener(sub); + } + return this; + } +} diff --git a/tsconfig.json b/tsconfig.json index d6e310f3e..4f982d6b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "resolveJsonModule": true, "skipLibCheck": true, "strictNullChecks": true, - "target": "esnext" + "target": "esnext", }, "ts-node": { // these options are overrides used only by ts-node @@ -17,5 +17,7 @@ "module": "commonjs" } }, - "include": ["ts/client/src"] -} + "include": [ + "ts/client/src" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d0ec615ad..7590e1e40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43,6 +43,13 @@ bn.js "^5.1.2" buffer-layout "^1.2.0" +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -94,6 +101,24 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@noble/curves@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" @@ -144,13 +169,69 @@ dependencies: buffer "~6.0.3" -"@solana/spl-token@0.3.8": - version "0.3.8" - resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.8.tgz#8e9515ea876e40a4cc1040af865f61fc51d27edf" - integrity sha512-ogwGDcunP9Lkj+9CODOWMiVJEdRtqHAtX2rWF62KxnnSWtMZtV9rDhTrZFshiyJmxDnRL/1nKE1yJHg4jjs3gg== +"@solana/codecs-core@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-experimental.8618508.tgz#4f6709dd50e671267f3bea7d09209bc6471b7ad0" + integrity sha512-JCz7mKjVKtfZxkuDtwMAUgA7YvJcA2BwpZaA1NOLcted4OMC4Prwa3DUe3f3181ixPYaRyptbF0Ikq2MbDkYEA== + +"@solana/codecs-data-structures@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-experimental.8618508.tgz#c16a704ac0f743a2e0bf73ada42d830b3402d848" + integrity sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + "@solana/codecs-numbers" "2.0.0-experimental.8618508" + +"@solana/codecs-numbers@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-experimental.8618508.tgz#d84f9ed0521b22e19125eefc7d51e217fcaeb3e4" + integrity sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + +"@solana/codecs-strings@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-experimental.8618508.tgz#72457b884d9be80b59b263bcce73892b081e9402" + integrity sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + "@solana/codecs-numbers" "2.0.0-experimental.8618508" + +"@solana/options@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-experimental.8618508.tgz#95385340e85f9e8a81b2bfba089404a61c8e9520" + integrity sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + "@solana/codecs-numbers" "2.0.0-experimental.8618508" + +"@solana/spl-token-metadata@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.2.tgz#876e13432bd2960bd3cac16b9b0af63e69e37719" + integrity sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + "@solana/codecs-data-structures" "2.0.0-experimental.8618508" + "@solana/codecs-numbers" "2.0.0-experimental.8618508" + "@solana/codecs-strings" "2.0.0-experimental.8618508" + "@solana/options" "2.0.0-experimental.8618508" + "@solana/spl-type-length-value" "0.1.0" + +"@solana/spl-token@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.0.tgz#c057e82afd2b9ea59019525f3fbf6c9a20349a54" + integrity sha512-jjBIBG9IsclqQVl5Y82npGE6utdCh7Z9VFcF5qgJa5EUq2XgspW3Dt1wujWjH/vQDRnkp9zGO+BqQU/HhX/3wg== dependencies: "@solana/buffer-layout" "^4.0.0" "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-metadata" "^0.1.2" + buffer "^6.0.3" + +"@solana/spl-type-length-value@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@solana/spl-type-length-value/-/spl-type-length-value-0.1.0.tgz#b5930cf6c6d8f50c7ff2a70463728a4637a2f26b" + integrity sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA== + dependencies: buffer "^6.0.3" "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.77.3": @@ -174,6 +255,26 @@ rpc-websockets "^7.5.1" superstruct "^0.14.2" +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/bn.js@^5.1.0": version "5.1.5" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.5.tgz#2e0dacdcce2c0f16b905d20ff87aedbc6f7b4bf0" @@ -339,6 +440,16 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-walk@^8.1.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + +acorn@^8.4.1: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + acorn@^8.9.0: version "8.11.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" @@ -386,6 +497,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -691,6 +807,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-fetch@^3.1.5: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -783,6 +904,11 @@ diff@^3.1.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -2333,6 +2459,25 @@ ts-node@7.0.1: source-map-support "^0.5.6" yn "^2.0.0" +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsconfig-paths@^3.14.2, tsconfig-paths@^3.5.0: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" @@ -2455,6 +2600,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -2569,6 +2719,11 @@ yargs@16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"