diff --git a/.changeset/weak-bushes-decide.md b/.changeset/weak-bushes-decide.md new file mode 100644 index 000000000..9a0f0d7d7 --- /dev/null +++ b/.changeset/weak-bushes-decide.md @@ -0,0 +1,5 @@ +--- +'@mysten/deepbook-v3': minor +--- + +Take Profit Stop Loss support diff --git a/packages/deepbook-v3/.gitignore b/packages/deepbook-v3/.gitignore index 45ac06150..0f568a7bd 100644 --- a/packages/deepbook-v3/.gitignore +++ b/packages/deepbook-v3/.gitignore @@ -135,3 +135,4 @@ dist example.ts marginSetup.ts liquidations.ts +tpslExecution.ts diff --git a/packages/deepbook-v3/examples/tpslExample.ts b/packages/deepbook-v3/examples/tpslExample.ts new file mode 100644 index 000000000..1c9229244 --- /dev/null +++ b/packages/deepbook-v3/examples/tpslExample.ts @@ -0,0 +1,269 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/** + * This example demonstrates how to use the Take Profit / Stop Loss (TPSL) functionality + * in the DeepBook margin trading system. + * + * TPSL allows users to set conditional orders that automatically execute when + * the price reaches a specified trigger level: + * - Take Profit: Trigger when price goes above a threshold (sell to lock in profits) + * - Stop Loss: Trigger when price goes below a threshold (sell to limit losses) + */ + +import { execSync } from 'child_process'; +import { getFullnodeUrl, SuiClient } from '@mysten/sui/client'; +import { Transaction } from '@mysten/sui/transactions'; + +import { DeepBookClient, OrderType, SelfMatchingOptions } from '../src/index.js'; + +const SUI = process.env.SUI_BINARY ?? `sui`; + +export const getActiveAddress = () => { + return execSync(`${SUI} client active-address`, { encoding: 'utf8' }).trim(); +}; + +export type Network = 'mainnet' | 'testnet' | 'devnet' | 'localnet'; + +(async () => { + // ============================================================================ + // CONFIGURATION + // ============================================================================ + const env = 'testnet'; + + // Configure margin managers - update these with your actual margin manager addresses + const marginManagers = { + MARGIN_MANAGER_1: { + address: '0x20f689b98e9afe22b5f4ec2e7e39a1b5fbbbb09e4f1f580a387dcc2015a9abda', + poolKey: 'SUI_DBUSDC', + }, + }; + + const dbClient = new DeepBookClient({ + address: getActiveAddress(), + env: env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + marginManagers, + }); + + // ============================================================================ + // SECTION 1: READ-ONLY FUNCTIONS + // These functions query the state of conditional orders without modifying anything + // ============================================================================ + + console.log('\n=== SECTION 1: READ-ONLY FUNCTIONS ===\n'); + + // 1.1 Get all conditional order IDs for a margin manager + try { + const conditionalOrderIds = await dbClient.getConditionalOrderIds('MARGIN_MANAGER_1'); + console.log('Conditional Order IDs:', conditionalOrderIds); + } catch (error) { + console.log('Error getting conditional order IDs:', error); + } + + // 1.2 Get the lowest trigger price for trigger_above orders (take profit for shorts) + // Returns MAX_U64 if there are no trigger_above orders + try { + const lowestTriggerAbove = await dbClient.getLowestTriggerAbovePrice('MARGIN_MANAGER_1'); + console.log('Lowest Trigger Above Price:', lowestTriggerAbove.toString()); + } catch (error) { + console.log('Error getting lowest trigger above price:', error); + } + + // 1.3 Get the highest trigger price for trigger_below orders (stop loss for longs) + // Returns 0 if there are no trigger_below orders + try { + const highestTriggerBelow = await dbClient.getHighestTriggerBelowPrice('MARGIN_MANAGER_1'); + console.log('Highest Trigger Below Price:', highestTriggerBelow.toString()); + } catch (error) { + console.log('Error getting highest trigger below price:', error); + } + + // 1.4 Get manager state which includes TPSL trigger prices + try { + const managerState = await dbClient.getMarginManagerState('MARGIN_MANAGER_1'); + console.log('Manager State:', { + managerId: managerState.managerId, + riskRatio: managerState.riskRatio, + currentPrice: managerState.currentPrice.toString(), + lowestTriggerAbovePrice: managerState.lowestTriggerAbovePrice.toString(), + highestTriggerBelowPrice: managerState.highestTriggerBelowPrice.toString(), + }); + } catch (error) { + console.log('Error getting manager state:', error); + } + + // ============================================================================ + // SECTION 2: TRANSACTION FUNCTIONS + // These functions create transactions that modify the state of conditional orders + // ============================================================================ + + console.log('\n=== SECTION 2: TRANSACTION FUNCTIONS ===\n'); + + const tx = new Transaction(); + + // Update Pyth price feeds (required for TPSL operations) + await dbClient.getPriceInfoObject(tx, 'SUI'); + await dbClient.getPriceInfoObject(tx, 'DBUSDC'); + + // ---------------------------------------------------------------------------- + // 2.1 Add a Stop Loss order (trigger when price drops BELOW threshold) + // This is useful for limiting losses on a long position + // ---------------------------------------------------------------------------- + console.log('Adding Stop Loss order (trigger below price)...'); + + // Stop Loss with a limit order + dbClient.marginTPSL.addConditionalOrder({ + marginManagerKey: 'MARGIN_MANAGER_1', + conditionalOrderId: '1001', // Unique ID for this conditional order + triggerBelowPrice: true, // Trigger when price goes BELOW trigger price + triggerPrice: 3.5, // Trigger when price drops below 3.5 + pendingOrder: { + // This is a limit order that will be placed when triggered + clientOrderId: '2001', + orderType: OrderType.IMMEDIATE_OR_CANCEL, // IOC - execute immediately or cancel + selfMatchingOption: SelfMatchingOptions.SELF_MATCHING_ALLOWED, + price: 3.4, // Sell at 3.4 or better + quantity: 10, // Sell 10 units + isBid: false, // This is a sell order + payWithDeep: true, + expireTimestamp: Date.now() + 7 * 24 * 60 * 60 * 1000, // Expires in 7 days + }, + })(tx); + + // Stop Loss with a market order (simpler, but may have slippage) + dbClient.marginTPSL.addConditionalOrder({ + marginManagerKey: 'MARGIN_MANAGER_1', + conditionalOrderId: '1002', + triggerBelowPrice: true, + triggerPrice: 3.0, // Trigger when price drops below 3.0 + pendingOrder: { + // This is a market order - no price specified + clientOrderId: '2002', + selfMatchingOption: SelfMatchingOptions.SELF_MATCHING_ALLOWED, + quantity: 5, + isBid: false, // Sell order + payWithDeep: true, + }, + })(tx); + + // ---------------------------------------------------------------------------- + // 2.2 Add a Take Profit order (trigger when price rises ABOVE threshold) + // This is useful for locking in profits on a long position + // ---------------------------------------------------------------------------- + console.log('Adding Take Profit order (trigger above price)...'); + + // Take Profit with a limit order + dbClient.marginTPSL.addConditionalOrder({ + marginManagerKey: 'MARGIN_MANAGER_1', + conditionalOrderId: '1003', + triggerBelowPrice: false, // Trigger when price goes ABOVE trigger price + triggerPrice: 5.0, // Trigger when price rises above 5.0 + pendingOrder: { + clientOrderId: '2003', + orderType: OrderType.NO_RESTRICTION, + selfMatchingOption: SelfMatchingOptions.SELF_MATCHING_ALLOWED, + price: 4.9, // Sell at 4.9 or better + quantity: 10, + isBid: false, // Sell order to take profits + payWithDeep: true, + expireTimestamp: Date.now() + 30 * 24 * 60 * 60 * 1000, // Expires in 30 days + }, + })(tx); + + // Take Profit with a market order + dbClient.marginTPSL.addConditionalOrder({ + marginManagerKey: 'MARGIN_MANAGER_1', + conditionalOrderId: '1004', + triggerBelowPrice: false, + triggerPrice: 6.0, // Trigger when price rises above 6.0 + pendingOrder: { + clientOrderId: '2004', + selfMatchingOption: SelfMatchingOptions.SELF_MATCHING_ALLOWED, + quantity: 5, + isBid: false, + payWithDeep: true, + }, + })(tx); + + // ---------------------------------------------------------------------------- + // 2.3 Cancel a specific conditional order + // ---------------------------------------------------------------------------- + console.log('Canceling a specific conditional order...'); + + dbClient.marginTPSL.cancelConditionalOrder('MARGIN_MANAGER_1', '1002')(tx); + + // ---------------------------------------------------------------------------- + // 2.4 Cancel all conditional orders for a margin manager + // ---------------------------------------------------------------------------- + console.log('Canceling all conditional orders...'); + + dbClient.marginTPSL.cancelAllConditionalOrders('MARGIN_MANAGER_1')(tx); + + // ---------------------------------------------------------------------------- + // 2.5 Execute conditional orders that have been triggered + // This is a permissionless function - anyone can call it to execute + // triggered orders and earn keeper rewards + // ---------------------------------------------------------------------------- + console.log('Executing triggered conditional orders...'); + + // Execute up to 10 triggered orders + dbClient.marginTPSL.executeConditionalOrders('MARGIN_MANAGER_1', 10)(tx); + + // ============================================================================ + // SECTION 3: USING HELPER FUNCTIONS DIRECTLY (Advanced Usage) + // These functions allow you to build conditional orders manually + // ============================================================================ + + console.log('\n=== SECTION 3: ADVANCED - USING HELPER FUNCTIONS ===\n'); + + const tx2 = new Transaction(); + + // Update Pyth price feeds + await dbClient.getPriceInfoObject(tx2, 'SUI'); + await dbClient.getPriceInfoObject(tx2, 'DBUSDC'); + + // 3.1 Create a condition manually + const condition = dbClient.marginTPSL.newCondition( + 'SUI_DBUSDC', // Pool key for price calculation + true, // triggerBelowPrice + 4.0, // triggerPrice + )(tx2); + + // 3.2 Create a pending limit order manually + const pendingLimitOrder = dbClient.marginTPSL.newPendingLimitOrder('SUI_DBUSDC', { + clientOrderId: '3001', + orderType: OrderType.IMMEDIATE_OR_CANCEL, + selfMatchingOption: SelfMatchingOptions.SELF_MATCHING_ALLOWED, + price: 3.9, + quantity: 5, + isBid: false, + payWithDeep: true, + expireTimestamp: Date.now() + 7 * 24 * 60 * 60 * 1000, + })(tx2); + + // 3.3 Create a pending market order manually + const pendingMarketOrder = dbClient.marginTPSL.newPendingMarketOrder('SUI_DBUSDC', { + clientOrderId: '3002', + selfMatchingOption: SelfMatchingOptions.SELF_MATCHING_ALLOWED, + quantity: 2, + isBid: true, // Buy order + payWithDeep: true, + })(tx2); + + console.log('Created condition and pending orders (for demonstration)'); + console.log('Condition result:', condition); + console.log('Pending limit order result:', pendingLimitOrder); + console.log('Pending market order result:', pendingMarketOrder); + + // ============================================================================ + // SIGN AND EXECUTE TRANSACTIONS + // Sign and execute the transactions using your preferred method. + // The Transaction objects `tx` and `tx2` are ready to be signed and executed. + // ============================================================================ + + console.log('\n=== TPSL Example Complete ==='); + console.log('Transaction objects `tx` and `tx2` are ready to be signed and executed.'); +})(); diff --git a/packages/deepbook-v3/src/client.ts b/packages/deepbook-v3/src/client.ts index 633fcced6..ee994aad3 100644 --- a/packages/deepbook-v3/src/client.ts +++ b/packages/deepbook-v3/src/client.ts @@ -34,6 +34,7 @@ import { MarginLiquidationsContract } from './transactions/marginLiquidations.js import { SuiPriceServiceConnection } from './pyth/pyth.js'; import { SuiPythClient } from './pyth/pyth.js'; import { PoolProxyContract } from './transactions/poolProxy.js'; +import { MarginTPSLContract } from './transactions/marginTPSL.js'; /** * DeepBookClient class for managing DeepBook operations. @@ -54,6 +55,7 @@ export class DeepBookClient { marginRegistry: MarginRegistryContract; marginLiquidations: MarginLiquidationsContract; poolProxy: PoolProxyContract; + marginTPSL: MarginTPSLContract; /** * @param {SuiClient} client SuiClient instance @@ -115,6 +117,7 @@ export class DeepBookClient { this.marginRegistry = new MarginRegistryContract(this.#config); this.marginLiquidations = new MarginLiquidationsContract(this.#config); this.poolProxy = new PoolProxyContract(this.#config); + this.marginTPSL = new MarginTPSLContract(this.#config); } /** @@ -1700,6 +1703,86 @@ export class DeepBookClient { ); } + // === Margin TPSL (Take Profit / Stop Loss) Read-Only Functions === + + /** + * @description Get all conditional order IDs for a margin manager + * @param {string} marginManagerKey The key to identify the margin manager + * @returns {Promise} Array of conditional order IDs + */ + async getConditionalOrderIds(marginManagerKey: string): Promise { + const manager = this.#config.getMarginManager(marginManagerKey); + const tx = new Transaction(); + tx.add(this.marginTPSL.conditionalOrderIds(manager.poolKey, manager.address)); + + const res = await this.client.devInspectTransactionBlock({ + sender: normalizeSuiAddress(this.#address), + transactionBlock: tx, + }); + + if (!res.results || !res.results[0] || !res.results[0].returnValues) { + throw new Error( + `Failed to get conditional order IDs: ${res.effects?.status?.error || 'Unknown error'}`, + ); + } + + const bytes = res.results[0].returnValues[0][0]; + const orderIds = bcs.vector(bcs.u64()).parse(new Uint8Array(bytes)); + return orderIds.map((id) => id.toString()); + } + + /** + * @description Get the lowest trigger price for trigger_above orders + * Returns MAX_U64 if there are no trigger_above orders + * @param {string} marginManagerKey The key to identify the margin manager + * @returns {Promise} The lowest trigger above price + */ + async getLowestTriggerAbovePrice(marginManagerKey: string): Promise { + const manager = this.#config.getMarginManager(marginManagerKey); + const tx = new Transaction(); + tx.add(this.marginTPSL.lowestTriggerAbovePrice(manager.poolKey, manager.address)); + + const res = await this.client.devInspectTransactionBlock({ + sender: normalizeSuiAddress(this.#address), + transactionBlock: tx, + }); + + if (!res.results || !res.results[0] || !res.results[0].returnValues) { + throw new Error( + `Failed to get lowest trigger above price: ${res.effects?.status?.error || 'Unknown error'}`, + ); + } + + const bytes = res.results[0].returnValues[0][0]; + return BigInt(bcs.U64.parse(new Uint8Array(bytes))); + } + + /** + * @description Get the highest trigger price for trigger_below orders + * Returns 0 if there are no trigger_below orders + * @param {string} marginManagerKey The key to identify the margin manager + * @returns {Promise} The highest trigger below price + */ + async getHighestTriggerBelowPrice(marginManagerKey: string): Promise { + const manager = this.#config.getMarginManager(marginManagerKey); + const tx = new Transaction(); + tx.add(this.marginTPSL.highestTriggerBelowPrice(manager.poolKey, manager.address)); + + const res = await this.client.devInspectTransactionBlock({ + sender: normalizeSuiAddress(this.#address), + transactionBlock: tx, + }); + + if (!res.results || !res.results[0] || !res.results[0].returnValues) { + throw new Error( + `Failed to get highest trigger below price: ${res.effects?.status?.error || 'Unknown error'}`, + ); + } + + const bytes = res.results[0].returnValues[0][0]; + return BigInt(bcs.U64.parse(new Uint8Array(bytes))); + } + // === Margin Registry Functions === /** diff --git a/packages/deepbook-v3/src/index.ts b/packages/deepbook-v3/src/index.ts index 090659f86..b476413db 100644 --- a/packages/deepbook-v3/src/index.ts +++ b/packages/deepbook-v3/src/index.ts @@ -18,6 +18,7 @@ export { MarginMaintainerContract } from './transactions/marginMaintainer.js'; export { MarginManagerContract } from './transactions/marginManager.js'; export { MarginPoolContract } from './transactions/marginPool.js'; export { PoolProxyContract } from './transactions/poolProxy.js'; +export { MarginTPSLContract } from './transactions/marginTPSL.js'; // Pyth price feed integration export { SuiPythClient, SuiPriceServiceConnection } from './pyth/pyth.js'; @@ -53,6 +54,13 @@ export type { InterestConfigParams, } from './types/index.js'; +// TPSL (Take Profit / Stop Loss) parameter interfaces +export type { + PendingLimitOrderParams, + PendingMarketOrderParams, + AddConditionalOrderParams, +} from './types/index.js'; + // Enums for trading export { OrderType, SelfMatchingOptions } from './types/index.js'; diff --git a/packages/deepbook-v3/src/transactions/marginTPSL.ts b/packages/deepbook-v3/src/transactions/marginTPSL.ts new file mode 100644 index 000000000..5ac08b08b --- /dev/null +++ b/packages/deepbook-v3/src/transactions/marginTPSL.ts @@ -0,0 +1,296 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import type { Transaction } from '@mysten/sui/transactions'; + +import type { DeepBookConfig } from '../utils/config.js'; +import type { + PendingLimitOrderParams, + PendingMarketOrderParams, + AddConditionalOrderParams, +} from '../types/index.js'; +import { OrderType, SelfMatchingOptions } from '../types/index.js'; +import { MAX_TIMESTAMP, FLOAT_SCALAR } from '../utils/config.js'; + +/** + * MarginTPSLContract class for managing Take Profit / Stop Loss operations. + */ +export class MarginTPSLContract { + #config: DeepBookConfig; + + /** + * @param {DeepBookConfig} config Configuration for MarginTPSLContract + */ + constructor(config: DeepBookConfig) { + this.#config = config; + } + + // === Helper Functions === + + /** + * @description Create a new condition for a conditional order + * @param {string} poolKey The key to identify the pool + * @param {boolean} triggerBelowPrice Whether to trigger when price is below trigger price + * @param {number} triggerPrice The price at which to trigger the order + * @returns A function that takes a Transaction object + */ + newCondition = + (poolKey: string, triggerBelowPrice: boolean, triggerPrice: number) => (tx: Transaction) => { + const pool = this.#config.getPool(poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + const inputPrice = Math.round( + (triggerPrice * FLOAT_SCALAR * quoteCoin.scalar) / baseCoin.scalar, + ); + return tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::tpsl::new_condition`, + arguments: [tx.pure.bool(triggerBelowPrice), tx.pure.u64(inputPrice)], + }); + }; + + /** + * @description Create a new pending limit order for use in conditional orders + * @param {string} poolKey The key to identify the pool + * @param {PendingLimitOrderParams} params Parameters for the pending limit order + * @returns A function that takes a Transaction object + */ + newPendingLimitOrder = + (poolKey: string, params: PendingLimitOrderParams) => (tx: Transaction) => { + const { + clientOrderId, + orderType = OrderType.NO_RESTRICTION, + selfMatchingOption = SelfMatchingOptions.SELF_MATCHING_ALLOWED, + price, + quantity, + isBid, + payWithDeep = true, + expireTimestamp = MAX_TIMESTAMP, + } = params; + const pool = this.#config.getPool(poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + const inputPrice = Math.round((price * FLOAT_SCALAR * quoteCoin.scalar) / baseCoin.scalar); + const inputQuantity = Math.round(quantity * baseCoin.scalar); + return tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::tpsl::new_pending_limit_order`, + arguments: [ + tx.pure.u64(clientOrderId), + tx.pure.u8(orderType), + tx.pure.u8(selfMatchingOption), + tx.pure.u64(inputPrice), + tx.pure.u64(inputQuantity), + tx.pure.bool(isBid), + tx.pure.bool(payWithDeep), + tx.pure.u64(expireTimestamp), + ], + }); + }; + + /** + * @description Create a new pending market order for use in conditional orders + * @param {string} poolKey The key to identify the pool + * @param {PendingMarketOrderParams} params Parameters for the pending market order + * @returns A function that takes a Transaction object + */ + newPendingMarketOrder = + (poolKey: string, params: PendingMarketOrderParams) => (tx: Transaction) => { + const { + clientOrderId, + selfMatchingOption = SelfMatchingOptions.SELF_MATCHING_ALLOWED, + quantity, + isBid, + payWithDeep = true, + } = params; + const pool = this.#config.getPool(poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const inputQuantity = Math.round(quantity * baseCoin.scalar); + return tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::tpsl::new_pending_market_order`, + arguments: [ + tx.pure.u64(clientOrderId), + tx.pure.u8(selfMatchingOption), + tx.pure.u64(inputQuantity), + tx.pure.bool(isBid), + tx.pure.bool(payWithDeep), + ], + }); + }; + + // === Public Functions === + + /** + * @description Add a conditional order (take profit or stop loss) + * @param {AddConditionalOrderParams} params Parameters for adding the conditional order + * @returns A function that takes a Transaction object + */ + addConditionalOrder = (params: AddConditionalOrderParams) => (tx: Transaction) => { + const { marginManagerKey, conditionalOrderId, triggerBelowPrice, triggerPrice, pendingOrder } = + params; + const manager = this.#config.getMarginManager(marginManagerKey); + const pool = this.#config.getPool(manager.poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + + // Create condition + const condition = this.newCondition(manager.poolKey, triggerBelowPrice, triggerPrice)(tx); + + // Create pending order based on type + const isLimitOrder = 'price' in pendingOrder; + const pending = isLimitOrder + ? this.newPendingLimitOrder(manager.poolKey, pendingOrder as PendingLimitOrderParams)(tx) + : this.newPendingMarketOrder(manager.poolKey, pendingOrder as PendingMarketOrderParams)(tx); + + tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::margin_manager::add_conditional_order`, + arguments: [ + tx.object(manager.address), + tx.object(pool.address), + tx.object(baseCoin.priceInfoObjectId!), + tx.object(quoteCoin.priceInfoObjectId!), + tx.object(this.#config.MARGIN_REGISTRY_ID), + tx.pure.u64(conditionalOrderId), + condition, + pending, + tx.object.clock(), + ], + typeArguments: [baseCoin.type, quoteCoin.type], + }); + }; + + /** + * @description Cancel all conditional orders for a margin manager + * @param {string} marginManagerKey The key to identify the margin manager + * @returns A function that takes a Transaction object + */ + cancelAllConditionalOrders = (marginManagerKey: string) => (tx: Transaction) => { + const manager = this.#config.getMarginManager(marginManagerKey); + const pool = this.#config.getPool(manager.poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::margin_manager::cancel_all_conditional_orders`, + arguments: [tx.object(manager.address), tx.object.clock()], + typeArguments: [baseCoin.type, quoteCoin.type], + }); + }; + + /** + * @description Cancel a specific conditional order + * @param {string} marginManagerKey The key to identify the margin manager + * @param {string} conditionalOrderId The ID of the conditional order to cancel + * @returns A function that takes a Transaction object + */ + cancelConditionalOrder = + (marginManagerKey: string, conditionalOrderId: string) => (tx: Transaction) => { + const manager = this.#config.getMarginManager(marginManagerKey); + const pool = this.#config.getPool(manager.poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::margin_manager::cancel_conditional_order`, + arguments: [tx.object(manager.address), tx.pure.u64(conditionalOrderId), tx.object.clock()], + typeArguments: [baseCoin.type, quoteCoin.type], + }); + }; + + /** + * @description Execute conditional orders that have been triggered + * This is a permissionless function that can be called by anyone + * @param {string} marginManagerKey The key to identify the margin manager + * @param {number} maxOrdersToExecute Maximum number of orders to execute in this call + * @returns A function that takes a Transaction object + */ + executeConditionalOrders = + (marginManagerKey: string, maxOrdersToExecute: number) => (tx: Transaction) => { + const manager = this.#config.getMarginManager(marginManagerKey); + const pool = this.#config.getPool(manager.poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + return tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::margin_manager::execute_conditional_orders`, + arguments: [ + tx.object(manager.address), + tx.object(pool.address), + tx.object(baseCoin.priceInfoObjectId!), + tx.object(quoteCoin.priceInfoObjectId!), + tx.object(this.#config.MARGIN_REGISTRY_ID), + tx.pure.u64(maxOrdersToExecute), + tx.object.clock(), + ], + typeArguments: [baseCoin.type, quoteCoin.type], + }); + }; + + // === Read-Only Functions === + + /** + * @description Get all conditional order IDs for a margin manager + * @param {string} poolKey The key to identify the pool + * @param {string} marginManagerId The ID of the margin manager + * @returns A function that takes a Transaction object + */ + conditionalOrderIds = (poolKey: string, marginManagerId: string) => (tx: Transaction) => { + const pool = this.#config.getPool(poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + return tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::margin_manager::conditional_order_ids`, + arguments: [tx.object(marginManagerId)], + typeArguments: [baseCoin.type, quoteCoin.type], + }); + }; + + /** + * @description Get a specific conditional order by ID + * @param {string} poolKey The key to identify the pool + * @param {string} marginManagerId The ID of the margin manager + * @param {string} conditionalOrderId The ID of the conditional order + * @returns A function that takes a Transaction object + */ + conditionalOrder = + (poolKey: string, marginManagerId: string, conditionalOrderId: string) => (tx: Transaction) => { + const pool = this.#config.getPool(poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + return tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::margin_manager::conditional_order`, + arguments: [tx.object(marginManagerId), tx.pure.u64(conditionalOrderId)], + typeArguments: [baseCoin.type, quoteCoin.type], + }); + }; + + /** + * @description Get the lowest trigger price for trigger_above orders + * Returns constants::max_u64() if there are no trigger_above orders + * @param {string} poolKey The key to identify the pool + * @param {string} marginManagerId The ID of the margin manager + * @returns A function that takes a Transaction object + */ + lowestTriggerAbovePrice = (poolKey: string, marginManagerId: string) => (tx: Transaction) => { + const pool = this.#config.getPool(poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + return tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::margin_manager::lowest_trigger_above_price`, + arguments: [tx.object(marginManagerId)], + typeArguments: [baseCoin.type, quoteCoin.type], + }); + }; + + /** + * @description Get the highest trigger price for trigger_below orders + * Returns 0 if there are no trigger_below orders + * @param {string} poolKey The key to identify the pool + * @param {string} marginManagerId The ID of the margin manager + * @returns A function that takes a Transaction object + */ + highestTriggerBelowPrice = (poolKey: string, marginManagerId: string) => (tx: Transaction) => { + const pool = this.#config.getPool(poolKey); + const baseCoin = this.#config.getCoin(pool.baseCoin); + const quoteCoin = this.#config.getCoin(pool.quoteCoin); + return tx.moveCall({ + target: `${this.#config.MARGIN_PACKAGE_ID}::margin_manager::highest_trigger_below_price`, + arguments: [tx.object(marginManagerId)], + typeArguments: [baseCoin.type, quoteCoin.type], + }); + }; +} diff --git a/packages/deepbook-v3/src/types/index.ts b/packages/deepbook-v3/src/types/index.ts index 4bd07e81e..889308814 100644 --- a/packages/deepbook-v3/src/types/index.ts +++ b/packages/deepbook-v3/src/types/index.ts @@ -115,6 +115,33 @@ export interface PlaceMarginMarketOrderParams { payWithDeep?: boolean; } +export interface PendingLimitOrderParams { + clientOrderId: string; + orderType?: OrderType; + selfMatchingOption?: SelfMatchingOptions; + price: number; + quantity: number; + isBid: boolean; + payWithDeep?: boolean; + expireTimestamp?: number | bigint; +} + +export interface PendingMarketOrderParams { + clientOrderId: string; + selfMatchingOption?: SelfMatchingOptions; + quantity: number; + isBid: boolean; + payWithDeep?: boolean; +} + +export interface AddConditionalOrderParams { + marginManagerKey: string; + conditionalOrderId: string; + triggerBelowPrice: boolean; + triggerPrice: number; + pendingOrder: PendingLimitOrderParams | PendingMarketOrderParams; +} + export interface ProposalParams { poolKey: string; balanceManagerKey: string;