@d8x/perpetuals-sdk
Version:
Node TypeScript SDK for D8X Perpetual Futures
353 lines • 16.8 kB
JavaScript
"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.priceUpdateFee() * 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.priceUpdateFee() * 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.priceUpdateFee() * 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 idx_lockedInValue = 5;
const markPrice = (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_markPrice]);
const ell = (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_lockedInValue]);
threshold = LiquidatorTool.maintenanceMarginPredMkts(maintMgnRate, pos, coll2quote, markPrice, ell);
}
else {
const base2collateral = indexPrices[0] / coll2quote;
threshold = Math.abs(pos * base2collateral * maintMgnRate);
}
return marginbalance >= threshold;
}
static maintenanceMarginPredMkts(maintMgnRateBase, pos, s3, markPx, ell) {
const s = pos > 0 ? 1 : -1; // pos is
const R = pos == 0 ? markPx - 1 : s > 0 ? Math.abs(ell / pos) - 1 : 2 - Math.abs(ell / pos);
const Rm = s > 0 ? markPx - 1 : 2 - markPx;
const L = Math.max(0, s * (R - Rm));
return (Math.abs(pos) * Math.min(maintMgnRateBase + L, R)) / s3; // liquidated if E < that
}
/**
* 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