From 6aca7bde1d93ef9346794ed25685571cae548836 Mon Sep 17 00:00:00 2001 From: Jonathan Fung <121899091+jonfung-dydx@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:39:31 +0300 Subject: [PATCH] [TS] [CLOB-895] `v0.38.2`: Add cancel order examples (#44) --- v4-client-js/examples/composite_example.ts | 4 +- v4-client-js/examples/constants.ts | 2 + .../long_term_order_cancel_example.ts | 82 +++++++++++++++++++ .../short_term_order_cancel_example.ts | 69 ++++++++++++++++ ... => short_term_order_composite_example.ts} | 0 v4-client-js/examples/test.ts | 60 ++++++++++++++ v4-client-js/package-lock.json | 4 +- v4-client-js/package.json | 2 +- v4-client-js/src/clients/composite-client.ts | 47 +++++++++-- v4-client-js/src/lib/utils.ts | 9 ++ v4-client-js/src/lib/validation.ts | 7 +- 11 files changed, 272 insertions(+), 14 deletions(-) create mode 100644 v4-client-js/examples/long_term_order_cancel_example.ts create mode 100644 v4-client-js/examples/short_term_order_cancel_example.ts rename v4-client-js/examples/{short_term_composite_example.ts => short_term_order_composite_example.ts} (100%) create mode 100644 v4-client-js/examples/test.ts diff --git a/v4-client-js/examples/composite_example.ts b/v4-client-js/examples/composite_example.ts index e1e9216f..939df3a3 100644 --- a/v4-client-js/examples/composite_example.ts +++ b/v4-client-js/examples/composite_example.ts @@ -6,7 +6,7 @@ import { import LocalWallet from '../src/clients/modules/local-wallet'; import { Subaccount } from '../src/clients/subaccount'; import { randomInt } from '../src/lib/utils'; -import { DYDX_TEST_MNEMONIC } from './constants'; +import { DYDX_TEST_MNEMONIC, MAX_CLIENT_ID } from './constants'; import ordersParams from './human_readable_orders.json'; async function sleep(ms: number): Promise { @@ -37,7 +37,7 @@ async function test(): Promise { side, price, 0.01, - randomInt(100_000_000), + randomInt(MAX_CLIENT_ID), timeInForce, timeInForceSeconds, OrderExecution.DEFAULT, diff --git a/v4-client-js/examples/constants.ts b/v4-client-js/examples/constants.ts index 849df0df..6565ecff 100644 --- a/v4-client-js/examples/constants.ts +++ b/v4-client-js/examples/constants.ts @@ -13,6 +13,8 @@ export const PERPETUAL_PAIR_BTC_USD: number = 0; const quantums: Long = new Long(1_000_000_000); const subticks: Long = new Long(1_000_000_000); +export const MAX_CLIENT_ID = 2 ** 32 - 1; + // PlaceOrder variables export const defaultOrder: IPlaceOrder = { clientId: 0, diff --git a/v4-client-js/examples/long_term_order_cancel_example.ts b/v4-client-js/examples/long_term_order_cancel_example.ts new file mode 100644 index 00000000..64a4cd5c --- /dev/null +++ b/v4-client-js/examples/long_term_order_cancel_example.ts @@ -0,0 +1,82 @@ +import { BECH32_PREFIX, OrderFlags } from '../src'; +import { CompositeClient } from '../src/clients/composite-client'; +import { + Network, OrderExecution, OrderSide, OrderTimeInForce, OrderType, +} from '../src/clients/constants'; +import LocalWallet from '../src/clients/modules/local-wallet'; +import { Subaccount } from '../src/clients/subaccount'; +import { randomInt } from '../src/lib/utils'; +import { DYDX_TEST_MNEMONIC, MAX_CLIENT_ID } from './constants'; + +async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function test(): Promise { + const wallet = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC, BECH32_PREFIX); + console.log(wallet); + const network = Network.staging(); + const client = await CompositeClient.connect(network); + console.log('**Client**'); + console.log(client); + const subaccount = new Subaccount(wallet, 0); + + /* + Note this example places a stateful order. + Programmatic traders should generally not use stateful orders for following reasons: + - Stateful orders received out of order by validators will fail sequence number validation + and be dropped. + - Stateful orders have worse time priority since they are only matched after they are included + on the block. + - Stateful order rate limits are more restrictive than Short-Term orders, specifically max 2 per + block / 20 per 100 blocks. + - Stateful orders can only be canceled after they’ve been included in a block. + */ + const longTermOrderClientId = randomInt(MAX_CLIENT_ID); + try { + // place a long term order + const tx = await client.placeOrder( + subaccount, + 'ETH-USD', + OrderType.LIMIT, + OrderSide.SELL, + 40000, + 0.01, + longTermOrderClientId, + OrderTimeInForce.GTT, + 60, + OrderExecution.DEFAULT, + false, + false, + ); + console.log('**Long Term Order Tx**'); + console.log(tx.hash); + } catch (error) { + console.log('**Long Term Order Failed**'); + console.log(error.message); + } + + await sleep(5000); // wait for placeOrder to complete + + try { + // cancel the long term order + const tx = await client.cancelOrder( + subaccount, + longTermOrderClientId, + OrderFlags.LONG_TERM, + 'ETH-USD', + 0, + 120, + ); + console.log('**Cancel Long Term Order Tx**'); + console.log(tx); + } catch (error) { + console.log('**Cancel Long Term Order Failed**'); + console.log(error.message); + } +} + +test().then(() => { +}).catch((error) => { + console.log(error.message); +}); diff --git a/v4-client-js/examples/short_term_order_cancel_example.ts b/v4-client-js/examples/short_term_order_cancel_example.ts new file mode 100644 index 00000000..8bfb2925 --- /dev/null +++ b/v4-client-js/examples/short_term_order_cancel_example.ts @@ -0,0 +1,69 @@ +import { BECH32_PREFIX, OrderFlags, Order_TimeInForce } from '../src'; +import { CompositeClient } from '../src/clients/composite-client'; +import { + Network, OrderSide, +} from '../src/clients/constants'; +import LocalWallet from '../src/clients/modules/local-wallet'; +import { Subaccount } from '../src/clients/subaccount'; +import { randomInt, sleep } from '../src/lib/utils'; +import { DYDX_TEST_MNEMONIC, MAX_CLIENT_ID } from './constants'; + +async function test(): Promise { + const wallet = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC, BECH32_PREFIX); + console.log(wallet); + const network = Network.staging(); + const client = await CompositeClient.connect(network); + console.log('**Client**'); + console.log(client); + const subaccount = new Subaccount(wallet, 0); + + const currentBlock = await client.validatorClient.get.latestBlockHeight(); + const nextValidBlockHeight = currentBlock + 1; + // Note, you can change this to any number between `next_valid_block_height` + // to `next_valid_block_height + SHORT_BLOCK_WINDOW` + const goodTilBlock = nextValidBlockHeight + 10; + const shortTermOrderClientId = randomInt(MAX_CLIENT_ID); + try { + // place a short term order + const tx = await client.placeShortTermOrder( + subaccount, + 'ETH-USD', + OrderSide.SELL, + 40000, + 0.01, + shortTermOrderClientId, + goodTilBlock, + Order_TimeInForce.TIME_IN_FORCE_UNSPECIFIED, + false, + ); + console.log('**Short Term Order Tx**'); + console.log(tx.hash); + } catch (error) { + console.log('**Short Term Order Failed**'); + console.log(error.message); + } + + await sleep(5000); // wait for placeOrder to complete + + try { + // cancel the short term order + const tx = await client.cancelOrder( + subaccount, + shortTermOrderClientId, + OrderFlags.SHORT_TERM, + 'ETH-USD', + goodTilBlock + 10, + 0, + ); + console.log('**Cancel Short Term Order Tx**'); + console.log(tx); + } catch (error) { + console.log('**Cancel Short Term Order Failed**'); + console.log(error.message); + } +} + +test().then(() => { +}).catch((error) => { + console.log(error.message); +}); diff --git a/v4-client-js/examples/short_term_composite_example.ts b/v4-client-js/examples/short_term_order_composite_example.ts similarity index 100% rename from v4-client-js/examples/short_term_composite_example.ts rename to v4-client-js/examples/short_term_order_composite_example.ts diff --git a/v4-client-js/examples/test.ts b/v4-client-js/examples/test.ts new file mode 100644 index 00000000..939df3a3 --- /dev/null +++ b/v4-client-js/examples/test.ts @@ -0,0 +1,60 @@ +import { BECH32_PREFIX } from '../src'; +import { CompositeClient } from '../src/clients/composite-client'; +import { + Network, OrderExecution, OrderSide, OrderTimeInForce, OrderType, +} from '../src/clients/constants'; +import LocalWallet from '../src/clients/modules/local-wallet'; +import { Subaccount } from '../src/clients/subaccount'; +import { randomInt } from '../src/lib/utils'; +import { DYDX_TEST_MNEMONIC, MAX_CLIENT_ID } from './constants'; +import ordersParams from './human_readable_orders.json'; + +async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function test(): Promise { + const wallet = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC, BECH32_PREFIX); + console.log(wallet); + const network = Network.staging(); + const client = await CompositeClient.connect(network); + console.log('**Client**'); + console.log(client); + const subaccount = new Subaccount(wallet, 0); + for (const orderParams of ordersParams) { + try { + const type = OrderType[orderParams.type as keyof typeof OrderType]; + const side = OrderSide[orderParams.side as keyof typeof OrderSide]; + const timeInForceString = orderParams.timeInForce ?? 'GTT'; + const timeInForce = OrderTimeInForce[timeInForceString as keyof typeof OrderTimeInForce]; + const price = orderParams.price ?? 1350; + const timeInForceSeconds = (timeInForce === OrderTimeInForce.GTT) ? 60 : 0; + const postOnly = orderParams.postOnly ?? false; + const tx = await client.placeOrder( + subaccount, + 'ETH-USD', + type, + side, + price, + 0.01, + randomInt(MAX_CLIENT_ID), + timeInForce, + timeInForceSeconds, + OrderExecution.DEFAULT, + postOnly, + false, + ); + console.log('**Order Tx**'); + console.log(tx); + } catch (error) { + console.log(error.message); + } + + await sleep(5000); // wait for placeOrder to complete + } +} + +test().then(() => { +}).catch((error) => { + console.log(error.message); +}); diff --git a/v4-client-js/package-lock.json b/v4-client-js/package-lock.json index 189c4c5a..9f43e208 100644 --- a/v4-client-js/package-lock.json +++ b/v4-client-js/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dydxprotocol/v4-client-js", - "version": "0.38.1", + "version": "0.38.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@dydxprotocol/v4-client-js", - "version": "0.38.1", + "version": "0.38.2", "license": "BSL-1.1", "dependencies": { "@cosmjs/amino": "^0.30.1", diff --git a/v4-client-js/package.json b/v4-client-js/package.json index 3534e35d..da0bfab1 100644 --- a/v4-client-js/package.json +++ b/v4-client-js/package.json @@ -1,6 +1,6 @@ { "name": "@dydxprotocol/v4-client-js", - "version": "0.38.1", + "version": "0.38.2", "description": "General client library for the new dYdX system (v4 decentralized)", "main": "build/src/index.js", "scripts": { diff --git a/v4-client-js/src/clients/composite-client.ts b/v4-client-js/src/clients/composite-client.ts index 8fd0beb5..2aa6e4c5 100644 --- a/v4-client-js/src/clients/composite-client.ts +++ b/v4-client-js/src/clients/composite-client.ts @@ -5,6 +5,7 @@ import { Order_ConditionType, Order_TimeInForce } from '@dydxprotocol/v4-proto/s import Long from 'long'; import protobuf from 'protobufjs'; +import { isStatefulOrder, verifyOrderFlags } from '../lib/validation'; import { OrderFlags } from '../types'; import { DYDX_DENOM, @@ -160,13 +161,13 @@ export class CompositeClient { } /** - * @description Calculate the goodTilBlock value for a SHORT_TERM order + * @description Validate the goodTilBlock value for a SHORT_TERM order * * @param goodTilBlock Number of blocks from the current block height the order will * be valid for. * - * @throws UnexpectedClientError if a malformed response is returned with no GRPC error - * at any point. + * @throws UserError if the goodTilBlock value is not valid given latest block height and + * SHORT_BLOCK_WINDOW. */ private async validateGoodTilBlock(goodTilBlock: number): Promise { const height = await this.validatorClient.get.latestBlockHeight(); @@ -238,8 +239,8 @@ export class CompositeClient { price, size, clientId, - timeInForce, goodTilBlock, + timeInForce, reduceOnly, ); msg.then((it) => resolve([it])).catch((err) => { @@ -497,7 +498,7 @@ export class CompositeClient { * @param subaccount The subaccount to cancel the order from * @param clientId The client id of the order to cancel * @param orderFlags The order flags of the order to cancel - * @param clobPairId The clob pair id of the order to cancel + * @param marketId The market to cancel the order on * @param goodTilBlock The goodTilBlock of the order to cancel * @param goodTilBlockTime The goodTilBlockTime of the order to cancel * @@ -509,10 +510,40 @@ export class CompositeClient { subaccount: Subaccount, clientId: number, orderFlags: OrderFlags, - clobPairId: number, - goodTilBlock?: number, - goodTilBlockTime?: number, + marketId: string, + goodTilBlock: number, + goodTilTimeInSeconds: number, ): Promise { + + const marketsResponse = await this.indexerClient.markets.getPerpetualMarkets(marketId); + const market = marketsResponse.markets[marketId]; + const clobPairId = market.clobPairId; + + if (!verifyOrderFlags(orderFlags)) { + throw new Error(`Invalid order flags: ${orderFlags}`); + } + + let goodTilBlockTime; + if (isStatefulOrder(orderFlags)) { + if (goodTilTimeInSeconds === 0) { + throw new Error('goodTilTimeInSeconds must be set for LONG_TERM or CONDITIONAL order'); + } + if (goodTilBlock !== 0) { + throw new Error( + 'goodTilBlock should be zero since LONG_TERM or CONDITIONAL orders ' + + 'use goodTilTimeInSeconds instead of goodTilBlock.', + ); + } + goodTilBlockTime = this.calculateGoodTilBlockTime(goodTilTimeInSeconds); + } else { + if (goodTilBlock === 0) { + throw new Error('goodTilBlock must be non-zero for SHORT_TERM orders'); + } + if (goodTilTimeInSeconds !== 0) { + throw new Error('goodTilTimeInSeconds should be zero since SHORT_TERM orders use goodTilBlock instead of goodTilTimeInSeconds.'); + } + } + return this.validatorClient.post.cancelOrder( subaccount, clientId, diff --git a/v4-client-js/src/lib/utils.ts b/v4-client-js/src/lib/utils.ts index 3a8398c8..18c3ec83 100644 --- a/v4-client-js/src/lib/utils.ts +++ b/v4-client-js/src/lib/utils.ts @@ -34,3 +34,12 @@ export function clientIdFromString( // We must coerce this into a 32-bit unsigned integer. return hash + (2 ** 31); } + +/** + * Pauses the execution of the program for a specified time. + * @param ms - The number of milliseconds to pause the program. + * @returns A promise that resolves after the specified number of milliseconds. + */ +export async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/v4-client-js/src/lib/validation.ts b/v4-client-js/src/lib/validation.ts index 6534185d..3a2763ff 100644 --- a/v4-client-js/src/lib/validation.ts +++ b/v4-client-js/src/lib/validation.ts @@ -131,7 +131,12 @@ function verifyNumberIsUint32(num: number): boolean { return num >= 0 && num <= MAX_UINT_32; } -function isStatefulOrder(orderFlags: OrderFlags): boolean { +export function verifyOrderFlags(orderFlags: OrderFlags): boolean { + return orderFlags === OrderFlags.SHORT_TERM || + orderFlags === OrderFlags.LONG_TERM || orderFlags === OrderFlags.CONDITIONAL; +} + +export function isStatefulOrder(orderFlags: OrderFlags): boolean { return orderFlags === OrderFlags.LONG_TERM || orderFlags === OrderFlags.CONDITIONAL; }