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..ebb52336 --- /dev/null +++ b/v4-client-js/examples/short_term_composite_example.ts @@ -0,0 +1,81 @@ +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 + 10; + + const timeInForce = orderExecutionToTimeInForce(orderParams.timeInForce); + + // uint32 + const clientId = randomInt(2 ** 32 - 1); + + const tx = await client.placeShortTermOrder( + subaccount, + 'ETH-USD', + side, + price, + 0.01, + clientId, + 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); +}); 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": { 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(),