Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TS] [CLOB-895] short term place orders for TS #39

Merged
merged 5 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions v4-client-js/examples/human_readable_short_term_orders.json
Original file line number Diff line number Diff line change
@@ -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
}
]
81 changes: 81 additions & 0 deletions v4-client-js/examples/short_term_composite_example.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function test(): Promise<void> {
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: do you think this function should be defined within v4-client-js/src/clients/composite-client.ts and called within placeShortTermOrder?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm fine with you leaving it here if it's meant to translate from a JSON order

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only meant to translate from the JSON example file, so will keep it in here

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);
});
2 changes: 1 addition & 1 deletion v4-client-js/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
152 changes: 151 additions & 1 deletion v4-client-js/src/clients/composite-client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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';

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,
Expand All @@ -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';
Expand Down Expand Up @@ -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<void> {
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.
Expand All @@ -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<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
const msgs: Promise<EncodeObject[]> = 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.
*
Expand Down Expand Up @@ -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<EncodeObject> {
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: client metadata can be non-zero for short-term and stateful 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.
*
Expand Down
2 changes: 2 additions & 0 deletions v4-client-js/src/clients/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down