UNPKG

@d8x/perpetuals-sdk

Version:

Node TypeScript SDK for D8X Perpetual Futures

614 lines 29.3 kB
import { JsonRpcProvider, ZeroHash, } from "ethers"; import { BUY_SIDE, MULTICALL_ADDRESS, OrderStatus, SELL_SIDE, ZERO_ADDRESS } from "./constants"; import { IPyth__factory, LimitOrderBook__factory, Multicall3__factory } from "./contracts"; import { ABK64x64ToFloat, floatToABK64x64 } from "./d8XMath"; import PerpetualDataHandler from "./perpetualDataHandler"; import WriteAccessHandler from "./writeAccessHandler"; /** * Functions to execute existing conditional orders from the limit order book. This class * requires a private key and executes smart-contract interactions that require * gas-payments. * @extends WriteAccessHandler */ export default class OrderExecutorTool extends WriteAccessHandler { /** * Constructor. * @param {NodeSDKConfig} config Configuration object, see PerpetualDataHandler.readSDKConfig. * @example * import { OrderExecutorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(OrderExecutorTool); * // load configuration for Polygon zkEVM (testnet) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * // OrderExecutorTool (authentication required, PK is an environment variable with a private key) * const pk: string = <string>process.env.PK; * let orderTool = new OrderExecutorTool(config, pk); * // Create a proxy instance to access the blockchain * await orderTool.createProxyInstance(); * } * main(); * * @param {string | Signer} signer Private key or ethers Signer of the account */ constructor(config, signer) { super(config, signer); // override parent's gas limit with a lower number this.gasLimit = 4000000; } /** * Executes an order by symbol and ID. This action interacts with the blockchain and incurs gas costs. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @param {string} orderId ID of the order to be executed. * @param {string} executorAddr optional address of the wallet to be credited for executing the order, if different from the one submitting this transaction. * @param {number} nonce optional nonce * @param {PriceFeedSubmission=} submission optional signed prices obtained via PriceFeeds::fetchLatestFeedPriceInfoForPerpetual * @example * import { OrderExecutorTool, PerpetualDataHandler, Order } from "@d8x/perpetuals-sdk"; * async function main() { * console.log(OrderExecutorTool); * // Setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const pk: string = <string>process.env.PK; * const symbol = "ETH-USD-MATIC"; * let orderTool = new OrderExecutorTool(config, pk); * await orderTool.createProxyInstance(); * // get some open orders * const maxOrdersToGet = 5; * let [orders, ids]: [Order[], string[]] = await orderTool.pollLimitOrders(symbol, maxOrdersToGet); * console.log(`Got ${ids.length} orders`); * for (let k = 0; k < ids.length; k++) { * // check whether order meets conditions * let doExecute = await orderTool.isTradeable(orders[k]); * if (doExecute) { * // execute * let tx = await orderTool.executeOrder(symbol, ids[k]); * console.log(`Sent order id ${ids[k]} for execution, tx hash = ${tx.hash}`); * } * } * } * main(); * @returns Transaction object. */ async executeOrder(symbol, orderId, executorAddr, submission, overrides) { return this.executeOrders(symbol, [orderId], executorAddr, submission, overrides); } /** * Executes a list of orders of the symbol. This action interacts with the blockchain and incurs gas costs. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @param {string[]} orderIds IDs of the orders to be executed. * @param {string} executorAddr optional address of the wallet to be credited for executing the order, if different from the one submitting this transaction. * @param {number} nonce optional nonce * @param {PriceFeedSubmission=} submission optional signed prices obtained via PriceFeeds::fetchLatestFeedPriceInfoForPerpetual * @example * import { OrderExecutorTool, PerpetualDataHandler, Order } from "@d8x/perpetuals-sdk"; * async function main() { * console.log(OrderExecutorTool); * // Setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const pk: string = <string>process.env.PK; * const symbol = "ETH-USD-MATIC"; * let orderTool = new OrderExecutorTool(config, pk); * await orderTool.createProxyInstance(); * // get some open orders * const maxOrdersToGet = 5; * let [orders, ids]: [Order[], string[]] = await orderTool.pollLimitOrders(symbol, maxOrdersToGet); * console.log(`Got ${ids.length} orders`); * // execute * let tx = await orderTool.executeOrders(symbol, ids); * console.log(`Sent order ids ${ids.join(", ")} for execution, tx hash = ${tx.hash}`); * } * main(); * @returns Transaction object. */ async executeOrders(symbol, orderIds, executorAddr, submission, overrides) { if (this.proxyContract == null || this.signer == null) { throw Error("no proxy contract or wallet initialized. Use createProxyInstance()."); } let rpcURL; let splitTx; let maxGasLimit; if (overrides) { ({ rpcURL, splitTx, maxGasLimit, ...overrides } = overrides); } const provider = new JsonRpcProvider(rpcURL ?? this.nodeURL); if (typeof executorAddr == "undefined") { executorAddr = this.traderAddr; } if (submission == undefined) { submission = await this.priceFeedGetter.fetchLatestFeedPriceInfoForPerpetual(symbol); } const iOB = LimitOrderBook__factory.createInterface(); // update first let nonceInc = 0; let txData; let value = overrides?.value; if (splitTx) { try { const pyth = IPyth__factory.connect(this.pythAddr, provider).connect(this.signer); const priceIds = this.symbolToPerpStaticInfo.get(symbol).priceIds; const pythTx = await pyth.updatePriceFeedsIfNecessary(submission.priceFeedVaas, priceIds, submission.timestamps, { value: this.priceUpdateFee() * submission.timestamps.length, gasLimit: overrides?.gasLimit ?? this.gasLimit, nonce: overrides?.nonce, }); nonceInc += 1; // await pythTx.wait(); } catch (e) { console.log(e); } txData = iOB.encodeFunctionData("executeOrders", [orderIds, executorAddr, [], []]); } else { txData = iOB.encodeFunctionData("executeOrders", [ orderIds, executorAddr, submission.priceFeedVaas, submission.timestamps, ]); value = this.priceUpdateFee() * submission.timestamps.length; } if (overrides?.nonce != undefined) { overrides.nonce = overrides.nonce + nonceInc; } const unsignedTx = { to: this.getOrderBookContract(symbol).target, from: this.traderAddr, nonce: overrides?.nonce, data: txData, value: value, gasLimit: overrides?.gasLimit, chainId: this.chainId, // fee data, is given ...this._getFeeData(overrides), }; // no gas limit was specified, explicitly estimate if (!overrides?.gasLimit) { let gasLimit = await this.signer .estimateGas(unsignedTx) .then((gas) => (gas * 1500n) / 1000n) .catch((_e) => undefined); if (!gasLimit) { // gas estimate failed - txn would probably revert, double check (and possibly re-throw): overrides = { gasLimit: maxGasLimit ?? this.gasLimit, value: unsignedTx.value, ...overrides }; await this.getOrderBookContract(symbol).executeOrders.staticCall(orderIds, executorAddr, submission.priceFeedVaas, submission.timestamps, overrides); gasLimit = BigInt(maxGasLimit ?? this.gasLimit); } unsignedTx.gasLimit = gasLimit; } return await this.signer.connect(provider).sendTransaction(unsignedTx); } /** * Get order from the digest (=id) * @param symbol symbol of order book, e.g. ETH-USD-MATIC * @param digest digest of the order (=order ID) * @example * import { OrderExecutorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(OrderExecutorTool); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const pk: string = <string>process.env.PK; * let orderTool = new OrderExecutorTool(config, pk); * await orderTool.createProxyInstance(); * // get order by ID * let myorder = await orderTool.getOrderById("MATIC-USD-MATIC", * "0x0091a1d878491479afd09448966c1403e9d8753122e25260d3b2b9688d946eae"); * console.log(myorder); * } * main(); * * @returns order or undefined */ async getOrderById(symbol, id, overrides) { let ob = this.getOrderBookContract(symbol); // multicall let rpcURL; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); const multicall = Multicall3__factory.connect(this.config.multicall ?? MULTICALL_ADDRESS, provider); const calls = [ // 0: orderOfDigest { target: ob.target, allowFailure: false, callData: ob.interface.encodeFunctionData("orderOfDigest", [id]), }, // 1: orderDependency { target: ob.target, allowFailure: false, callData: ob.interface.encodeFunctionData("orderDependency", [id]), }, ]; const encodedResults = await multicall.aggregate3.staticCall(calls, overrides || {}); if (encodedResults.some(({ success }) => !success)) { return undefined; } const smartContractOrder = ob.interface.decodeFunctionResult("orderOfDigest", encodedResults[0].returnData); const orderDependency = ob.interface.decodeFunctionResult("orderDependency", encodedResults[1].returnData); if (smartContractOrder.traderAddr == ZERO_ADDRESS) { return undefined; } const order = OrderExecutorTool.fromSmartContractOrder(smartContractOrder, this.symbolToPerpStaticInfo); order.parentChildOrderIds = [orderDependency.parentChildDigest1, orderDependency.parentChildDigest2]; return order; } /** * Check if a conditional order can be executed * @param order order structure * @param indexPrices pair of index prices S2 and S3. S3 set to zero if not required. If undefined * the function will fetch the latest prices from the REST API * @example * import { OrderExecutorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(OrderExecutorTool); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const pk: string = <string>process.env.PK; * let orderTool = new OrderExecutorTool(config, pk); * await orderTool.createProxyInstance(); * // check if tradeable * let openOrders = await orderTool.getAllOpenOrders("MATIC-USD-MATIC"); * let check = await orderTool.isTradeable(openOrders[0][0]); * console.log(check); * } * main(); * @returns true if order can be executed for the current state of the perpetuals */ async isTradeable(order, orderId, blockTimestamp, indexPrices, overrides) { if (this.proxyContract == null || this.multicall == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } const isPred = this.isPredictionMarket(order.symbol); if (indexPrices == undefined) { indexPrices = await this.priceFeedGetter.fetchPricesForPerpetual(order.symbol); } let rpcURL; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); const fS2S3 = [indexPrices.s2, indexPrices.s3].map((x) => floatToABK64x64(x == undefined || Number.isNaN(x) ? 0 : x)); const perpId = this.getPerpIdFromSymbol(order.symbol); const fAmount = floatToABK64x64(order.quantity * (order.side == BUY_SIDE ? 1 : -1)); const orderBook = this.getOrderBookContract(order.symbol).connect(provider); const proxyCalls = [ // 0: trade amount price { target: this.proxyContract.target, allowFailure: true, callData: this.proxyContract.interface.encodeFunctionData("queryPerpetualPrice", [ perpId, fAmount, fS2S3, indexPrices.conf * 10n, indexPrices.predMktCLOBParams, ]), }, // 1: amm state to get the mark price { target: this.proxyContract.target, allowFailure: true, callData: this.proxyContract.interface.encodeFunctionData("getAMMState", [perpId, fS2S3]), }, // 2: order status to see if it's still open { target: orderBook.target, allowFailure: true, callData: orderBook.interface.encodeFunctionData("getOrderStatus", [orderId]), }, // 3: block timestamp { target: this.multicall.target, allowFailure: false, callData: this.multicall.interface.encodeFunctionData("getCurrentBlockTimestamp"), }, ]; const hasParent = order.parentChildOrderIds != undefined && order.parentChildOrderIds[0] == ZeroHash && order.parentChildOrderIds[1] != ZeroHash; if (hasParent) { // 4: order has a parent, one more call needed: proxyCalls.push({ target: orderBook.target, allowFailure: true, callData: orderBook.interface.encodeFunctionData("getOrderStatus", [order.parentChildOrderIds[1]]), }); } // multicall const multicall = Multicall3__factory.connect(this.config.multicall ?? MULTICALL_ADDRESS, provider); const encodedResults = await multicall.aggregate3.staticCall(proxyCalls, overrides || {}); // order status let iOrderStatus; if (encodedResults[2].success) { iOrderStatus = orderBook.interface.decodeFunctionResult("getOrderStatus", encodedResults[2].returnData)[0]; } else { iOrderStatus = await orderBook.getOrderStatus(orderId); } if (iOrderStatus != OrderStatus.OPEN) { // no need to continue - order is no longer open return false; } // parent status if (hasParent) { let iParentOrderStatus; if (encodedResults[4].success) { iParentOrderStatus = orderBook.interface.decodeFunctionResult("getOrderStatus", encodedResults[4].returnData)[0]; } else { iParentOrderStatus = await orderBook.getOrderStatus(order.parentChildOrderIds[1]); } if (iParentOrderStatus != OrderStatus.EXECUTED && iParentOrderStatus != OrderStatus.CANCELED) { // no need to continue - parent order is still pending return false; } } // mark price let ammState; if (encodedResults[1].success) { ammState = this.proxyContract.interface.decodeFunctionResult("getAMMState", encodedResults[1].returnData)[0]; } else { ammState = await this.proxyContract.getAMMState(perpId, fS2S3); } let markPrice; const idx_markPremRate = 8; if (isPred) { markPrice = indexPrices.ema + ABK64x64ToFloat(ammState[idx_markPremRate]); } else { markPrice = indexPrices.s2 * (1 + ABK64x64ToFloat(ammState[idx_markPremRate])); } // price let fOrderPrice; if (encodedResults[0].success) { fOrderPrice = this.proxyContract.interface.decodeFunctionResult("queryPerpetualPrice", encodedResults[0].returnData)[0]; } else { fOrderPrice = await this.proxyContract.queryPerpetualPrice(perpId, fAmount, fS2S3, indexPrices.conf * 10n, indexPrices.predMktCLOBParams); } const orderPrice = ABK64x64ToFloat(fOrderPrice); // block timestamp const ts = this.multicall.interface.decodeFunctionResult("getCurrentBlockTimestamp", encodedResults[3].returnData)[0]; blockTimestamp = Math.max(Number(ts) + 1, blockTimestamp ?? 0); return this._isTradeable(order, orderPrice, markPrice, blockTimestamp, this.symbolToPerpStaticInfo); } /** * Check for a batch of orders on the same perpetual whether they can be traded * @param orders orders belonging to 1 perpetual * @param indexPrice S2,S3-index prices for the given perpetual. Will fetch prices from REST API * if not defined. * @returns array of tradeable boolean * @example * import { OrderExecutorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(OrderExecutorTool); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const pk: string = <string>process.env.PK; * let orderTool = new OrderExecutorTool(config, pk); * await orderTool.createProxyInstance(); * // check if tradeable * let openOrders = await orderTool.getAllOpenOrders("MATIC-USD-MATIC"); * let check = await orderTool.isTradeableBatch( * [openOrders[0][0], openOrders[0][1]], * [openOrders[1][0], openOrders[1][1]] * ); * console.log(check); * } * main(); */ async isTradeableBatch(orders, orderIds, blockTimestamp, indexPrices, overrides) { const MAX_ORDERS_CHECKED = 10; let totalOrders = orders.length; let checks = await this._isTradeableBatch(orders.slice(0, MAX_ORDERS_CHECKED), orderIds.slice(0, MAX_ORDERS_CHECKED), blockTimestamp, indexPrices, overrides ?? {}); while (checks.length < totalOrders) { let res = await this._isTradeableBatch(orders.slice(checks.length, checks.length + MAX_ORDERS_CHECKED), orderIds.slice(checks.length, checks.length + MAX_ORDERS_CHECKED), blockTimestamp, indexPrices, overrides ?? {}); checks = checks.concat(res); } return checks; } /** * Performs on-chain checks via multicall * @param orders orders to check * @param orderIds order ids * @ignore */ async _isTradeableBatch(orders, orderIds, blockTimestamp, indexPrices, overrides) { if (orders.length == 0) { return []; } if (this.proxyContract == null || this.multicall == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } if (orders.filter((o) => o.symbol == orders[0].symbol).length < orders.length) { throw Error("all orders in a batch must have the same symbol"); } if (indexPrices == undefined) { indexPrices = await this.priceFeedGetter.fetchPricesForPerpetual(orders[0].symbol); } if (indexPrices.s2MktClosed || indexPrices.s3MktClosed) { // market closed return orders.map(() => false); } let rpcURL; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); const isPred = this.isPredictionMarket(orders[0].symbol); const fS2S3 = [indexPrices.s2, indexPrices.s3].map((x) => floatToABK64x64(x == undefined || Number.isNaN(x) ? 0 : x)); const perpId = this.getPerpIdFromSymbol(orders[0].symbol); const fAmounts = orders.map((order) => floatToABK64x64(order.quantity * (order.side == BUY_SIDE ? 1 : -1))); const orderBook = this.getOrderBookContract(orders[0].symbol).connect(provider); const multicall = Multicall3__factory.connect(this.config.multicall ?? MULTICALL_ADDRESS, provider); // mark price and timestamp let proxyCalls = [ // 0: amm state to get the mark price { target: this.proxyContract.target, allowFailure: false, callData: this.proxyContract.interface.encodeFunctionData("getAMMState", [perpId, fS2S3]), }, // 1: block timestamp { target: this.multicall.target, allowFailure: false, callData: this.multicall.interface.encodeFunctionData("getCurrentBlockTimestamp"), }, ]; // status calls const statusCalls = orderIds.map((orderId) => ({ target: orderBook.target, allowFailure: false, callData: orderBook.interface.encodeFunctionData("getOrderStatus", [orderId]), })); proxyCalls = proxyCalls.concat(statusCalls); // price calls const priceCalls = fAmounts.map((fAmount) => ({ target: this.proxyContract.target, allowFailure: false, callData: this.proxyContract.interface.encodeFunctionData("queryPerpetualPrice", [ perpId, fAmount, fS2S3, indexPrices.conf * 10n, indexPrices.predMktCLOBParams, ]), })); proxyCalls = proxyCalls.concat(priceCalls); // possibly also get parent orders' status const parentStatusCalls = orders .filter((order) => order.parentChildOrderIds != undefined && order.parentChildOrderIds[0] == ZeroHash && order.parentChildOrderIds[1] != ZeroHash) .map((order) => { return { target: orderBook.target, allowFailure: false, callData: orderBook.interface.encodeFunctionData("getOrderStatus", [order.parentChildOrderIds[1]]), }; }); proxyCalls = proxyCalls.concat(parentStatusCalls); // --- multicall --- const encodedResults = await multicall.aggregate3.staticCall(proxyCalls, overrides || {}); // mark price const ammState = this.proxyContract.interface.decodeFunctionResult("getAMMState", encodedResults[0].returnData)[0]; let markprice; if (isPred) { markprice = indexPrices.ema + ABK64x64ToFloat(ammState[8]); } else { markprice = indexPrices.s2 * (1 + ABK64x64ToFloat(ammState[8])); } // block timestamp const ts = this.multicall.interface.decodeFunctionResult("getCurrentBlockTimestamp", encodedResults[1].returnData)[0]; blockTimestamp = Math.max(Number(ts), blockTimestamp ?? 0); // order status const isOrderOpen = encodedResults.slice(2, 2 + orders.length).map((encodedResult) => { const iOrderStatus = orderBook.interface.decodeFunctionResult("getOrderStatus", encodedResult.returnData)[0]; return iOrderStatus == OrderStatus.OPEN; }); // order prices const orderPrices = encodedResults.slice(2 + orders.length, 2 + 2 * orders.length).map((encodedResult) => { const orderPrice = ABK64x64ToFloat(this.proxyContract.interface.decodeFunctionResult("queryPerpetualPrice", encodedResult.returnData)[0]); return orderPrice; }); // check parent status let idxInResults = 2 + 2 * orders.length; let isParentReady = new Array(orders.length).fill(true); for (let i = 0; i < orders.length; i++) { const order = orders[i]; const hasParent = order.parentChildOrderIds != undefined && order.parentChildOrderIds[0] == ZeroHash && order.parentChildOrderIds[1] != ZeroHash; if (hasParent) { const iParentStatus = orderBook.interface.decodeFunctionResult("getOrderStatus", encodedResults[idxInResults].returnData)[0]; isParentReady[i] = iParentStatus == OrderStatus.EXECUTED || iParentStatus == OrderStatus.CANCELED; idxInResults += 1; } } // sync checks return orders.map((o, idx) => { if (!isOrderOpen[idx] || !isParentReady[idx]) { return false; } return this._isTradeable(o, orderPrices[idx], markprice, blockTimestamp, this.symbolToPerpStaticInfo); }); } /** * Can the order be executed? * @param order order struct * @param tradePrice "preview" price of this order * @param markPrice current mark price * @param atBlockTimestamp block timestamp when execution would take place * @param symbolToPerpInfoMap metadata * @returns true if trading conditions met, false otherwise * @ignore */ _isTradeable(order, tradePrice, markPrice, atBlockTimestamp, symbolToPerpInfoMap) { // check expiration date if (order.deadline != undefined && order.deadline < Date.now() / 1000) { // console.log("order expired"); return false; } // check execution timestamp if (order.executionTimestamp > 0 && atBlockTimestamp < order.executionTimestamp) { // console.log(`execution deferred by ${order.executionTimestamp - atBlockTimestamp} more seconds`); return false; } if (order.submittedTimestamp != undefined && atBlockTimestamp <= order.submittedTimestamp) { // console.log(`on hold for ${order.submittedTimestamp - atBlockTimestamp} more seconds`); return false; } // check order size const lotSize = PerpetualDataHandler._getLotSize(order.symbol, symbolToPerpInfoMap); if (order.quantity < lotSize) { // console.log(`order size too small: ${order.quantity} < ${lotSize}`); return false; } // check limit price: fromSmartContractOrder will set it to undefined when not tradeable if (order.limitPrice == undefined) { // console.log("limit price undefined"); return false; } let limitPrice = order.limitPrice; if ((order.side == BUY_SIDE && tradePrice > limitPrice) || (order.side == SELL_SIDE && tradePrice < limitPrice)) { // console.log(`limit price not met: ${limitPrice} ${order.side} @ ${tradePrice}`); return false; } // check stop price if (order.stopPrice != undefined && ((order.side == BUY_SIDE && markPrice < order.stopPrice) || (order.side == SELL_SIDE && markPrice > order.stopPrice))) { // console.log("stop price not met"); return false; } // all checks passed -> order is tradeable return true; } /** * Wrapper of static method to use after mappings have been loaded into memory. * @param scOrder Perpetual order as received in the proxy events. * @returns A user-friendly order struct. */ smartContractOrderToOrder(scOrder) { return PerpetualDataHandler.fromSmartContractOrder(scOrder, this.symbolToPerpStaticInfo); } /** * Gets the current transaction count for the connected signer * @param blockTag * @returns The nonce for the next transaction */ async getTransactionCount(blockTag) { if (this.signer == null) { throw Error("no wallet initialized. Use createProxyInstance()."); } return await this.signer.getNonce(blockTag); } } //# sourceMappingURL=orderExecutorTool.js.map