diff --git a/idl/openbook_v2.json b/idl/openbook_v2.json index a756c6021..25c0d8b63 100644 --- a/idl/openbook_v2.json +++ b/idl/openbook_v2.json @@ -620,6 +620,120 @@ "option": "u128" } }, + { + "name": "placeOrders", + "docs": [ + "Place multiple orders" + ], + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "openOrdersAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "openOrdersAdmin", + "isMut": false, + "isSigner": true, + "isOptional": true + }, + { + "name": "userQuoteAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userBaseAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "market", + "isMut": true, + "isSigner": false + }, + { + "name": "bids", + "isMut": true, + "isSigner": false + }, + { + "name": "asks", + "isMut": true, + "isSigner": false + }, + { + "name": "eventHeap", + "isMut": true, + "isSigner": false + }, + { + "name": "marketQuoteVault", + "isMut": true, + "isSigner": false + }, + { + "name": "marketBaseVault", + "isMut": true, + "isSigner": false + }, + { + "name": "oracleA", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "oracleB", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ordersType", + "type": { + "defined": "PlaceOrderType" + } + }, + { + "name": "bids", + "type": { + "vec": { + "defined": "PlaceMultipleOrdersArgs" + } + } + }, + { + "name": "asks", + "type": { + "vec": { + "defined": "PlaceMultipleOrdersArgs" + } + } + }, + { + "name": "limit", + "type": "u8" + } + ], + "returns": { + "vec": { + "option": "u128" + } + } + }, { "name": "cancelAllAndPlaceOrders", "docs": [ diff --git a/package.json b/package.json index abbeaeeca..3f9155713 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openbook-dex/openbook-v2", - "version": "0.1.7", + "version": "0.1.9", "description": "Typescript Client for openbook-v2 program.", "repository": "https://github.com/openbook-dex/openbook-v2/", "author": { diff --git a/programs/openbook-v2/src/instructions/cancel_all_and_place_orders.rs b/programs/openbook-v2/src/instructions/cancel_all_and_place_orders.rs index b52cbb828..28ff24075 100644 --- a/programs/openbook-v2/src/instructions/cancel_all_and_place_orders.rs +++ b/programs/openbook-v2/src/instructions/cancel_all_and_place_orders.rs @@ -10,6 +10,7 @@ use crate::token_utils::*; #[allow(clippy::too_many_arguments)] pub fn cancel_all_and_place_orders<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, CancelAllAndPlaceOrders<'info>>, + cancel: bool, mut orders: Vec, limit: u8, ) -> Result>> { @@ -39,7 +40,9 @@ pub fn cancel_all_and_place_orders<'c: 'info, 'info>( clock.slot, )?; - book.cancel_all_orders(&mut open_orders_account, *market, u8::MAX, None)?; + if cancel { + book.cancel_all_orders(&mut open_orders_account, *market, u8::MAX, None)?; + } let mut base_amount = 0_u64; let mut quote_amount = 0_u64; diff --git a/programs/openbook-v2/src/lib.rs b/programs/openbook-v2/src/lib.rs index 47660e193..626a63bc2 100644 --- a/programs/openbook-v2/src/lib.rs +++ b/programs/openbook-v2/src/lib.rs @@ -259,6 +259,54 @@ pub mod openbook_v2 { Ok(None) } + /// Place multiple orders + pub fn place_orders<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, CancelAllAndPlaceOrders<'info>>, + orders_type: PlaceOrderType, + bids: Vec, + asks: Vec, + limit: u8, + ) -> Result>> { + let n_bids = bids.len(); + + let mut orders = vec![]; + for (i, order) in bids.into_iter().chain(asks).enumerate() { + require_gte!(order.price_lots, 1, OpenBookError::InvalidInputPriceLots); + + let time_in_force = match Order::tif_from_expiry(order.expiry_timestamp) { + Some(t) => t, + None => { + msg!("Order is already expired"); + continue; + } + }; + orders.push(Order { + side: if i < n_bids { Side::Bid } else { Side::Ask }, + max_base_lots: i64::MIN, // this will be overriden to max_base_lots + max_quote_lots_including_fees: order.max_quote_lots_including_fees, + client_order_id: i as u64, + time_in_force, + self_trade_behavior: SelfTradeBehavior::CancelProvide, + params: match orders_type { + PlaceOrderType::Market => OrderParams::Market, + PlaceOrderType::ImmediateOrCancel => OrderParams::ImmediateOrCancel { + price_lots: order.price_lots, + }, + _ => OrderParams::Fixed { + price_lots: order.price_lots, + order_type: orders_type.to_post_order_type()?, + }, + }, + }); + } + + #[cfg(feature = "enable-gpl")] + return instructions::cancel_all_and_place_orders(ctx, false, orders, limit); + + #[cfg(not(feature = "enable-gpl"))] + Ok(vec![]) + } + /// Cancel orders and place multiple orders. pub fn cancel_all_and_place_orders<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, CancelAllAndPlaceOrders<'info>>, @@ -301,7 +349,7 @@ pub mod openbook_v2 { } #[cfg(feature = "enable-gpl")] - return instructions::cancel_all_and_place_orders(ctx, orders, limit); + return instructions::cancel_all_and_place_orders(ctx, true, orders, limit); #[cfg(not(feature = "enable-gpl"))] Ok(vec![]) diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index e746a069a..8ee2c0fe1 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -29,11 +29,12 @@ import { } from '@solana/web3.js'; import { IDL, type OpenbookV2 } from './openbook_v2'; import { sendTransaction } from './utils/rpc'; -import { Side } from './utils/utils'; +import { SideUtils } from './utils/utils'; 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 PlaceOrderPeggedArgs = IdlTypes['PlaceOrderPeggedArgs']; export type PlaceMultipleOrdersArgs = IdlTypes['PlaceMultipleOrdersArgs']; @@ -627,7 +628,9 @@ export class OpenBookV2Client { openOrdersDelegate?: Keypair, ): Promise<[TransactionInstruction, Signer[]]> { const marketVault = - args.side === Side.Bid ? market.marketQuoteVault : market.marketBaseVault; + args.side === SideUtils.Bid + ? market.marketQuoteVault + : market.marketBaseVault; const accountsMeta: AccountMeta[] = remainingAccounts.map((remaining) => ({ pubkey: remaining, isSigner: false, @@ -673,7 +676,9 @@ export class OpenBookV2Client { openOrdersDelegate?: Keypair, ): Promise<[TransactionInstruction, Signer[]]> { const marketVault = - args.side === Side.Bid ? market.marketQuoteVault : market.marketBaseVault; + args.side === SideUtils.Bid + ? market.marketQuoteVault + : market.marketBaseVault; const accountsMeta: AccountMeta[] = remainingAccounts.map((remaining) => ({ pubkey: remaining, isSigner: false, @@ -798,6 +803,49 @@ export class OpenBookV2Client { return [ix, signers]; } + // Use OrderType from './utils/utils' for orderType + public async placeOrdersIx( + openOrdersPublicKey: PublicKey, + marketPublicKey: PublicKey, + market: MarketAccount, + userBaseAccount: PublicKey, + userQuoteAccount: PublicKey, + openOrdersAdmin: PublicKey | null, + orderType: PlaceOrderType, + bids: PlaceMultipleOrdersArgs[], + asks: PlaceMultipleOrdersArgs[], + limit: number = 12, + openOrdersDelegate?: Keypair, + ): Promise<[TransactionInstruction, Signer[]]> { + const ix = await this.program.methods + .placeOrders(orderType, bids, asks, limit) + .accounts({ + signer: + openOrdersDelegate != null + ? openOrdersDelegate.publicKey + : this.walletPk, + asks: market.asks, + bids: market.bids, + marketQuoteVault: market.marketQuoteVault, + marketBaseVault: market.marketBaseVault, + eventHeap: market.eventHeap, + market: marketPublicKey, + openOrdersAccount: openOrdersPublicKey, + oracleA: market.oracleA.key, + oracleB: market.oracleB.key, + userBaseAccount, + userQuoteAccount, + tokenProgram: TOKEN_PROGRAM_ID, + openOrdersAdmin, + }) + .instruction(); + const signers: Signer[] = []; + if (openOrdersDelegate != null) { + signers.push(openOrdersDelegate); + } + return [ix, signers]; + } + public async cancelOrderById( openOrdersPublicKey: PublicKey, openOrdersAccount: OpenOrdersAccount, @@ -846,6 +894,31 @@ export class OpenBookV2Client { return [ix, signers]; } + public async cancelAllOrders( + openOrdersPublicKey: PublicKey, + openOrdersAccount: OpenOrdersAccount, + market: MarketAccount, + limit: number, + side: Side | null, + openOrdersDelegate?: Keypair, + ): Promise<[TransactionInstruction, Signer[]]> { + const ix = await this.program.methods + .cancelAllOrders(side, limit) + .accounts({ + signer: openOrdersAccount.owner, + asks: market.asks, + bids: market.bids, + market: openOrdersAccount.market, + openOrdersAccount: openOrdersPublicKey, + }) + .instruction(); + const signers: Signer[] = []; + if (openOrdersDelegate != null) { + signers.push(openOrdersDelegate); + } + return [ix, signers]; + } + public async closeOpenOrdersIndexerIx( owner: Keypair, market: MarketAccount, diff --git a/ts/client/src/market.ts b/ts/client/src/market.ts index 02c8fd107..35fc62aaa 100644 --- a/ts/client/src/market.ts +++ b/ts/client/src/market.ts @@ -16,7 +16,7 @@ import { getProvider, BN, } from '@coral-xyz/anchor'; -import { QUOTE_DECIMALS, toNative, toUiDecimals } from './utils/utils'; +import { toNative, toUiDecimals } from './utils/utils'; import Big from 'big.js'; import { IDL, type OpenbookV2 } from './openbook_v2'; const BATCH_TX_SIZE = 50; @@ -124,7 +124,7 @@ export async function findAllMarkets( function priceLotsToUiConverter(market: MarketAccount): number { return new Big(10) - .pow(market.baseDecimals - QUOTE_DECIMALS) + .pow(market.baseDecimals - market.quoteDecimals) .mul(new Big(market.quoteLotSize.toString())) .div(new Big(market.baseLotSize.toString())) .toNumber(); @@ -142,7 +142,7 @@ function quoteLotsToUiConverter(market: MarketAccount): number { } export function uiPriceToLots(market: MarketAccount, price: number): BN { - return toNative(price, QUOTE_DECIMALS) + return toNative(price, market.quoteDecimals) .mul(market.baseLotSize) .div(market.quoteLotSize.mul(new BN(Math.pow(10, market.baseDecimals)))); } @@ -152,7 +152,7 @@ export function uiBaseToLots(market: MarketAccount, quantity: number): BN { } export function uiQuoteToLots(market: MarketAccount, uiQuote: number): BN { - return toNative(uiQuote, QUOTE_DECIMALS).div(market.quoteLotSize); + return toNative(uiQuote, market.quoteDecimals).div(market.quoteLotSize); } export function priceLotsToNative(market: MarketAccount, price: BN): BN { @@ -164,7 +164,7 @@ export function priceLotsToUi(market: MarketAccount, price: BN): number { } export function priceNativeToUi(market: MarketAccount, price: number): number { - return toUiDecimals(price, QUOTE_DECIMALS - market.baseDecimals); + return toUiDecimals(price, market.quoteDecimals - market.baseDecimals); } export function baseLotsToUi(market: MarketAccount, quantity: BN): number { diff --git a/ts/client/src/openbook_v2.ts b/ts/client/src/openbook_v2.ts index c8b11ddf5..91e38a549 100644 --- a/ts/client/src/openbook_v2.ts +++ b/ts/client/src/openbook_v2.ts @@ -616,6 +616,118 @@ export interface OpenbookV2 { option: 'u128'; }; }, + { + name: 'placeOrders'; + docs: ['Place multiple orders']; + accounts: [ + { + name: 'signer'; + isMut: false; + isSigner: true; + }, + { + name: 'openOrdersAccount'; + isMut: true; + isSigner: false; + }, + { + name: 'openOrdersAdmin'; + isMut: false; + isSigner: true; + isOptional: true; + }, + { + name: 'userQuoteAccount'; + isMut: true; + isSigner: false; + }, + { + name: 'userBaseAccount'; + isMut: true; + isSigner: false; + }, + { + name: 'market'; + isMut: true; + isSigner: false; + }, + { + name: 'bids'; + isMut: true; + isSigner: false; + }, + { + name: 'asks'; + isMut: true; + isSigner: false; + }, + { + name: 'eventHeap'; + isMut: true; + isSigner: false; + }, + { + name: 'marketQuoteVault'; + isMut: true; + isSigner: false; + }, + { + name: 'marketBaseVault'; + isMut: true; + isSigner: false; + }, + { + name: 'oracleA'; + isMut: false; + isSigner: false; + isOptional: true; + }, + { + name: 'oracleB'; + isMut: false; + isSigner: false; + isOptional: true; + }, + { + name: 'tokenProgram'; + isMut: false; + isSigner: false; + }, + ]; + args: [ + { + name: 'ordersType'; + type: { + defined: 'PlaceOrderType'; + }; + }, + { + name: 'bids'; + type: { + vec: { + defined: 'PlaceMultipleOrdersArgs'; + }; + }; + }, + { + name: 'asks'; + type: { + vec: { + defined: 'PlaceMultipleOrdersArgs'; + }; + }; + }, + { + name: 'limit'; + type: 'u8'; + }, + ]; + returns: { + vec: { + option: 'u128'; + }; + }; + }, { name: 'cancelAllAndPlaceOrders'; docs: ['Cancel orders and place multiple orders.']; @@ -4189,6 +4301,118 @@ export const IDL: OpenbookV2 = { option: 'u128', }, }, + { + name: 'placeOrders', + docs: ['Place multiple orders'], + accounts: [ + { + name: 'signer', + isMut: false, + isSigner: true, + }, + { + name: 'openOrdersAccount', + isMut: true, + isSigner: false, + }, + { + name: 'openOrdersAdmin', + isMut: false, + isSigner: true, + isOptional: true, + }, + { + name: 'userQuoteAccount', + isMut: true, + isSigner: false, + }, + { + name: 'userBaseAccount', + isMut: true, + isSigner: false, + }, + { + name: 'market', + isMut: true, + isSigner: false, + }, + { + name: 'bids', + isMut: true, + isSigner: false, + }, + { + name: 'asks', + isMut: true, + isSigner: false, + }, + { + name: 'eventHeap', + isMut: true, + isSigner: false, + }, + { + name: 'marketQuoteVault', + isMut: true, + isSigner: false, + }, + { + name: 'marketBaseVault', + isMut: true, + isSigner: false, + }, + { + name: 'oracleA', + isMut: false, + isSigner: false, + isOptional: true, + }, + { + name: 'oracleB', + isMut: false, + isSigner: false, + isOptional: true, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'ordersType', + type: { + defined: 'PlaceOrderType', + }, + }, + { + name: 'bids', + type: { + vec: { + defined: 'PlaceMultipleOrdersArgs', + }, + }, + }, + { + name: 'asks', + type: { + vec: { + defined: 'PlaceMultipleOrdersArgs', + }, + }, + }, + { + name: 'limit', + type: 'u8', + }, + ], + returns: { + vec: { + option: 'u128', + }, + }, + }, { name: 'cancelAllAndPlaceOrders', docs: ['Cancel orders and place multiple orders.'], diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts deleted file mode 100644 index 65b93592c..000000000 --- a/ts/client/src/utils.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { - PublicKey, - SystemProgram, - TransactionInstruction, -} from '@solana/web3.js'; -import BN from 'bn.js'; -import { - MintLayout, - ASSOCIATED_TOKEN_PROGRAM_ID, - type RawMint, - TOKEN_PROGRAM_ID, -} from '@solana/spl-token'; -/// -/// numeric helpers -/// -export const U64_MAX_BN = new BN('18446744073709551615'); -export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64); -export const QUOTE_DECIMALS = 6; - -export function bpsToDecimal(bps: number): number { - return bps / 10000; -} - -export function percentageToDecimal(percentage: number): number { - return percentage / 100; -} - -export function toNative(uiAmount: number, decimals: number): BN { - return new BN((uiAmount * Math.pow(10, decimals)).toFixed(0)); -} - -export function toUiDecimals(nativeAmount: number, decimals: number): number { - return nativeAmount / Math.pow(10, decimals); -} - -export function nativeToUiDecimalsForQuote( - nativeAmount: number, - quoteDecimals = QUOTE_DECIMALS, -): number { - return toUiDecimals(nativeAmount, QUOTE_DECIMALS); -} - -export function nativeToUiDecimalsForBase( - nativeAmount: number, - baseDecimals: number, -): number { - return toUiDecimals(nativeAmount, baseDecimals); -} - -export function roundTo5(number): number { - if (number < 1) { - const numString = number.toString(); - const nonZeroIndex = numString.search(/[1-9]/); - if (nonZeroIndex === -1 || nonZeroIndex >= numString.length - 5) { - return number; - } - return Number(numString.slice(0, (nonZeroIndex as number) + 5)); - } else if (number < 10) { - return ( - Math.floor(number) + - Number((number % 1).toString().padEnd(10, '0').slice(0, 6)) - ); - } else if (number < 100) { - return ( - Math.floor(number) + - Number((number % 1).toString().padEnd(10, '0').slice(0, 5)) - ); - } else if (number < 1000) { - return ( - Math.floor(number) + - Number((number % 1).toString().padEnd(10, '0').slice(0, 4)) - ); - } else if (number < 10000) { - return ( - Math.floor(number) + - Number((number % 1).toString().padEnd(10, '0').slice(0, 3)) - ); - } - return Math.round(number); -} - -/// - -/// -/// web3js extensions -/// - -/** - * Get the address of the associated token account for a given mint and owner - * - * @param mint Token mint account - * @param owner Owner of the new account - * @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address) - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * - * @return Address of the associated token account - */ -export async function getAssociatedTokenAddress( - mint: PublicKey, - owner: PublicKey, - allowOwnerOffCurve = true, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -): Promise { - if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer())) - throw new Error('TokenOwnerOffCurve!'); - - const [address] = await PublicKey.findProgramAddress( - [owner.toBuffer(), programId.toBuffer(), mint.toBuffer()], - associatedTokenProgramId, - ); - - return address; -} - -export async function createAssociatedTokenAccountIdempotentInstruction( - payer: PublicKey, - owner: PublicKey, - mint: PublicKey, -): Promise { - const account = await getAssociatedTokenAddress(mint, owner); - return new TransactionInstruction({ - keys: [ - { pubkey: payer, isSigner: true, isWritable: true }, - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: owner, isSigner: false, isWritable: false }, - { pubkey: mint, isSigner: false, isWritable: false }, - { - pubkey: SystemProgram.programId, - isSigner: false, - isWritable: false, - }, - { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, - ], - programId: ASSOCIATED_TOKEN_PROGRAM_ID, - data: Buffer.from([0x1]), - }); -} - -export function decodeMint(data: Buffer): RawMint { - return MintLayout.decode(data); -} diff --git a/ts/client/src/utils/utils.ts b/ts/client/src/utils/utils.ts index 1b64c0d14..2dbfa2290 100644 --- a/ts/client/src/utils/utils.ts +++ b/ts/client/src/utils/utils.ts @@ -9,7 +9,7 @@ import { TOKEN_PROGRAM_ID, } from '@solana/spl-token'; -export const Side = { +export const SideUtils = { Bid: { bid: {} }, Ask: { ask: {} }, }; @@ -50,12 +50,6 @@ export function toUiDecimals(nativeAmount: number, decimals: number): number { return nativeAmount / Math.pow(10, decimals); } -export const QUOTE_DECIMALS = 6; - -export function toUiDecimalsForQuote(nativeAmount: number): number { - return toUiDecimals(nativeAmount, QUOTE_DECIMALS); -} - /// ///