资源加载中... loading...

Quantitative Practice of DEX Exchanges (1) -- dYdX v4 User Guide

Author: FMZ~Lydia, Created: 2024-12-26 15:32:24, Updated: 2024-12-26 17:36:31

Preface

With the rapid rise of decentralized exchanges (DEX) in the field of cryptocurrency trading, quantitative traders have turned to these platforms gradually for efficient automated trading. As one of the most popular decentralized trading platforms, dYdX provides powerful trading functions and supports futures perpetual contract trading. Its latest version v4 optimizes performance and user experience, making it the first choice for many quantitative traders.

This article will introduce how to practice quantitative trading on dYdX v4, including how to use its API to trade, obtain market data, and manage accounts.

  • Test environment switching
  • Market information query
  • Order information and position information query
  • Place an order
  • Sub-account management
  • Node method request

dYdX v4 DEX

  • dYdX testnet App Page

img

  • As dYdX v3 with Ethereum, trading generates rewards, which are reward dYdX tokens.

Wallet Connection, Login, and Configuration Information

The previous dYdX v3 protocol DEX exchange has been offline. The current dYdX v4 App address is:

https://dydx.trade/trade/ETH-USD

After opening the App page, there is a button to connect to the wallet in the upper right corner. Scan the QR code to connect to the wallet.

If you want to test and familiarize yourself with the test network environment first, you can use the test network:

https://v4.testnet.dydx.exchange/trade/ETH-USD

Also, click the connect wallet button in the upper right corner, scan the code to connect the wallet, and verify the signature. After the wallet is connected successfully, a dydx v4 address will be generated automatically. This address will be displayed in the upper right corner of the App page. Click it and a menu will pop up. There are operations such as recharge, withdrawal, and transfer. One of the differences between the dYdX mainnet (production environment) and the testnet is that when you click recharge on the testnet, 300 USDC assets will be automatically charged using the faucet for testing. If you want to do real transactions on dYdX, you need to charge USDC assets. Recharge is also very convenient and compatible with multiple assets and chains for recharge.

  • dYdX v4 account address The dYdX v4 account address is derived from the wallet address. The dYdX v4 account address looks like: dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx, which is an address starting with dydx1. This address can be queried in blockchain explorers.

  • Mnemonics You can use the “Export Password” button in the upper right corner menu to export the mnemonics of the current dYdX address account. When adding an exchange on the FMZ platform, you need to configure this mnemonic.

Mnemonics can be configured directly on the FMZ platform or saved locally on the docker. When using the dydx v4 exchange object, the file content recording the mnemonics will be read, which will be demonstrated in the practical part of this article.

Differences between Mainnet and Testnet

The testnet environment is different from the mainnet environment in some aspects. Here are a few simple differences.

  • Sub-account asset transfer. The main network has a sub-account cleanup mechanism. When subAccountNumber >= 128, if the sub-account of the ID has no positions, the assets will be automatically cleaned to the sub-account whose subAccountNumber is 0. During testing, it was found that the test network does not have this mechanism (or the triggering conditions are different and it has not been triggered in the test network).
  • Certain token names. The native token dydx has different names: mainnet DYDX, testnet Dv4TNT
  • Address configuration, such as chain ID, node address, indexer address, etc. There are many nodes and configurations, here is one of them:

Mainnet: Indexer Address: https://indexer.dydx.trade Chain ID: dydx-mainnet-1 REST Node: https://dydx-dao-api.polkachu.com:443

Testnet: Indexer Address: https://indexer.v4testnet.dydx.exchange Chain ID: dydx-testnet-4 REST Node: https://dydx-testnet-api.polkachu.com

dYdX v4 Protocol Architecture

The dYdX v4 protocol is developed based on the cosmos ecosystem. The dYdX v4 DEX system transaction-related content mainly consists of two parts:

  • An indexer responsible for querying ticker information, account information, etc.
  • dydx blockchain order messages, order cancellation messages, transfer messages, etc.

Indexer

The indexer service provides REST and Websocket protocols.

  • REST protocol The REST protocol interface supports market information query, account information, position information, order information and other queries, and has been encapsulated as a unified API interface on the FMZ platform.

  • WebSocket protocol On the FMZ platform, you can use the Dial function to create a Websocket connection and subscribe to market information.

It should be noted that the indexer of dydx v4 has the same problem as centralized exchanges, that is, data updates are not so timely. For example, sometimes you may not be able to find the order if you query it immediately after placing an order. It is recommended to wait for a few seconds after (Sleep(n)) certain operations before querying.

Here is an example of using the Dial function to create a Websocket API connection and subscribe to order book data:

function dYdXIndexerWSconnManager(streamingPoint) {
    var self = {}
    self.base = streamingPoint
    self.wsThread = null

    // subscription
    self.CreateWsThread = function (msgSubscribe) {
        self.wsThread = threading.Thread(function (streamingPoint, msgSubscribe) {
            // Order book
            var orderBook = null 

            // Update order book
            var updateOrderbook = function(orderbook, update) {
                // Update bids
                if (update.bids) {
                    update.bids.forEach(([price, size]) => {
                        const priceFloat = parseFloat(price)
                        const sizeFloat = parseFloat(size)

                        if (sizeFloat === 0) {
                            // Delete the buy order with price
                            orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
                        } else {
                            // Update or add a buy order
                            orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
                            orderbook.bids.push({price: price, size: size})
                            // Sort by price descending
                            orderbook.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))
                        }
                    })
                }

                // Update asks
                if (update.asks) {
                    update.asks.forEach(([price, size]) => {
                        const priceFloat = parseFloat(price)
                        const sizeFloat = parseFloat(size)

                        if (sizeFloat === 0) {
                            // Delete the sell order with price
                            orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
                        } else {
                            // Update or add a sell order
                            orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
                            orderbook.asks.push({price: price, size: size})
                            // Sort by price ascending
                            orderbook.asks.sort((a, b) => parseFloat(a.price) - parseFloat(b.price))
                        }
                    })
                }

                return orderbook
            }

            var conn = Dial(`${streamingPoint}|reconnect=true&payload=${JSON.stringify(msgSubscribe)}`)
            if (!conn) {
                Log("createWsThread failed.")
                return
            }
            while (true) {
                var data = conn.read()
                if (data) {
                    var msg = null                    
                    try {
                        msg = JSON.parse(data)
                        if (msg["type"] == "subscribed") {
                            orderBook = msg["contents"]
                            threading.currentThread().postMessage(orderBook)
                        } else if (msg["type"] == "channel_data") {
                            orderBook = updateOrderbook(orderBook, msg["contents"])
                            threading.currentThread().postMessage(orderBook)
                        }
                    } catch (e) {
                        Log("e.name:", e.name, "e.stack:", e.stack, "e.message:", e.message)
                    }
                }
            }
        }, streamingPoint, msgSubscribe)
    }

    // monitor
    self.Peek = function () {
        return self.wsThread.peekMessage()
    }

    return self
}

function main() {
    // real : wss://indexer.dydx.trade/v4/ws
    // simulate : wss://indexer.v4testnet.dydx.exchange/v4/ws

    var symbol = "ETH-USD"
    var manager = dYdXIndexerWSconnManager("wss://indexer.dydx.trade/v4/ws")
    manager.CreateWsThread({"type": "subscribe", "channel": "v4_orderbook", "id": symbol})

    var redCode = "#FF0000"
    var greenCode = "#006400"
    while (true) {
        var depthTbl = {type: "table", title: symbol + " / depth", cols: ["level", "price", "amount"], rows: []}
        var depth = manager.Peek()
        if (depth) {
            for (var i = 0; i < depth.asks.length; i++) {
                if (i > 9) {
                    break
                }
                var ask = depth.asks[i]
                depthTbl.rows.push(["asks " + (i + 1) + greenCode, ask.price + greenCode, ask.size + greenCode])
            }
            depthTbl.rows.reverse()

            for (var i = 0; i < depth.bids.length; i++) {
                if (i > 9) {
                    break
                }
                var bid = depth.bids[i]
                depthTbl.rows.push(["bids " + (i + 1) + redCode, bid.price + redCode, bid.size + redCode])
            }
        }
        LogStatus(_D(), "\n`" + JSON.stringify(depthTbl) + "`")
    }
}

dYdX Chain Node Message Broadcast

The most commonly used messages in transactions are order messages, order cancellation messages, and transfer messages.

  • Order message summary
{
  "@type": "/dydxprotocol.clob.MsgPlaceOrder",
  "order": {
    "orderId": {
      "subaccountId": {
        "owner": "xxx"
      },
      "clientId": xxx,
      "orderFlags": 64,
      "clobPairId": 1
    },
    "side": "SIDE_BUY",
    "quantums": "2000000",
    "subticks": "3500000000",
    "goodTilBlockTime": 1742295981
  }
}
  • Limit order: In the order function encapsulated on the FMZ platform, the orderFlags value used for limit orders is: ORDER_FLAGS_LONG_TERM = 64 # Long-term order. According to the limitations of the dydx v4 protocol, the longest order validity period is used, which is 90 days (all types of orders on dydx v4 have validity periods).

  • Market order: In the order function encapsulated on the FMZ platform, the orderFlags value used for market orders is: ORDER_FLAGS_SHORT_TERM = 0 # Short-term order, according to the recommendations of the dydx v4 protocol:

// Recommend set to oracle price - 5% or lower for SELL, oracle price + 5% for BUY

Since it is not a true market order, the oracle price is used, plus or minus 5% slippage as the market order. The validity period of short-term orders is also different from that of long-term orders. Short-term orders use the block height validity period, which is set to the current block + 10 block heights according to the recommendation of dydx v4.

  • Order ID: Since the order operation is performed directly on the chain, there will be no order ID generated by the indexer after the message is broadcast, and the indexer order cannot be used as the return value of the platform order function. In order to ensure the uniqueness of the order ID and the accuracy of order query, the returned order ID consists of the following information (comma-separated):

Trading Pairs dydx current account address Subaccount number (subaccountNumber) clientId (randomly generated) clobPairId (transaction symbol ID) orderFlags goodTilData (milliseconds)

  • Order cancellation message summary
{
  "@type": "/dydxprotocol.clob.MsgCancelOrder",
  "orderId": {
    "subaccountId": {
      "owner": "xxx"
    },
    "clientId": 2585872024,
    "orderFlags": 64,
    "clobPairId": 1
  },
  "goodTilBlockTime": 1742295981
}

The order ID returned by the FMZ platform order interface needs to be passed in.

  • Transfer message summary
{
  "@type": "/dydxprotocol.sending.MsgCreateTransfer",
  "transfer": {
    "sender": {
      "owner": "xxx"
    },
    "recipient": {
      "owner": "xxx",
      "number": 128
    },
    "amount": "10000000"
  }
}

Many sub-accounts can be created under the current dydx v4 address. The sub-account with subAccountNumber 0 is the first automatically created sub-account. The sub-account ID with subAccountNumber greater than or equal to 128 is used for isolated position trading, which requires at least 20 USDC assets. For example, you can go from subAccountNumber 0 -> 128, or from subAccountNumber 128 -> 0. Transfers require Gas Fee. Gas Fee can be USDC or dydx tokens.

FMZ Platform dYdX v4 Practice

The above content explains some packaging details briefly. Next, let’s practice the specific usage. Here we use the dYdX v4 test network for demonstration. The test network is basically the same as the main network, and there is an automatic faucet to receive test assets. The docker deployment operation will not be repeated. Create a live trading test on FMZ.

1. Configuration

After connecting to the dYdX v4 App successfully by using a cryptocurrency wallet (I use the imToken wallet here), claim your test assets and then export the mnemonic for your current dYdX v4 account (derived from your wallet).

img

Configure the mnemonic on the FMZ platform. Here we use the local file method to configure it (you can also fill it out directly and configure it to the platform. The mnemonic is configured after encryption, not in plain text).

  • Mnemonic file: mnemonic.txt

img

Place it in the live trading ID folder directory under the docker directory. Of course, it can also be placed in other directories (the specific path needs to be written during configuration).

  • Configure the exchange on FMZ

https://www.fmz.com/m/platforms/add

Fill in the mnemonic edit box: file:///mnemonic.txt, the corresponding actual path is: docker directory/logs/storage/594291.

img

2. Switch to dydx v4 Test Network

function main() {
    // Switch the indexer address of the test chain
    exchange.SetBase("https://indexer.v4testnet.dydx.exchange")

    // Switch the ChainId of the test chain
    exchange.IO("chainId", "dydx-testnet-4")

    // Switch the REST node address of the test chain
    exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")

    // Read account information test
    Log(exchange.GetAccount()) 
}

Read the test network account information:

{
	"Info": {
		"subaccounts": [{
			"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
			"subaccountNumber": 0,
			"equity": "300.386228",
			"latestProcessedBlockHeight": "28193227",
			"freeCollateral": "300.386228",
			"openPerpetualPositions": {},
			"assetPositions": {
				"USDC": {
					"subaccountNumber": 0,
					"size": "300.386228",
					"symbol": "USDC",
					"side": "LONG",
					"assetId": "0"
				}
			},
			"marginEnabled": true,
			"updatedAtHeight": "28063818"
		}, {
			"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
			"equity": "0",
			"freeCollateral": "0",
			"openPerpetualPositions": {},
			"marginEnabled": true,
			"subaccountNumber": 1,
			"assetPositions": {},
			"updatedAtHeight": "27770289",
			"latestProcessedBlockHeight": "28193227"
		}, {
			"equity": "0",
			"openPerpetualPositions": {},
			"marginEnabled": true,
			"updatedAtHeight": "28063818",
			"latestProcessedBlockHeight": "28193227",
			"subaccountNumber": 128,
			"freeCollateral": "0",
			"assetPositions": {},
			"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez"
		}],
		"totalTradingRewards": "0.021744179376211564"
	},
	"Stocks": 0,
	"FrozenStocks": 0,
	"Balance": 300.386228,
	"FrozenBalance": 0,
	"Equity": 300.386228,
	"UPnL": 0
}

3. Market Information Inquiry

Did not switch to the test network, tested with the main network

function main() {
    var markets = exchange.GetMarkets()
    if (!markets) {
        throw "get markets error"
    }
    var tbl = {type: "table", title: "test markets", cols: ["key", "Symbol", "BaseAsset", "QuoteAsset", "TickSize", "AmountSize", "PricePrecision", "AmountPrecision", "MinQty", "MaxQty", "MinNotional", "MaxNotional", "CtVal"], rows: []}
    for (var symbol in markets) {
        var market = markets[symbol]
        tbl.rows.push([symbol, market.Symbol, market.BaseAsset, market.QuoteAsset, market.TickSize, market.AmountSize, market.PricePrecision, market.AmountPrecision, market.MinQty, market.MaxQty, market.MinNotional, market.MaxNotional, market.CtVal])
    }
    LogStatus("`" + JSON.stringify(tbl) +  "`")
}

img

4. Place an Order

function main() {
    // Switch the indexer address of the test chain
    exchange.SetBase("https://indexer.v4testnet.dydx.exchange")

    // Switch the ChainId of the test chain
    exchange.IO("chainId", "dydx-testnet-4")

    // Switch the REST node address of the test chain
    exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")

    // Limit order, pending order
    var idSell = exchange.CreateOrder("ETH_USD.swap", "sell", 4000, 0.002)
    var idBuy = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.003)

    // Market order
    var idMarket = exchange.CreateOrder("ETH_USD.swap", "buy", -1, 0.01)

    Log("idSell:", idSell)
    Log("idBuy:", idBuy)
    Log("idMarket:", idMarket)
}

img

dYdX v4 App page:

img

5. Order Information

The test network places two orders in advance, tests obtaining the current pending orders, and cancels the orders.

function main() {    
    // Switch the indexer address of the test chain
    exchange.SetBase("https://indexer.v4testnet.dydx.exchange")

    // Switch the ChainId of the test chain
    exchange.IO("chainId", "dydx-testnet-4")

    // Switch the REST node address of the test chain
    exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")

    var orders = exchange.GetOrders()
    Log("orders:", orders)
    for (var order of orders) {
        exchange.CancelOrder(order.Id, order)
        Sleep(2000)
    }

    var tbl = {type: "table", title: "test GetOrders", cols: ["Id", "Price", "Amount", "DealAmount", "AvgPrice", "Status", "Type", "Offset", "ContractType"], rows: []}
    for (var order of orders) {
        tbl.rows.push([order.Id, order.Price, order.Amount, order.DealAmount, order.AvgPrice, order.Status, order.Type, order.Offset, order.ContractType])
    }
    LogStatus("`" + JSON.stringify(tbl) +  "`")
}

img

6. Position Information Query

function main() {
    // Switch the indexer address of the test chain
    exchange.SetBase("https://indexer.v4testnet.dydx.exchange")

    // Switch the ChainId of the test chain
    exchange.IO("chainId", "dydx-testnet-4")

    // Switch the REST node address of the test chain
    exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")

    var p1 = exchange.GetPositions("USD.swap")
    var p2 = exchange.GetPositions("ETH_USD.swap")
    var p3 = exchange.GetPositions()
    var p4 = exchange.GetPositions("SOL_USD.swap")

    var tbls = []
    for (var positions of [p1, p2, p3, p4]) {
        var tbl = {type: "table", title: "test GetPosition/GetPositions", cols: ["Symbol", "Amount", "Price", "FrozenAmount", "Type", "Profit", "Margin", "ContractType", "MarginLevel"], rows: []}
        for (var p of positions) {
            tbl.rows.push([p.Symbol, p.Amount, p.Price, p.FrozenAmount, p.Type, p.Profit, p.Margin, p.ContractType, p.MarginLevel])
        } 
        tbls.push(tbl)
    }

    LogStatus("`" + JSON.stringify(tbls) +  "`")
}

img

7. Sub-account Management

function main() {
    // Switch the indexer address of the test chain
    exchange.SetBase("https://indexer.v4testnet.dydx.exchange")

    // Switch the ChainId of the test chain
    exchange.IO("chainId", "dydx-testnet-4")

    // Switch the REST node address of the test chain
    exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")

    // subAccountNumber 0 -> 128 : 20 USDC, Gas Fee is adv4tnt, i.e. dydx token
    // var ret = exchange.IO("transferUSDCToSubaccount", 0, 128, "adv4tnt", 20)  
    // Log("ret:", ret)

    // Switch to subaccount subAccountNumber 128 and read account information to check
    exchange.IO("subAccountNumber", 128)

    var account = exchange.GetAccount()
    Log("account:", account)
}

Log information:

img

Switch to the subaccount whose subAccountNumber is 128, and the data returned by GetAccount is:

{
	"Info": {
		"subaccounts": [{
			"subaccountNumber": 0,
			"assetPositions": {
				"USDC": {
					"size": "245.696892",
					"symbol": "USDC",
					"side": "LONG",
					"assetId": "0",
					"subaccountNumber": 0
				}
			},
			"updatedAtHeight": "28194977",
			"latestProcessedBlockHeight": "28195008",
			"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
			"freeCollateral": "279.5022142346",
			"openPerpetualPositions": {
				"ETH-USD": {
					"closedAt": null,
					"size": "0.01",
					"maxSize": "0.01",
					"exitPrice": null,
					"unrealizedPnl": "-0.17677323",
					"subaccountNumber": 0,
					"status": "OPEN",
					"createdAt": "2024-12-26T03:36:09.264Z",
					"createdAtHeight": "28194494",
					"sumClose": "0",
					"netFunding": "0",
					"market": "ETH-USD",
					"side": "LONG",
					"entryPrice": "3467.2",
					"realizedPnl": "0",
					"sumOpen": "0.01"
				}
			},
			"marginEnabled": true,
			"equity": "280.19211877"
		}, {
			"openPerpetualPositions": {},
			"assetPositions": {},
			"marginEnabled": true,
			"latestProcessedBlockHeight": "28195008",
			"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
			"subaccountNumber": 1,
			"equity": "0",
			"freeCollateral": "0",
			"updatedAtHeight": "27770289"
		}, {
			"openPerpetualPositions": {},
			"updatedAtHeight": "28194977",
			"latestProcessedBlockHeight": "28195008",
			"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
			"subaccountNumber": 128,
			"assetPositions": {
				"USDC": {
					"assetId": "0",
					"subaccountNumber": 128,
					"size": "20",
					"symbol": "USDC",
					"side": "LONG"
				}
			},
			"marginEnabled": true,
			"equity": "20",
			"freeCollateral": "20"
		}],
		"totalTradingRewards": "0.021886899964446858"
	},
	"Stocks": 0,
	"FrozenStocks": 0,
	"Balance": 20,
	"FrozenBalance": 0,
	"Equity": 20,
	"UPnL": 0
}

It can be seen that the sub-account with subAccountNumber 128 has transferred 20 USDC.

8. Get TxHash and Call the REST Node Interface

According to the order, get TxHash and test the method of IO calling REST node

How to get the TxHash of an order? The exchange object dydx will cache the TxHash, which can be queried by using the order ID. However, after the strategy is stopped, the cached order tx hash map will be cleared.

function main() {
    // Switch the indexer address of the test chain
    exchange.SetBase("https://indexer.v4testnet.dydx.exchange")

    // Switch the ChainId of the test chain
    exchange.IO("chainId", "dydx-testnet-4")

    // Switch the REST node address of the test chain
    exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")

    var id1 = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.002)
    var hash1 = exchange.IO("getTxHash", id1)
    Log("id1:", id1, "hash1:", hash1)

    var id2 = exchange.CreateOrder("ETH_USD.swap", "buy", 2900, 0.003)
    var hash2 = exchange.IO("getTxHash", id2)
    Log("id2:", id2, "hash2:", hash2)
    
    // To clear the mapping table, use: exchange.IO("getTxHash", "")
    var arr = [hash1, hash2]
    
    Sleep(10000)
    for (var txHash of arr) {
        // GET https://docs.cosmos.network   /cosmos/tx/v1beta1/txs/{hash}
        var ret = exchange.IO("api", "GET", "/cosmos/tx/v1beta1/txs/" + txHash)
        Log("ret:", ret)
    }
}

img

Messages queried through TxHash:

var ret = exchange.IO("api", “GET”, “/cosmos/tx/v1beta1/txs/” + txHash)

The content is too long, so here are some excerpts for demonstration:

{
	"tx_response": {
		"codespace": "",
		"code": 0,
		"logs": [],
		"info": "",
		"height": "28195603",
		"data": "xxx",
		"raw_log": "",
		"gas_wanted": "-1",
		"gas_used": "0",
		"tx": {
			"@type": "/cosmos.tx.v1beta1.Tx",
			"body": {
				"messages": [{
					"@type": "/dydxprotocol.clob.MsgPlaceOrder",
					"order": {
						"good_til_block_time": 1742961542,
						"condition_type": "CONDITION_TYPE_UNSPECIFIED",
						"order_id": {
							"clob_pair_id": 1,
							"subaccount_id": {
								"owner": "xxx",
								"number": 0
							},
							"client_id": 2999181974,
							"order_flags": 64
						},
						"side": "SIDE_BUY",
						"quantums": "3000000",
						"client_metadata": 0,
						"conditional_order_trigger_subticks": "0",
						"subticks": "2900000000",
						"time_in_force": "TIME_IN_FORCE_UNSPECIFIED",
						"reduce_only": false
					}
				}],
				"memo": "FMZ",
				"timeout_height": "0",
				"extension_options": [],
				"non_critical_extension_options": []
			},
      ...

The End

The above tests are based on the latest docker. You need to download the latest docker to support dYdX v4 DEX

Thank you for your support and thank you for reading.


More