diff --git a/v4-client-js/examples/short_term_order_composite_example.ts b/v4-client-js/examples/short_term_order_composite_example.ts index ebb52336..d0381e3e 100644 --- a/v4-client-js/examples/short_term_order_composite_example.ts +++ b/v4-client-js/examples/short_term_order_composite_example.ts @@ -46,8 +46,8 @@ async function test(): Promise { price, 0.01, clientId, - timeInForce, goodTilBlock, + timeInForce, false, ); console.log('**Order Tx**'); diff --git a/v4-client-js/examples/transfer_example_send.ts b/v4-client-js/examples/transfer_example_send.ts index 0d6ca762..d8116859 100644 --- a/v4-client-js/examples/transfer_example_send.ts +++ b/v4-client-js/examples/transfer_example_send.ts @@ -15,6 +15,7 @@ import LocalWallet from '../src/clients/modules/local-wallet'; import { Subaccount } from '../src/clients/subaccount'; import { ValidatorClient } from '../src/clients/validator-client'; import { DYDX_TEST_MNEMONIC } from './constants'; +import { Account } from '@cosmjs/stargate'; // TODO: Test after staging deploy latest transfer contracts. @@ -48,9 +49,15 @@ async function test(): Promise { resolve([msg]); }); + const account: Promise = client.post.account( + subaccount.address, + undefined, + ); + const totalFee = await client.post.simulate( subaccount.wallet, () => msgs, + () => account, GAS_PRICE_DYDX_DENOM, undefined, ); diff --git a/v4-client-js/examples/transfer_example_withdraw_other.ts b/v4-client-js/examples/transfer_example_withdraw_other.ts index d57350dd..67d332e8 100644 --- a/v4-client-js/examples/transfer_example_withdraw_other.ts +++ b/v4-client-js/examples/transfer_example_withdraw_other.ts @@ -1,4 +1,5 @@ import { EncodeObject } from '@cosmjs/proto-signing'; +import { Account } from '@cosmjs/stargate'; import { Method } from '@cosmjs/tendermint-rpc'; import Long from 'long'; @@ -40,9 +41,14 @@ async function test(): Promise { resolve([msg]); }); + const account: Promise = client.post.account( + subaccount.address, + undefined, + ); const totalFee = await client.post.simulate( subaccount.wallet, () => msgs, + () => account, undefined, ); console.log('**Total Fee**'); diff --git a/v4-client-js/src/clients/composite-client.ts b/v4-client-js/src/clients/composite-client.ts index 2aa6e4c5..e689ac20 100644 --- a/v4-client-js/src/clients/composite-client.ts +++ b/v4-client-js/src/clients/composite-client.ts @@ -1,5 +1,7 @@ import { EncodeObject } from '@cosmjs/proto-signing'; -import { GasPrice, IndexedTx, StdFee } from '@cosmjs/stargate'; +import { + Account, 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'; @@ -35,6 +37,14 @@ import { ValidatorClient } from './validator-client'; protobuf.util.Long = Long; protobuf.configure(); +export interface MarketInfo { + clobPairId: number; + atomicResolution: number; + stepBaseQuantums: number; + quantumConversionExponent: number; + subticksPerTick: number; +} + export class CompositeClient { public readonly network: Network; private _indexerClient: IndexerClient; @@ -86,11 +96,12 @@ export class CompositeClient { async sign( wallet: LocalWallet, messaging: () => Promise, + account: () => Promise, zeroFee: boolean, gasPrice: GasPrice = GAS_PRICE, memo?: string, ): Promise { - return this.validatorClient.post.sign(wallet, messaging, zeroFee, gasPrice, memo); + return this.validatorClient.post.sign(wallet, messaging, account, zeroFee, gasPrice, memo); } /** @@ -104,11 +115,12 @@ export class CompositeClient { async send( wallet: LocalWallet, messaging: () => Promise, + account: () => Promise, zeroFee: boolean, gasPrice: GasPrice = GAS_PRICE, memo?: string, ): Promise { - return this.validatorClient.post.send(wallet, messaging, zeroFee, gasPrice, memo); + return this.validatorClient.post.send(wallet, messaging, account, zeroFee, gasPrice, memo); } /** @@ -142,10 +154,11 @@ export class CompositeClient { async simulate( wallet: LocalWallet, messaging: () => Promise, + account: () => Promise, gasPrice: GasPrice = GAS_PRICE, memo?: string, ): Promise { - return this.validatorClient.post.simulate(wallet, messaging, gasPrice, memo); + return this.validatorClient.post.simulate(wallet, messaging, account, gasPrice, memo); } /** @@ -155,9 +168,17 @@ export class CompositeClient { * at any point. * @returns The goodTilBlock value */ - private async calculateGoodTilBlock(): Promise { - const height = await this.validatorClient.get.latestBlockHeight(); - return height + 3; + + private async calculateGoodTilBlock( + orderFlags: OrderFlags, + currentHeight?: number, + ): Promise { + if (orderFlags === OrderFlags.SHORT_TERM) { + const height = currentHeight ?? await this.validatorClient.get.latestBlockHeight(); + return height + 3; + } else { + return Promise.resolve(0); + } } /** @@ -247,9 +268,14 @@ export class CompositeClient { console.log(err); }); }); + const account: Promise = this.validatorClient.post.account( + subaccount.address, + undefined, + ); return this.send( subaccount.wallet, () => msgs, + () => account, true); } @@ -292,6 +318,8 @@ export class CompositeClient { postOnly?: boolean, reduceOnly?: boolean, triggerPrice?: number, + marketInfo?: MarketInfo, + currentHeight?: number, ): Promise { const msgs: Promise = new Promise((resolve) => { const msg = this.placeOrderMessage( @@ -309,14 +337,21 @@ export class CompositeClient { postOnly, reduceOnly, triggerPrice, + marketInfo, + currentHeight, ); msg.then((it) => resolve([it])).catch((err) => { console.log(err); }); }); + const account: Promise = this.validatorClient.post.account( + subaccount.address, + undefined, + ); return this.send( subaccount.wallet, () => msgs, + () => account, true); } @@ -360,14 +395,22 @@ export class CompositeClient { postOnly?: boolean, reduceOnly?: boolean, triggerPrice?: number, + marketInfo?: MarketInfo, + currentHeight?: number, ): Promise { - 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 orderFlags = calculateOrderFlags(type, timeInForce); + + const result = await Promise.all([ + this.calculateGoodTilBlock(orderFlags, currentHeight), + this.retrieveMarketInfo(marketId, marketInfo), + ], + ); + const goodTilBlock = result[0]; + const clobPairId = result[1].clobPairId; + const atomicResolution = result[1].atomicResolution; + const stepBaseQuantums = result[1].stepBaseQuantums; + const quantumConversionExponent = result[1].quantumConversionExponent; + const subticksPerTick = result[1].subticksPerTick; const orderSide = calculateSide(side); const quantums = calculateQuantums( size, @@ -380,16 +423,13 @@ export class CompositeClient { quantumConversionExponent, subticksPerTick, ); - const orderFlags = calculateOrderFlags(type, timeInForce); const orderTimeInForce = calculateTimeInForce(type, timeInForce, execution, postOnly); - const goodTilBlock = (orderFlags === OrderFlags.SHORT_TERM) - ? await this.calculateGoodTilBlock() : 0; let goodTilBlockTime = 0; if (orderFlags === OrderFlags.LONG_TERM || orderFlags === OrderFlags.CONDITIONAL) { if (goodTilTimeInSeconds == null) { throw new Error('goodTilTimeInSeconds must be set for LONG_TERM or CONDITIONAL order'); } else { - goodTilBlockTime = await this.calculateGoodTilBlockTime(goodTilTimeInSeconds); + goodTilBlockTime = this.calculateGoodTilBlockTime(goodTilTimeInSeconds); } } const clientMetadata = calculateClientMetadata(type); @@ -419,6 +459,27 @@ export class CompositeClient { ); } + private async retrieveMarketInfo(marketId: string, marketInfo?:MarketInfo): Promise { + if (marketInfo) { + return Promise.resolve(marketInfo); + } else { + 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; + return { + clobPairId, + atomicResolution, + stepBaseQuantums, + quantumConversionExponent, + subticksPerTick, + }; + } + } + /** * @description Calculate and create the short term place order message * @@ -581,9 +642,14 @@ export class CompositeClient { ); resolve([msg]); }); + const account: Promise = this.validatorClient.post.account( + subaccount.address, + undefined, + ); return this.send( subaccount.wallet, () => msgs, + () => account, true); } @@ -638,8 +704,13 @@ export class CompositeClient { ); resolve([msg]); }); + const account: Promise = this.validatorClient.post.account( + subaccount.address, + undefined, + ); return this.validatorClient.post.send(subaccount.wallet, () => msgs, + () => account, false); } @@ -690,9 +761,14 @@ export class CompositeClient { ); resolve([msg]); }); + const account: Promise = this.validatorClient.post.account( + subaccount.address, + undefined, + ); return this.send( subaccount.wallet, () => msgs, + () => account, false); } @@ -784,9 +860,14 @@ export class CompositeClient { console.log(err); }); }); + const account: Promise = this.validatorClient.post.account( + subaccount.address, + undefined, + ); const signature = await this.sign( wallet, () => msgs, + () => account, true); return Buffer.from(signature).toString('base64'); @@ -812,7 +893,16 @@ export class CompositeClient { ); resolve([msg]); }); - const signature = await this.sign(subaccount.wallet, () => msgs, true); + const account: Promise = this.validatorClient.post.account( + subaccount.address, + undefined, + ); + const signature = await this.sign( + subaccount.wallet, + () => msgs, + () => account, + true, + ); return Buffer.from(signature).toString('base64'); } diff --git a/v4-client-js/src/clients/modules/post.ts b/v4-client-js/src/clients/modules/post.ts index 87d67438..b906d9d4 100644 --- a/v4-client-js/src/clients/modules/post.ts +++ b/v4-client-js/src/clients/modules/post.ts @@ -5,6 +5,7 @@ import { Registry, } from '@cosmjs/proto-signing'; import { + Account, calculateFee, GasPrice, IndexedTx, @@ -55,6 +56,8 @@ export class Post { private readonly chainId: string; public readonly get: Get; + private accountNumberCache: Map = new Map(); + constructor( get: Get, chainId: string, @@ -76,12 +79,22 @@ export class Post { async simulate( wallet: LocalWallet, messaging: () => Promise, + account: () => Promise, gasPrice: GasPrice = GAS_PRICE, memo?: string, ): Promise { - const msgs = await messaging(); - const account = await this.get.getAccount(wallet.address!); - return this.simulateTransaction(wallet.pubKey!, account.sequence, msgs, gasPrice, memo); + const msgsPromise = messaging(); + const accountPromise = account(); + const msgsAndAccount = await Promise.all([msgsPromise, accountPromise]); + const msgs = msgsAndAccount[0]; + + return this.simulateTransaction( + wallet.pubKey!, + msgsAndAccount[1].sequence, + msgs, + gasPrice, + memo, + ); } /** @@ -95,12 +108,16 @@ export class Post { async sign( wallet: LocalWallet, messaging: () => Promise, + account: () => Promise, zeroFee: boolean, gasPrice: GasPrice = GAS_PRICE, memo?: string, ): Promise { - const msgs = await messaging(); - return this.signTransaction(wallet, msgs, zeroFee, gasPrice, memo); + const msgsPromise = await messaging(); + const accountPromise = await account(); + const msgsAndAccount = await Promise.all([msgsPromise, accountPromise]); + const msgs = msgsAndAccount[0]; + return this.signTransaction(wallet, msgs, msgsAndAccount[1], zeroFee, gasPrice, memo); } /** @@ -114,14 +131,20 @@ export class Post { async send( wallet: LocalWallet, messaging: () => Promise, + account: () => Promise, zeroFee: boolean, gasPrice: GasPrice = GAS_PRICE, memo?: string, broadcastMode?: BroadcastMode, ): Promise { - const msgs = await messaging(); + const msgsPromise = messaging(); + const accountPromise = account(); + const msgsAndAccount = await Promise.all([msgsPromise, accountPromise]); + const msgs = msgsAndAccount[0]; + return this.signAndSendTransaction( wallet, + msgsAndAccount[1], msgs, zeroFee, gasPrice, @@ -159,11 +182,11 @@ export class Post { private async signTransaction( wallet: LocalWallet, messages: EncodeObject[], + account: Account, zeroFee: boolean, gasPrice: GasPrice = GAS_PRICE, memo?: string, ): Promise { - const account = await this.get.getAccount(wallet.address!); // Simulate transaction if no fee is specified. const fee: StdFee = zeroFee ? { amount: [], @@ -190,6 +213,18 @@ export class Post { ); } + public async account(address: string, orderFlags?: number): Promise { + if (orderFlags === OrderFlags.SHORT_TERM) { + if (this.accountNumberCache.has(address)) { + // For SHORT_TERM orders, the sequence doesn't matter + return this.accountNumberCache.get(address)!; + } + } + const account = await this.get.getAccount(address); + this.accountNumberCache.set(address, account); + return account; + } + /** * @description Sign and send a message * @@ -197,6 +232,7 @@ export class Post { */ private async signAndSendTransaction( wallet: LocalWallet, + account: Account, messages: EncodeObject[], zeroFee: boolean, gasPrice: GasPrice = GAS_PRICE, @@ -206,6 +242,7 @@ export class Post { const signedTransaction = await this.signTransaction( wallet, messages, + account, zeroFee, gasPrice, memo, @@ -329,7 +366,16 @@ export class Post { ); resolve([msg]); }); - return this.send(subaccount.wallet, () => msgs, true, undefined, undefined, broadcastMode); + const account: Promise = this.account(subaccount.address, orderFlags); + return this.send( + subaccount.wallet, + () => msgs, + () => account, + true, + undefined, + undefined, + broadcastMode, + ); } async placeOrderObject( @@ -377,7 +423,15 @@ export class Post { ); resolve([msg]); }); - return this.send(subaccount.wallet, () => msgs, true, undefined, undefined, broadcastMode); + const account: Promise = this.account(subaccount.address, undefined); + return this.send( + subaccount.wallet, + () => msgs, + () => account, + true, + undefined, + undefined, + broadcastMode); } async cancelOrderObject( @@ -415,7 +469,16 @@ export class Post { ); resolve([msg]); }); - return this.send(subaccount.wallet, () => msgs, false, undefined, undefined, broadcastMode); + const account: Promise = this.account(subaccount.address, undefined); + return this.send( + subaccount.wallet, + () => msgs, + () => account, + false, + undefined, + undefined, + broadcastMode, + ); } async deposit( @@ -433,7 +496,16 @@ export class Post { ); resolve([msg]); }); - return this.send(subaccount.wallet, () => msgs, false, undefined, undefined, broadcastMode); + const account: Promise = this.account(subaccount.address, undefined); + return this.send( + subaccount.wallet, + () => msgs, + () => account, + false, + undefined, + undefined, + broadcastMode, + ); } async withdraw( @@ -453,7 +525,16 @@ export class Post { ); resolve([msg]); }); - return this.send(subaccount.wallet, () => msgs, false, undefined, undefined, broadcastMode); + const account: Promise = this.account(subaccount.address, undefined); + return this.send( + subaccount.wallet, + () => msgs, + () => account, + false, + undefined, + undefined, + broadcastMode, + ); } async sendToken( @@ -473,9 +554,11 @@ export class Post { ); resolve([msg]); }); + const account: Promise = this.account(subaccount.address, undefined); return this.send( subaccount.wallet, () => msgs, + () => account, zeroFee, coinDenom === DYDX_DENOM ? GAS_PRICE_DYDX_DENOM : GAS_PRICE, undefined, diff --git a/v4-client-js/src/clients/native.ts b/v4-client-js/src/clients/native.ts index cd646871..207777e2 100644 --- a/v4-client-js/src/clients/native.ts +++ b/v4-client-js/src/clients/native.ts @@ -3,7 +3,7 @@ */ import { EncodeObject } from '@cosmjs/proto-signing'; -import { accountFromAny } from '@cosmjs/stargate'; +import { Account, accountFromAny } from '@cosmjs/stargate'; import { Order_Side, Order_TimeInForce } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/order'; import * as AuthModule from 'cosmjs-types/cosmos/auth/v1beta1/query'; import Long from 'long'; @@ -13,7 +13,7 @@ import { UserError } from '../lib/errors'; import { encodeJson } from '../lib/helpers'; import { deriveHDKeyFromEthereumSignature } from '../lib/onboarding'; import { NetworkOptimizer } from '../network_optimizer'; -import { CompositeClient } from './composite-client'; +import { CompositeClient, MarketInfo } from './composite-client'; import { Network, OrderType, OrderSide, OrderTimeInForce, OrderExecution, IndexerConfig, ValidatorConfig, } from './constants'; @@ -224,6 +224,9 @@ export async function placeOrder( const reduceOnly = json.reduceOnly ?? false; const triggerPrice = json.triggerPrice; + const marketInfo = json.marketInfo as MarketInfo; + const currentHeight = json.currentHeight as number; + const subaccount = new Subaccount(wallet, subaccountNumber); const tx = await client.placeOrder( subaccount, @@ -239,6 +242,8 @@ export async function placeOrder( postOnly, reduceOnly, triggerPrice, + marketInfo, + currentHeight, ); return encodeJson(tx); } catch (error) { @@ -429,12 +434,17 @@ export async function withdrawToIBC( const msgs = [subaccountMsg, ibcMsg]; const encodeObjects: Promise = new Promise((resolve) => resolve(msgs)); + const account: Promise = client.validatorClient.post.account( + subaccount.address, + undefined, + ); const tx = await client.send( wallet, () => { return encodeObjects; }, + () => account, false, undefined, undefined, @@ -477,11 +487,17 @@ export async function transferNativeToken( const msgs = [msg]; const encodeObjects: Promise = new Promise((resolve) => resolve(msgs)); + const account: Promise = client.validatorClient.post.account( + subaccount.address, + undefined, + ); + const tx = await client.send( wallet, () => { return encodeObjects; }, + () => account, false, GAS_PRICE_DYDX_DENOM, undefined, @@ -580,9 +596,19 @@ export async function simulateDeposit( ); const msgs: EncodeObject[] = [msg]; const encodeObjects: Promise = new Promise((resolve) => resolve(msgs)); - const stdFee = await client.simulate(globalThis.wallet, () => { - return encodeObjects; - }); + + const account: Promise = client.validatorClient.post.account( + subaccount.address, + undefined, + ); + + const stdFee = await client.simulate( + globalThis.wallet, + () => { + return encodeObjects; + }, + () => account, + ); return JSON.stringify(stdFee); } catch (error) { return wrappedError(error); @@ -619,9 +645,18 @@ export async function simulateWithdraw( ); const msgs: EncodeObject[] = [msg]; const encodeObjects: Promise = new Promise((resolve) => resolve(msgs)); - const stdFee = await client.simulate(globalThis.wallet, () => { - return encodeObjects; - }); + const account: Promise = client.validatorClient.post.account( + subaccount.address, + undefined, + ); + + const stdFee = await client.simulate( + globalThis.wallet, + () => { + return encodeObjects; + }, + () => account, + ); return encodeJson(stdFee); } catch (error) { return wrappedError(error); @@ -662,11 +697,17 @@ export async function simulateTransferNativeToken( ); const msgs: EncodeObject[] = [msg]; const encodeObjects: Promise = new Promise((resolve) => resolve(msgs)); + + const account: Promise = client.validatorClient.post.account( + subaccount.address, + undefined, + ); const stdFee = await client.simulate( globalThis.wallet, () => { return encodeObjects; }, + () => account, GAS_PRICE_DYDX_DENOM, ); return encodeJson(stdFee); @@ -717,9 +758,14 @@ export async function signRawPlaceOrder( ); resolve([msg]); }); + const account: Promise = client.validatorClient.post.account( + wallet.address!, + orderFlags, + ); const signed = await client.sign( wallet, () => msgs, + () => account, true, ); return Buffer.from(signed).toString('base64');