UNPKG

@d8x/perpetuals-sdk

Version:

Node TypeScript SDK for D8X Perpetual Futures

353 lines 16.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const ethers_1 = require("ethers"); const factories_1 = require("./contracts/factories"); const d8XMath_1 = require("./d8XMath"); const writeAccessHandler_1 = __importDefault(require("./writeAccessHandler")); /** * Functions to liquidate traders. This class requires a private key * and executes smart-contract interactions that require gas-payments. * @extends WriteAccessHandler */ class LiquidatorTool extends writeAccessHandler_1.default { /** * Constructs a LiquidatorTool instance for a given configuration and private key. * @param {NodeSDKConfig} config Configuration object, see PerpetualDataHandler. * readSDKConfig. * @example * import { LiquidatorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(LiquidatorTool); * // load configuration for Polygon zkEVM (tesnet) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * // LiquidatorTool (authentication required, PK is an environment variable with a private key) * const pk: string = <string>process.env.PK; * let lqudtrTool = new LiquidatorTool(config, pk); * // Create a proxy instance to access the blockchain * await lqudtrTool.createProxyInstance(); * } * main(); * * @param {string | Signer} signer Private key or ethers Signer of the account */ constructor(config, signer) { super(config, signer); } async updateOracles(symbol, priceUpdates, overrides) { if (this.proxyContract == null || this.signer == null) { throw Error("no proxy contract or wallet initialized. Use createProxyInstance()."); } let rpcURL; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new ethers_1.JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); let perpID = LiquidatorTool.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); if (priceUpdates == undefined) { priceUpdates = await this.fetchLatestFeedPriceInfo(symbol); } if (!overrides || overrides.gasLimit == undefined) { overrides = { gasLimit: overrides?.gasLimit ?? this.gasLimit, ...overrides, }; } const pyth = factories_1.IPyth__factory.connect(this.pythAddr, provider).connect(this.signer); const priceIds = this.symbolToPerpStaticInfo.get(symbol).priceIds; return await pyth.updatePriceFeedsIfNecessary(priceUpdates.priceFeedVaas, priceIds, priceUpdates.timestamps, { value: this.PRICE_UPDATE_FEE_GWEI * priceUpdates.timestamps.length, gasLimit: overrides?.gasLimit ?? this.gasLimit, nonce: overrides.nonce, }); } /** * Liquidate a trader. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @param {string} traderAddr Address of the trader to be liquidated. * @param {string=} liquidatorAddr Address to be credited if the liquidation succeeds. * @param {PriceFeedSubmission} priceFeedData optional. VAA and timestamps for oracle. If not provided will query from REST API. * Defaults to the wallet used to execute the liquidation. * @example * import { LiquidatorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(LiquidatorTool); * // 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 lqudtrTool = new LiquidatorTool(config, pk); * await lqudtrTool.createProxyInstance(); * // liquidate trader * let liqAmount = await lqudtrTool.liquidateTrader("ETH-USD-MATIC", * "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"); * console.log(liqAmount); * } * main(); * * @returns Transaction object. */ async liquidateTrader(symbol, traderAddr, liquidatorAddr = "", submission, overrides) { // this operation spends gas, so signer is required if (this.proxyContract == null || this.signer == null) { throw Error("no proxy contract or wallet initialized. Use createProxyInstance()."); } // liquidator is signer unless specified otherwise if (liquidatorAddr == "") { liquidatorAddr = this.traderAddr; } let rpcURL; let splitTx; let maxGasLimit; if (overrides) { ({ rpcURL, splitTx, maxGasLimit, ...overrides } = overrides); } const provider = new ethers_1.JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); let perpID = LiquidatorTool.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); if (submission == undefined) { submission = await this.fetchLatestFeedPriceInfo(symbol); } // update first let nonceInc = 0; let txData; let value = overrides?.value; if (splitTx) { try { const pyth = factories_1.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.PRICE_UPDATE_FEE_GWEI * submission.timestamps.length, gasLimit: overrides?.gasLimit ?? this.gasLimit, nonce: overrides?.nonce, }); nonceInc += 1; // await pythTx.wait(); } catch (e) { console.log(e); } txData = this.proxyContract.interface.encodeFunctionData("liquidateByAMM", [ perpID, liquidatorAddr, traderAddr, [], [], ]); } else { txData = this.proxyContract.interface.encodeFunctionData("liquidateByAMM", [ perpID, liquidatorAddr, traderAddr, submission.priceFeedVaas, submission.timestamps, ]); value = this.PRICE_UPDATE_FEE_GWEI * submission.timestamps.length; } if (overrides?.nonce != undefined) { overrides.nonce = overrides.nonce + nonceInc; } let unsignedTx = { to: this.proxyAddr, from: this.traderAddr, nonce: overrides?.nonce, data: txData, value: value, gasLimit: overrides?.gasLimit, chainId: this.chainId, ...this._getFeeData(overrides), }; // estimate earnings const posLiquidated = await this.proxyContract.liquidateByAMM .staticCall(perpID, liquidatorAddr, traderAddr, submission.priceFeedVaas, submission.timestamps, { value, gasLimit: overrides?.gasLimit, }) .then((fAmount) => Math.abs((0, d8XMath_1.ABK64x64ToFloat)(fAmount))) .catch((e) => { console.log(e); return -1; }); if (posLiquidated <= 0) { throw Error("not liquidatable"); } // no gas limit was specified --> try again with buffered 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 }; const earnings = await this.proxyContract.liquidateByAMM.staticCall(perpID, liquidatorAddr, traderAddr, submission.priceFeedVaas, submission.timestamps, { value }); if ((0, d8XMath_1.ABK64x64ToFloat)(earnings) == 0) { throw Error("not liquidatable"); } gasLimit = BigInt(maxGasLimit ?? this.gasLimit); } unsignedTx.gasLimit = gasLimit; } return await this.signer.connect(provider).sendTransaction(unsignedTx); } /** * Check if the collateral of a trader is above the maintenance margin ("maintenance margin safe"). * If not, the position can be liquidated. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @param {string} traderAddr Address of the trader whose position you want to assess. * @param {number[]} indexPrices optional, index price S2/S3 for which we test * @example * import { LiquidatorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(LiquidatorTool); * // 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 lqudtrTool = new LiquidatorTool(config, pk); * await lqudtrTool.createProxyInstance(); * // check if trader can be liquidated * let safe = await lqudtrTool.isMaintenanceMarginSafe("ETH-USD-MATIC", * "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"); * console.log(safe); * } * main(); * * @returns {boolean} True if the trader is maintenance margin safe in the perpetual. * False means that the trader's position can be liquidated. */ async isMaintenanceMarginSafe(symbol, traderAddr, indexPrices, overrides) { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } const idx_notional = 4; let perpID = LiquidatorTool.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); if (indexPrices == undefined) { // fetch from API let obj = await this.priceFeedGetter.fetchPricesForPerpetual(symbol); indexPrices = [ obj.ema, obj.s3 ?? 0, // s3 ]; } const fIdxPx = indexPrices.map((x) => (0, d8XMath_1.floatToABK64x64)(x == undefined || Number.isNaN(x) ? 0 : x)); let traderState = await this.proxyContract.getTraderState(perpID, traderAddr, fIdxPx, overrides || {}); if (traderState[idx_notional] === 0n) { // trader does not have open position return true; } // calculate margin from traderstate const idx_maintenanceMgnRate = 10; const idx_marginAccountPositionBC = 4; const idx_collateralToQuoteConversion = 9; const idx_marginBalance = 0; const maintMgnRate = (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_maintenanceMgnRate]); const pos = (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_marginAccountPositionBC]); const marginbalance = (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_marginBalance]); const coll2quote = (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_collateralToQuoteConversion]); let threshold; if (this.isPredictionMarket(symbol)) { const idx_markPrice = 8; const markPrice = (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_markPrice]); threshold = LiquidatorTool.maintenanceMarginPredMkts(maintMgnRate, pos, coll2quote, markPrice); } else { const base2collateral = indexPrices[0] / coll2quote; threshold = Math.abs(pos * base2collateral * maintMgnRate); } return marginbalance >= threshold; } static maintenanceMarginPredMkts(maintMgnRateBase, pos, s3, markPx) { let p = markPx - 1; // p: price = 1+prob if (pos < 0) { p = 1 - p; } const tau = maintMgnRateBase + (0.4 - maintMgnRateBase) * (0, d8XMath_1.entropy)(p); return (Math.abs(pos) * p * tau) / s3; } /** * Total number of active accounts for this symbol, i.e. accounts with positions that are currently open. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @example * import { LiquidatorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(LiquidatorTool); * // 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 lqudtrTool = new LiquidatorTool(config, pk); * await lqudtrTool.createProxyInstance(); * // get number of active accounts * let accounts = await lqudtrTool.countActivePerpAccounts("ETH-USD-MATIC"); * console.log(accounts); * } * main(); * * @returns {number} Number of active accounts. */ async countActivePerpAccounts(symbol, overrides) { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } let perpID = LiquidatorTool.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); let numAccounts = await this.proxyContract.countActivePerpAccounts(perpID, overrides || {}); return Number(numAccounts); } /** * Get addresses of active accounts by chunks. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @param {number} from From which account we start counting (0-indexed). * @param {number} to Until which account we count, non inclusive. * @example * import { LiquidatorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(LiquidatorTool); * // 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 lqudtrTool = new LiquidatorTool(config, pk); * await lqudtrTool.createProxyInstance(); * // get all active accounts in chunks * let accounts = await lqudtrTool.getActiveAccountsByChunks("ETH-USD-MATIC", 0, 4); * console.log(accounts); * } * main(); * * @returns {string[]} Array of addresses at locations 'from', 'from'+1 ,..., 'to'-1. */ async getActiveAccountsByChunks(symbol, from, to, overrides) { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } let perpID = LiquidatorTool.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); return await this.proxyContract.getActivePerpAccountsByChunks(perpID, from, to, overrides || {}); } /** * Addresses for all the active accounts in this perpetual symbol. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @example * import { LiquidatorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(LiquidatorTool); * // 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 lqudtrTool = new LiquidatorTool(config, pk); * await lqudtrTool.createProxyInstance(); * // get all active accounts * let accounts = await lqudtrTool.getAllActiveAccounts("ETH-USD-MATIC"); * console.log(accounts); * } * main(); * * @returns {string[]} Array of addresses. */ async getAllActiveAccounts(symbol, overrides) { // checks are done inside the intermediate functions let totalAccounts = await this.countActivePerpAccounts(symbol); return await this.getActiveAccountsByChunks(symbol, 0, totalAccounts, overrides); } } exports.default = LiquidatorTool; //# sourceMappingURL=liquidatorTool.js.map