From f4861ac0fc283027cf7f689ed2ac417c68d41ae4 Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Wed, 20 Sep 2023 12:14:30 -0700 Subject: [PATCH 1/5] WIP short term place orders for TS --- v4-client-js/examples/composite_example.ts | 25 +-- v4-client-js/src/clients/composite-client.ts | 152 ++++++++++++++++++- v4-client-js/src/clients/constants.ts | 2 + 3 files changed, 166 insertions(+), 13 deletions(-) diff --git a/v4-client-js/examples/composite_example.ts b/v4-client-js/examples/composite_example.ts index e1e9216f..05c2be48 100644 --- a/v4-client-js/examples/composite_example.ts +++ b/v4-client-js/examples/composite_example.ts @@ -1,7 +1,9 @@ +import { Order_TimeInForce } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/order'; + import { BECH32_PREFIX } from '../src'; import { CompositeClient } from '../src/clients/composite-client'; import { - Network, OrderExecution, OrderSide, OrderTimeInForce, OrderType, + Network, OrderSide, } from '../src/clients/constants'; import LocalWallet from '../src/clients/modules/local-wallet'; import { Subaccount } from '../src/clients/subaccount'; @@ -23,25 +25,24 @@ async function test(): Promise { 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( + + 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 + 3; + + const tx = await client.placeShortTermOrder( subaccount, 'ETH-USD', - type, side, price, 0.01, randomInt(100_000_000), - timeInForce, - timeInForceSeconds, - OrderExecution.DEFAULT, - postOnly, + Order_TimeInForce.TIME_IN_FORCE_UNSPECIFIED, + goodTilBlock, false, ); console.log('**Order Tx**'); diff --git a/v4-client-js/src/clients/composite-client.ts b/v4-client-js/src/clients/composite-client.ts index 7328d612..8fd0beb5 100644 --- a/v4-client-js/src/clients/composite-client.ts +++ b/v4-client-js/src/clients/composite-client.ts @@ -1,6 +1,7 @@ import { EncodeObject } from '@cosmjs/proto-signing'; import { GasPrice, IndexedTx, StdFee } from '@cosmjs/stargate'; import { BroadcastTxAsyncResponse, BroadcastTxSyncResponse } from '@cosmjs/tendermint-rpc/build/tendermint37'; +import { Order_ConditionType, Order_TimeInForce } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/order'; import Long from 'long'; import protobuf from 'protobufjs'; @@ -8,7 +9,7 @@ import { OrderFlags } from '../types'; import { DYDX_DENOM, GAS_PRICE, - Network, OrderExecution, OrderSide, OrderTimeInForce, OrderType, + Network, OrderExecution, OrderSide, OrderTimeInForce, OrderType, SHORT_BLOCK_WINDOW, } from './constants'; import { calculateQuantums, @@ -21,6 +22,7 @@ import { calculateConditionalOrderTriggerSubticks, } from './helpers/chain-helpers'; import { IndexerClient } from './indexer-client'; +import { UserError } from './lib/errors'; import LocalWallet from './modules/local-wallet'; import { Subaccount } from './subaccount'; import { ValidatorClient } from './validator-client'; @@ -157,6 +159,27 @@ export class CompositeClient { return height + 3; } + /** + * @description Calculate 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. + */ + private async validateGoodTilBlock(goodTilBlock: number): Promise { + const height = await this.validatorClient.get.latestBlockHeight(); + const nextValidBlockHeight = height + 1; + const lowerBound = nextValidBlockHeight; + const upperBound = nextValidBlockHeight + SHORT_BLOCK_WINDOW; + if (goodTilBlock < lowerBound || goodTilBlock > upperBound) { + throw new UserError(`Invalid Short-Term order GoodTilBlock. + Should be greater-than-or-equal-to ${lowerBound} and less-than-or-equal-to ${upperBound}. + Provided good til block: ${goodTilBlock}`); + } + } + /** * @description Calculate the goodTilBlockTime value for a LONG_TERM order * the calling function is responsible for creating the messages. @@ -175,6 +198,60 @@ export class CompositeClient { return Math.round(future.getTime() / 1000); } + /** + * @description Place a short term order with human readable input. + * + * Use human readable form of input, including price and size + * The quantum and subticks are calculated and submitted + * + * @param subaccount The subaccount to place the order under + * @param marketId The market to place the order on + * @param side The side of the order to place + * @param price The price of the order to place + * @param size The size of the order to place + * @param clientId The client id of the order to place + * @param timeInForce The time in force of the order to place + * @param goodTilBlock The goodTilBlock of the order to place + * @param reduceOnly The reduceOnly of the order to place + * + * + * @throws UnexpectedClientError if a malformed response is returned with no GRPC error + * at any point. + * @returns The transaction hash. + */ + async placeShortTermOrder( + subaccount: Subaccount, + marketId: string, + side: OrderSide, + price: number, + size: number, + clientId: number, + goodTilBlock: number, + timeInForce: Order_TimeInForce, + reduceOnly: boolean, + ): Promise { + const msgs: Promise = new Promise((resolve) => { + const msg = this.placeShortTermOrderMessage( + subaccount, + marketId, + side, + price, + size, + clientId, + timeInForce, + goodTilBlock, + reduceOnly, + ); + msg.then((it) => resolve([it])).catch((err) => { + console.log(err); + }); + }); + return this.send( + subaccount.wallet, + () => msgs, + true); + } + /** * @description Place an order with human readable input. * @@ -341,6 +418,79 @@ export class CompositeClient { ); } + /** + * @description Calculate and create the short term place order message + * + * Use human readable form of input, including price and size + * The quantum and subticks are calculated and submitted + * + * @param subaccount The subaccount to place the order under + * @param marketId The market to place the order on + * @param side The side of the order to place + * @param price The price of the order to place + * @param size The size of the order to place + * @param clientId The client id of the order to place + * @param timeInForce The time in force of the order to place + * @param goodTilBlock The goodTilBlock of the order to place + * @param reduceOnly The reduceOnly of the order to place + * + * + * @throws UnexpectedClientError if a malformed response is returned with no GRPC error + * at any point. + * @returns The message to be passed into the protocol + */ + private async placeShortTermOrderMessage( + subaccount: Subaccount, + marketId: string, + side: OrderSide, + price: number, + size: number, + clientId: number, + goodTilBlock: number, + timeInForce: Order_TimeInForce, + reduceOnly: boolean, + ): Promise { + await this.validateGoodTilBlock(goodTilBlock); + + const marketsResponse = await this.indexerClient.markets.getPerpetualMarkets(marketId); + const market = marketsResponse.markets[marketId]; + const clobPairId = market.clobPairId; + const atomicResolution = market.atomicResolution; + const stepBaseQuantums = market.stepBaseQuantums; + const quantumConversionExponent = market.quantumConversionExponent; + const subticksPerTick = market.subticksPerTick; + const orderSide = calculateSide(side); + const quantums = calculateQuantums( + size, + atomicResolution, + stepBaseQuantums, + ); + const subticks = calculateSubticks( + price, + atomicResolution, + quantumConversionExponent, + subticksPerTick, + ); + const orderFlags = OrderFlags.SHORT_TERM; + return this.validatorClient.post.composer.composeMsgPlaceOrder( + subaccount.address, + subaccount.subaccountNumber, + clientId, + clobPairId, + orderFlags, + goodTilBlock, + 0, // Short term orders use goodTilBlock. + orderSide, + quantums, + subticks, + timeInForce, + reduceOnly, + 0, // Client metadata is 0 for short term orders. + Order_ConditionType.CONDITION_TYPE_UNSPECIFIED, // Short term orders cannot be conditional. + Long.fromInt(0), // Short term orders cannot be conditional. + ); + } + /** * @description Cancel an order with human readable input. * diff --git a/v4-client-js/src/clients/constants.ts b/v4-client-js/src/clients/constants.ts index a0cc748f..a7326634 100644 --- a/v4-client-js/src/clients/constants.ts +++ b/v4-client-js/src/clients/constants.ts @@ -123,6 +123,8 @@ export const DEFAULT_API_TIMEOUT: number = 3_000; export const MAX_MEMO_CHARACTERS: number = 256; +export const SHORT_BLOCK_WINDOW: number = 20; + // Querying export const PAGE_REQUEST: PageRequest = { key: new Uint8Array(), From 6bb0499aea663bbce7c0ac13066938f83c937a8a Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Wed, 20 Sep 2023 14:09:48 -0700 Subject: [PATCH 2/5] short term example, data for short term --- v4-client-js/examples/composite_example.ts | 25 +++--- .../human_readable_short_term_orders.json | 42 ++++++++++ .../examples/short_term_composite_example.ts | 78 +++++++++++++++++++ 3 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 v4-client-js/examples/human_readable_short_term_orders.json create mode 100644 v4-client-js/examples/short_term_composite_example.ts diff --git a/v4-client-js/examples/composite_example.ts b/v4-client-js/examples/composite_example.ts index 05c2be48..e1e9216f 100644 --- a/v4-client-js/examples/composite_example.ts +++ b/v4-client-js/examples/composite_example.ts @@ -1,9 +1,7 @@ -import { Order_TimeInForce } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/order'; - import { BECH32_PREFIX } from '../src'; import { CompositeClient } from '../src/clients/composite-client'; import { - Network, OrderSide, + Network, OrderExecution, OrderSide, OrderTimeInForce, OrderType, } from '../src/clients/constants'; import LocalWallet from '../src/clients/modules/local-wallet'; import { Subaccount } from '../src/clients/subaccount'; @@ -25,24 +23,25 @@ async function test(): Promise { 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 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 + 3; - - const tx = await client.placeShortTermOrder( + 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(100_000_000), - Order_TimeInForce.TIME_IN_FORCE_UNSPECIFIED, - goodTilBlock, + timeInForce, + timeInForceSeconds, + OrderExecution.DEFAULT, + postOnly, false, ); console.log('**Order Tx**'); diff --git a/v4-client-js/examples/human_readable_short_term_orders.json b/v4-client-js/examples/human_readable_short_term_orders.json new file mode 100644 index 00000000..987bae56 --- /dev/null +++ b/v4-client-js/examples/human_readable_short_term_orders.json @@ -0,0 +1,42 @@ +[ + { + "timeInForce": "DEFAULT", + "side": "BUY", + "price": 40000 + }, + { + "timeInForce": "DEFAULT", + "side": "SELL", + "price": 1000 + }, + { + "timeInForce": "FOK", + "side": "BUY", + "price": 1000 + }, + { + "timeInForce": "FOK", + "side": "SELL", + "price": 40000 + }, + { + "timeInForce": "IOC", + "side": "BUY", + "price": 40000 + }, + { + "timeInForce": "IOC", + "side": "SELL", + "price": 1000 + }, + { + "timeInForce": "POST_ONLY", + "side": "BUY", + "price": 1000 + }, + { + "timeInForce": "POST_ONLY", + "side": "SELL", + "price": 40000 + } +] \ No newline at end of file diff --git a/v4-client-js/examples/short_term_composite_example.ts b/v4-client-js/examples/short_term_composite_example.ts new file mode 100644 index 00000000..2706c255 --- /dev/null +++ b/v4-client-js/examples/short_term_composite_example.ts @@ -0,0 +1,78 @@ +import { Order_TimeInForce } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/order'; + +import { BECH32_PREFIX } from '../src'; +import { CompositeClient } from '../src/clients/composite-client'; +import { + Network, OrderExecution, OrderSide, +} 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 } from './constants'; +import ordersParams from './human_readable_short_term_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 side = OrderSide[orderParams.side as keyof typeof OrderSide]; + const price = orderParams.price ?? 1350; + + 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 + 3; + + const timeInForce = orderExecutionToTimeInForce(orderParams.timeInForce); + + const tx = await client.placeShortTermOrder( + subaccount, + 'ETH-USD', + side, + price, + 0.01, + randomInt(100_000_000), + timeInForce, + goodTilBlock, + false, + ); + console.log('**Order Tx**'); + console.log(tx.hash.toString()); + } catch (error) { + console.log(error.message); + } + + await sleep(5000); // wait for placeOrder to complete + } +} + +function orderExecutionToTimeInForce(orderExecution: string): Order_TimeInForce { + switch (orderExecution) { + case OrderExecution.DEFAULT: + return Order_TimeInForce.TIME_IN_FORCE_UNSPECIFIED; + case OrderExecution.FOK: + return Order_TimeInForce.TIME_IN_FORCE_FILL_OR_KILL; + case OrderExecution.IOC: + return Order_TimeInForce.TIME_IN_FORCE_IOC; + case OrderExecution.POST_ONLY: + return Order_TimeInForce.TIME_IN_FORCE_POST_ONLY; + default: + throw new Error('Unrecognized order execution'); + } +} + +test().then(() => { +}).catch((error) => { + console.log(error.message); +}); From e1b26e69c115c8206e276fad1b64b39e89ed33f0 Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Wed, 20 Sep 2023 19:16:03 -0700 Subject: [PATCH 3/5] version bump --- v4-client-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v4-client-js/package.json b/v4-client-js/package.json index 3842f17c..219a909a 100644 --- a/v4-client-js/package.json +++ b/v4-client-js/package.json @@ -1,6 +1,6 @@ { "name": "@dydxprotocol/v4-client-js", - "version": "0.36.2", + "version": "0.37.2", "description": "General client library for the new dYdX system (v4 decentralized)", "main": "build/src/index.js", "scripts": { From 5426bf294b297f2c763f279549c4cb009773b1d3 Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Thu, 21 Sep 2023 12:41:52 -0700 Subject: [PATCH 4/5] block height +10 --- v4-client-js/examples/short_term_composite_example.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v4-client-js/examples/short_term_composite_example.ts b/v4-client-js/examples/short_term_composite_example.ts index 2706c255..97142510 100644 --- a/v4-client-js/examples/short_term_composite_example.ts +++ b/v4-client-js/examples/short_term_composite_example.ts @@ -32,7 +32,7 @@ async function test(): Promise { 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 + 3; + const goodTilBlock = nextValidBlockHeight + 10; const timeInForce = orderExecutionToTimeInForce(orderParams.timeInForce); From 0eee5d98a6f1b341c1b50cebe28e0f9cf7a472fe Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Thu, 21 Sep 2023 12:44:20 -0700 Subject: [PATCH 5/5] uint32 --- v4-client-js/examples/short_term_composite_example.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/v4-client-js/examples/short_term_composite_example.ts b/v4-client-js/examples/short_term_composite_example.ts index 97142510..ebb52336 100644 --- a/v4-client-js/examples/short_term_composite_example.ts +++ b/v4-client-js/examples/short_term_composite_example.ts @@ -36,13 +36,16 @@ async function test(): Promise { const timeInForce = orderExecutionToTimeInForce(orderParams.timeInForce); + // uint32 + const clientId = randomInt(2 ** 32 - 1); + const tx = await client.placeShortTermOrder( subaccount, 'ETH-USD', side, price, 0.01, - randomInt(100_000_000), + clientId, timeInForce, goodTilBlock, false,