UNPKG

@parifi/synthetix-sdk-ts

Version:

A Typescript SDK for interactions with the Synthetix protocol

1,256 lines (1,242 loc) 90.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/perps/index.ts var perps_exports = {}; __export(perps_exports, { Perps: () => Perps }); module.exports = __toCommonJS(perps_exports); var import_viem7 = require("viem"); // src/utils/common.ts var import_viem3 = require("viem"); var import_chains3 = require("viem/chains"); var import_crypto = require("crypto"); // src/constants/protocol.ts var DEFAULT_DECIMALS = 18; // src/constants/common.ts var import_viem = require("viem"); var DISABLED_MARKETS = { 84532: [3, 6300], 8453: [6300] }; var CUSTOM_DECIMALS = { [421614 /* ARBITUM_SEPOLIA */]: { 2: 6, USDC: 6 }, [42161 /* ARBITRUM */]: { 2: 6, USDC: 6 }, [84532 /* BASE_SEPOLIA */]: { 1: 6, USDC: 6 }, [8453 /* BASE */]: { 1: 6, USDC: 6 } }; // src/contracts/addreses/zap.ts var import_viem2 = require("viem"); var ZAP_BY_CHAIN = { [8453 /* BASE */]: "0x459Bbb4231f0a606DC514BacD5712A5922CBe6c8", [84532 /* BASE_SEPOLIA */]: import_viem2.zeroAddress, [42161 /* ARBITRUM */]: "0x9ec181B2E69fB36C50031F0c87Bc0749b766A9f4", [421614 /* ARBITUM_SEPOLIA */]: "0xb569ed692206a1d73996088ae646333b1d59d9c5" }; var SYNTHETIX_ZAP = { [84532 /* BASE_SEPOLIA */]: "0xA6Fab50eB36F3eB2118eE2f4C12C968F8608bbc9", [8453 /* BASE */]: "0xA6Fab50eB36F3eB2118eE2f4C12C968F8608bbc9" }; // src/utils/common.ts function convertWeiToEther(amountInWei) { if (amountInWei == void 0) { throw new Error("Invalid amount received during conversion: undefined"); } if (typeof amountInWei == "bigint") { return Number((0, import_viem3.formatEther)(amountInWei)); } else if (typeof amountInWei == "string") { return Number((0, import_viem3.formatEther)(BigInt(amountInWei))); } else { throw new Error("Expected string or bigint for conversion"); } } function convertEtherToWei(amount) { if (amount == void 0) { throw new Error("Invalid amount received during conversion: undefined"); } if (typeof amount == "number") { return (0, import_viem3.parseEther)(amount.toString()); } else if (typeof amount == "string") { return (0, import_viem3.parseEther)(amount); } else { throw new Error("Expected string or bigint for conversion"); } } function sleep(seconds) { return new Promise((resolve) => setTimeout(resolve, seconds * 1e3)); } function generateRandomAccountId() { const maxUint128Half = import_viem3.maxUint128 / BigInt(2); const buffer = (0, import_crypto.randomBytes)(8); const randomAccountId = BigInt("0x" + buffer.toString("hex")); if (randomAccountId > maxUint128Half) { throw new Error("Account ID greater than Maxuint128"); } return randomAccountId; } var batchArray = (arr, batchSize) => { return arr.reduce((acc, _, i) => i % batchSize ? acc : [...acc, arr.slice(i, i + batchSize)], []); }; var fetcher = (url, init = { headers: { "Content-Type": "application/json" } }) => fetch(url, { ...init, body: JSON.stringify(init.body), headers: init.headers, method: init.body && !init.method ? "POST" : init.method }); var odoFetcher = async (url, options) => { return fetcher(`https://api.odos.xyz/sor${url}`, options); }; var assemblePath = async (user, pathId) => { const response = await odoFetcher(`/assemble`, { body: { userAddr: user, pathId }, headers: { "Content-Type": "application/json" } }); if (!response.ok) return ""; const data = await response.json(); return data.transaction.data; }; var getOdosPath = async (quoteParams) => { if (!quoteParams.fromToken || !quoteParams.toToken || !quoteParams.fromAmount) { throw new Error("Missing required parameters for Odos path generation"); } const data = { chainId: quoteParams.fromChain, inputTokens: [{ tokenAddress: quoteParams.fromToken, amount: quoteParams.fromAmount }], outputTokens: [{ tokenAddress: quoteParams.toToken, proportion: 1 }], userAddr: ZAP_BY_CHAIN[quoteParams.fromChain] }; const response = await odoFetcher("/quote/v2", { body: data, headers: { "Content-Type": "application/json" } }); if (!response.ok) { console.error(`Failed to get Odos quote: ${response.status} ${response.statusText}`); return { path: "" }; } const quote = await response.json(); if (!quote?.pathId) { console.error("Invalid response from Odos API: missing pathId"); return { path: "" }; } const quoteId = quote.pathId; const path = await assemblePath(data.userAddr, quoteId); if (!path) { console.error("Failed to assemble path from pathId"); } return { path }; }; // src/utils/index.ts var import_viem4 = require("viem"); // src/contracts/addreses/oracleProxy.ts var ORACLE_PROXY_BY_CHAIN = { [421614 /* ARBITUM_SEPOLIA */]: "0x59d6ec32e05900949d7fff679a4adc7f94f0208c", [42161 /* ARBITRUM */]: "0xe87ceB87b63267ef925E2897B629052eb815bB7d", [8453 /* BASE */]: "0xa03A0Be2f3B85B76765A442683d62E992972d816", [84532 /* BASE_SEPOLIA */]: "0x4Bc47c016EE6Ead253152CCfe6427D0bC06F544c" }; // src/utils/market.ts var import_viem5 = require("viem"); var Market = class { constructor(synthetixSdk) { this.disabledMarkets = []; this.sdk = synthetixSdk; this.marketsById = /* @__PURE__ */ new Map(); this.marketsByName = /* @__PURE__ */ new Map(); this.marketsBySymbol = /* @__PURE__ */ new Map(); if (synthetixSdk.rpcConfig.chainId in DISABLED_MARKETS) { this.disabledMarkets = DISABLED_MARKETS[synthetixSdk.rpcConfig.chainId]; } } /** * Look up the market_id and market_name for a market. If only one is provided, * the other is resolved. If both are provided, they are checked for consistency. * @param marketIdOrName Id or name of the market to resolve */ async resolveMarket(marketIdOrName) { if (!this.sdk.resolveMarketNames && typeof marketIdOrName === "number") { console.log("Resolve markets set to false. Returning market id without validating..."); return { resolvedMarketName: "Unresolved Market", resolvedMarketId: Number(marketIdOrName) }; } const market = await this.getMarket(marketIdOrName); if (!market?.marketName || market?.marketId === void 0) throw new Error(`Market not found for ${marketIdOrName}`); return { resolvedMarketName: market.marketName, resolvedMarketId: market.marketId }; } /** * Format the size of a synth for an order. This is used for synths whose base asset * does not use 18 decimals. For example, USDC uses 6 decimals, so we need to handle size * differently from other assets. * @param size The size as an ether value (e.g. 100). * @param marketId The id of the market. * @returns The formatted size in wei. (e.g. 100 = 100000000000000000000) */ async formatSize(size, marketId) { return (0, import_viem5.parseUnits)(size.toString(), CUSTOM_DECIMALS[this.sdk.rpcConfig.chainId][marketId] || 18); } async prepareOracleCallsWithPriceId(priceFeedIds) { if (!priceFeedIds.length) { return []; } const stalenessTolerance = 30n; const updateData = await this.sdk.pyth.getPriceFeedsUpdateData(priceFeedIds); const signedRequiredData = (0, import_viem5.encodeAbiParameters)( [ { type: "uint8", name: "updateType" }, { type: "uint64", name: "stalenessTolerance" }, { type: "bytes32[]", name: "priceIds" }, { type: "bytes[]", name: "updateData" } ], [1, stalenessTolerance, priceFeedIds, updateData] ); const pythWrapper = await this.sdk.contracts.getPythErc7412WrapperInstance(); const dataVerificationTx = this.sdk.utils.generateDataVerificationTx(pythWrapper.address, signedRequiredData); return [{ ...dataVerificationTx, value: 0n, requireSuccess: false }]; } /** * Prepare a call to the external node with oracle updates for the specified market names. * The result can be passed as the first argument to a multicall function to improve performance * of ERC-7412 calls. If no market names are provided, all markets are fetched. This is useful for * read functions since the user does not pay gas for those oracle calls, and reduces RPC calls and * runtime. * @param {number[]} marketIds An array of market ids to fetch prices for. If not provided, all markets are fetched * @returns {Promise<Call3Value[]>} objects representing the target contract, call data, value, requireSuccess flag and other necessary details for executing the function in the blockchain. */ async prepareOracleCall(marketIds = []) { let priceFeedIds = []; if (marketIds.length != 0) { const marketSymbols = []; marketIds.forEach((marketId) => { const marketSymbol = this.marketsById.get(marketId)?.symbol; if (!marketSymbol) return; marketSymbols.push(marketSymbol); }); marketSymbols.forEach((marketSymbol) => { const feedId = this.sdk.pyth.priceFeedIds.get(marketSymbol); if (!feedId) return; priceFeedIds.push(feedId); }); } else { priceFeedIds = Array.from(this.sdk.pyth.priceFeedIds.values()); } return this.prepareOracleCallsWithPriceId(priceFeedIds); } async getMarket(marketIdOrName) { throw new Error("Method not implemented. " + marketIdOrName); } }; // src/utils/override.ts var import_viem6 = require("viem"); // src/constants/slots.ts var tokensSlots = [ { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", balanceSlot: 9, approveSlot: 10 }, { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", balanceSlot: 9, approveSlot: 10 }, { address: "0x4EA71A20e655794051D1eE8b6e4A3269B13ccaCc", balanceSlot: 5, approveSlot: 6 }, { address: "0x4EA71A20e655794051D1eE8b6e4A3269B13ccaCc", balanceSlot: 5, approveSlot: 6 }, { address: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", balanceSlot: 9, approveSlot: 10 }, { address: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", balanceSlot: 9, approveSlot: 10 }, { address: "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22", balanceSlot: 51, approveSlot: 52 }, { address: "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22", balanceSlot: 51, approveSlot: 52 }, { address: "0x4200000000000000000000000000000000000006", balanceSlot: 3, approveSlot: 4 }, { address: "0x4200000000000000000000000000000000000006", balanceSlot: 3, approveSlot: 4 }, { address: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", balanceSlot: 1, approveSlot: 2 }, { address: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", balanceSlot: 1, approveSlot: 2 }, { address: "0xc43708f8987Df3f3681801e5e640667D86Ce3C30", balanceSlot: 0, approveSlot: 1 }, { address: "0x8608d511E224180051A36d34121725D978064e6E", balanceSlot: 0, approveSlot: 1 }, { address: "0x4200000000000000000000000000000000000006", balanceSlot: 3, approveSlot: 4 }, { address: "0xB3f05d39504dA95876EA0174D25Ae51Ac2422a70", balanceSlot: 0, approveSlot: 1 }, { address: "0x00ab6b818652bB3bFE334983171edFD38184DbeD", balanceSlot: 0, approveSlot: 1 }, { address: "0x7Bf65af7EFBd0E933fb87dD2C9cE7A17d959b822", balanceSlot: 0, approveSlot: 1 } ]; // src/utils/override.ts function erc20BalanceOverride({ token, owner, slot, balance = BigInt("0x100000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") }) { const smartAccountErc20BalanceSlot = (0, import_viem6.keccak256)( (0, import_viem6.encodeAbiParameters)( [ { type: "address" }, { type: "uint256" } ], [owner, slot] ) ); return [ { address: token, stateDiff: [ { slot: smartAccountErc20BalanceSlot, value: (0, import_viem6.toHex)(balance) } ] } ]; } function erc20AllowanceOverride({ token, owner, spender, slot, amount = BigInt("0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") }) { const smartAccountErc20AllowanceSlot = (0, import_viem6.keccak256)( (0, import_viem6.encodeAbiParameters)( [ { type: "address" }, { type: "bytes32" } ], [ spender, (0, import_viem6.keccak256)( (0, import_viem6.encodeAbiParameters)( [ { type: "address" }, { type: "uint256" } ], [owner, BigInt(slot)] ) ) ] ) ); return [ { address: token, stateDiff: [ { slot: smartAccountErc20AllowanceSlot, value: (0, import_viem6.toHex)(amount) } ] } ]; } var erc20StateOverrideBalanceAndAllowance = ({ token, owner, balance = BigInt("0x100000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), spender }) => { const balanceSlot = tokensSlots.find((slotData) => slotData.address.toLowerCase() === token.toLowerCase()); if (!balanceSlot) throw new Error("No balance slot"); const balanceOverride = erc20BalanceOverride({ balance, slot: BigInt(balanceSlot.balanceSlot), token, owner }); const approveOverride = erc20AllowanceOverride({ spender, amount: balance, token, slot: BigInt(balanceSlot.approveSlot), owner }); return [...balanceOverride, ...approveOverride]; }; // src/contracts/abis/zap.ts var SYNTHETIX_ZAP_ABI = [ { type: "constructor", inputs: [ { name: "perps", type: "address", internalType: "address" }, { name: "spot", type: "address", internalType: "address" }, { name: "account", type: "address", internalType: "address" } ], stateMutability: "nonpayable" }, { type: "function", name: "createAccountAndDeposit", inputs: [ { name: "accountId", type: "uint128", internalType: "uint128" }, { name: "token", type: "address", internalType: "contract IERC20" }, { name: "sToken", type: "address", internalType: "contract IERC20" }, { name: "amount", type: "uint256", internalType: "uint256" }, { name: "collateralId", type: "uint128", internalType: "uint128" } ], outputs: [{ name: "", type: "uint128", internalType: "uint128" }], stateMutability: "nonpayable" }, { inputs: [ { internalType: "uint128", name: "accountId", type: "uint128" }, { internalType: "contract IERC20[]", name: "token", type: "address[]" }, { internalType: "contract IERC20[]", name: "sToken", type: "address[]" }, { internalType: "uint256[]", name: "amount", type: "uint256[]" }, { internalType: "uint128[]", name: "collateralId", type: "uint128[]" } ], stateMutability: "nonpayable", type: "function", name: "createAccountAndDeposits", outputs: [{ internalType: "uint128", name: "", type: "uint128" }] }, { type: "function", name: "deposit", inputs: [ { name: "accountId", type: "uint128", internalType: "uint128" }, { name: "token", type: "address", internalType: "contract IERC20" }, { name: "sToken", type: "address", internalType: "contract IERC20" }, { name: "amount", type: "uint256", internalType: "uint256" }, { name: "collateralId", type: "uint128", internalType: "uint128" } ], stateMutability: "nonpayable" }, { type: "function", name: "withdraw", inputs: [ { name: "accountId", type: "uint128", internalType: "uint128" }, { name: "token", type: "address", internalType: "contract IERC20" }, { name: "amount", type: "int256", internalType: "int256" }, { name: "collateralId", type: "uint128", internalType: "uint128" } ], outputs: [], stateMutability: "nonpayable" }, { type: "function", name: "createAccountDepositAndCommit", inputs: [ { name: "accountId", type: "uint128", internalType: "uint128" }, { name: "token", type: "address", internalType: "contract IERC20" }, { name: "sToken", type: "address", internalType: "contract IERC20" }, { name: "amount", type: "uint256", internalType: "uint256" }, { name: "collateralId", type: "uint128", internalType: "uint128" }, { name: "commit", type: "tuple", internalType: "struct SynthetixZap.Commit", components: [ { name: "marketId", type: "uint128", internalType: "uint128" }, { name: "sizeDelta", type: "int128", internalType: "int128" }, { name: "settlementStrategyId", type: "uint128", internalType: "uint128" }, { name: "acceptablePrice", type: "uint256", internalType: "uint256" }, { name: "trackingCode", type: "bytes32", internalType: "bytes32" }, { name: "referrer", type: "address", internalType: "address" } ] } ], outputs: [{ name: "", type: "uint128", internalType: "uint128" }], stateMutability: "nonpayable" } ]; // src/perps/index.ts var Perps = class extends Market { constructor(synthetixSdk) { super(synthetixSdk); // Markets data this.isErc7412Enabled = true; // Set multicollateral to false by default this.isMulticollateralEnabled = false; this.disabledMarkets = []; this.isInitialized = false; this.sdk = synthetixSdk; this.accountIds = []; this.marketMetadata = /* @__PURE__ */ new Map(); } // === READ CALLS === async initPerps() { if (this.isInitialized) return; await this.getAccountIds(); const marketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const debtFunctionData = (0, import_viem7.getAbiItem)({ abi: marketProxy.abi, name: "debt" }); const payDebtFunctionData = (0, import_viem7.getAbiItem)({ abi: marketProxy.abi, name: "payDebt" }); if (debtFunctionData != void 0 && payDebtFunctionData != void 0) { this.isMulticollateralEnabled = true; this.sdk.logger.info("Multicollateral perps is enabled"); } this.isInitialized = true; } /** * Fetch a list of perps ``account_id`` owned by an address. Perps accounts * are minted as an NFT to the owner's address. The ``account_id`` is the * token id of the NFTs held by the address. * @param {string} address The address to get accounts for. Uses connected address if not provided. * @param {bigint} defaultAccountId The default account ID to set after fetching. * @returns A list of account IDs owned by the address */ async getAccountIds(accountAddress = this.sdk.accountAddress, defaultAccountId) { if (!accountAddress) throw new Error("Invalid address"); const accountProxy = await this.sdk.contracts.getPerpsAccountProxyInstance(); const balance = await accountProxy.read.balanceOf([accountAddress]); this.sdk.logger.info("balance", balance); const argsList = []; for (let index = 0; index < Number(balance); index++) { argsList.push([accountAddress, index]); } const accountIds = await this.sdk.utils.multicallErc7412({ contractAddress: accountProxy.address, abi: accountProxy.abi, functionName: "tokenOfOwnerByIndex", args: argsList }); if (accountIds == void 0) return []; this.accountIds = accountIds; this.sdk.logger.info("accountIds", accountIds); if (defaultAccountId) { this.defaultAccountId = defaultAccountId; } else if (this.accountIds.length > 0) { this.defaultAccountId = this.accountIds[0]; this.sdk.logger.info("Using default account id as ", this.defaultAccountId); } return accountIds; } async getMarkets() { const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const marketIdResponse = await perpsMarketProxy.read.getMarkets(); const marketMetadataResponse = await this.sdk.utils.multicallErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "metadata", args: marketIdResponse }); const marketIds = marketIdResponse.map((id) => Number(id)); const settlementStrategies = await this.getSettlementStrategies(marketIds); const { marketMetadatas, pythPriceIds } = marketMetadataResponse.reduce( (acc, market, index) => { const [name, symbol] = market; const strategy = settlementStrategies.find((strategy2) => strategy2.marketId == marketIds[index]); acc.pythPriceIds.push({ symbol, feedId: strategy?.feedId ?? "0x" }); acc.marketMetadatas.push({ marketName: name, symbol, feedId: strategy?.feedId ?? "0x" }); return acc; }, { pythPriceIds: [], marketMetadatas: [] } ); this.sdk.pyth.updatePriceFeedIds(pythPriceIds); const [marketSummaries, fundingParameters, orderFees, maxMarketValues] = await Promise.all([ this.getMarketSummaries(marketIds), this.getFundingParameters(marketIds), this.getOrderFees(marketIds), this.getMaxMarketValues(marketIds) ]); const datas = marketMetadatas.map((marketMetadata, index) => { const marketId = Number(marketIds.at(index) || 0); const marketSummary = marketSummaries.find((summary) => summary.marketId == marketId); const fundingParam = fundingParameters.find((fundingParam2) => fundingParam2.marketId == marketId); const orderFee = orderFees.find((orderFee2) => orderFee2.marketId == marketId); const maxMarketValue = maxMarketValues.find((maxMarketValue2) => maxMarketValue2.marketId == marketId); const result = { marketId: marketIds[index], marketName: marketMetadata.marketName, symbol: marketMetadata.symbol, feedId: marketMetadata.feedId, skew: marketSummary?.skew, size: marketSummary?.size, maxOpenInterest: marketSummary?.maxOpenInterest, interestRate: marketSummary?.interestRate, currentFundingRate: marketSummary?.currentFundingRate, currentFundingVelocity: marketSummary?.currentFundingVelocity, indexPrice: marketSummary?.indexPrice, skewScale: fundingParam?.skewScale, maxFundingVelocity: fundingParam?.maxFundingVelocity, makerFee: orderFee?.makerFeeRatio, takerFee: orderFee?.takerFeeRatio, maxMarketValue: maxMarketValue?.maxMarketValue }; this.marketMetadata.set(marketId, marketMetadata); this.marketsById.set(marketId, result); this.marketsByName.set(marketMetadata.marketName, result); this.marketsBySymbol.set(marketMetadata.symbol, result); return result; }); return datas; } async getMarket(marketIdOrName) { const market = (this.marketsById.get(Number(marketIdOrName)) ?? this.marketsByName.get(marketIdOrName)) || {}; if (market.marketId) { return market; } let marketId, marketName, marketSymbol; const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); if (typeof marketIdOrName === "number" || typeof marketIdOrName === "bigint") { const marketMetadataResponse = await this.sdk.utils.callErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "metadata", args: [marketIdOrName] }); marketId = Number(marketIdOrName); marketName = marketMetadataResponse[0]; marketSymbol = marketMetadataResponse[1]; } else { const marketIdResponse = await perpsMarketProxy.read.getMarkets(); const marketMetadataResponse = await this.sdk.utils.multicallErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "metadata", args: marketIdResponse }); marketMetadataResponse.forEach((metadata, idx) => { if (metadata[0] == marketIdOrName) { marketId = marketIdResponse[idx]; marketName = metadata[0]; marketSymbol = metadata[1]; } }); } const functionNames = []; const argsList = []; functionNames.push("getSettlementStrategy"); argsList.push([marketId, 0]); functionNames.push("getMarketSummary"); argsList.push([marketId]); functionNames.push("interestRate"); argsList.push([]); functionNames.push("getFundingParameters"); argsList.push([marketId]); functionNames.push("getOrderFees"); argsList.push([marketId]); functionNames.push("getMaxMarketValue"); argsList.push([marketId]); const multicallResponse = await this.sdk.utils.multicallMultifunctionErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionNames, args: argsList }); const settlementStrategy = multicallResponse.at(0); const marketSummary = multicallResponse.at(1); const interestRate = multicallResponse.at(2); const fundingParams = multicallResponse.at(3); const orderFees = multicallResponse.at(4); const maxMarketValue = multicallResponse.at(5); market.marketId = marketId || 0; market.marketName = marketName || "INVALID MARKET"; market.symbol = marketSymbol || "INVALID"; market.feedId = settlementStrategy.feedId; market.skew = Number((0, import_viem7.formatEther)(marketSummary.skew)); market.size = Number((0, import_viem7.formatEther)(marketSummary.size)); market.maxOpenInterest = Number((0, import_viem7.formatEther)(marketSummary.maxOpenInterest)); market.interestRate = Number((0, import_viem7.formatEther)(interestRate)); market.currentFundingRate = Number((0, import_viem7.formatEther)(marketSummary.currentFundingRate)); market.currentFundingVelocity = Number((0, import_viem7.formatEther)(marketSummary.currentFundingVelocity)); market.indexPrice = Number((0, import_viem7.formatEther)(marketSummary.indexPrice)); market.skewScale = Number((0, import_viem7.formatEther)(fundingParams.at(0) || 0n)); market.maxFundingVelocity = Number((0, import_viem7.formatEther)(fundingParams.at(1) || 0n)); market.makerFee = Number((0, import_viem7.formatEther)(orderFees.at(0) || 0n)); market.takerFee = Number((0, import_viem7.formatEther)(orderFees.at(1) || 0n)); market.maxMarketValue = Number((0, import_viem7.formatEther)(maxMarketValue)); this.marketMetadata.set(market.marketId || 0, { marketName: market.marketName, symbol: market.symbol, feedId: market.feedId }); this.marketsById.set(market.marketId, market); this.marketsByName.set(market.marketName, market); this.marketsBySymbol.set(market.symbol, market); return market; } /** * Fetch the market summaries for an array of marketIds * @param marketIds Array of market ids to fetch * @returns Summary of market ids data fetched from the contract */ async getMarketSummaries(marketIds) { const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const interestRate = await this.sdk.utils.callErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "interestRate", args: [] }); const marketSummariesInput = marketIds.map((marketId) => [marketId]); const marketSummariesResponse = await this.sdk.utils.multicallErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "getMarketSummary", args: marketSummariesInput }); if (marketIds.length !== marketSummariesResponse.length) { this.sdk.logger.info("Inconsistent data"); } const marketSummaries = []; const batchedResponse = batchArray( marketSummariesResponse.map((market, index) => { return { ...market, marketId: marketIds[index] }; }), 10 ); for (const batch of batchedResponse) { const promises = batch.map(async (market) => { const marketId = market.marketId; marketSummaries.push({ marketId, marketName: (await this.getMarket(marketId)).marketName, feedId: (await this.getMarket(marketId)).feedId, indexPrice: Number((0, import_viem7.formatEther)(market.indexPrice)), skew: Number((0, import_viem7.formatEther)(market.skew)), size: Number((0, import_viem7.formatEther)(market.size)), maxOpenInterest: Number((0, import_viem7.formatEther)(market.maxOpenInterest)), interestRate: Number((0, import_viem7.formatEther)(interestRate)), currentFundingRate: Number((0, import_viem7.formatEther)(market.currentFundingRate)), currentFundingVelocity: Number((0, import_viem7.formatEther)(market.currentFundingVelocity)) }); }); await Promise.allSettled(promises); } return marketSummaries; } /** * @name getMarketSummary * @description Fetches the summary of a given market. This includes information like skew, size, max open interest, current funding rate, and more. * @param {string|number} marketIdOrName - The identifier or name of the market to fetch the summary for. * @returns {MarketSummary} - An object containing the summary data for the specified market. */ async getMarketSummary(marketIdOrName) { const { resolvedMarketId, resolvedMarketName } = await this.resolveMarket(marketIdOrName); const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const interestRate = await this.sdk.utils.callErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "interestRate", args: [] }); const marketSummaryResponse = await this.sdk.utils.callErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "getMarketSummary", args: [resolvedMarketId] }); this.sdk.logger.info("marketSummaryResponse", marketSummaryResponse); return { marketId: resolvedMarketId, marketName: resolvedMarketName, feedId: "", indexPrice: Number((0, import_viem7.formatEther)(marketSummaryResponse.indexPrice)), skew: Number((0, import_viem7.formatEther)(marketSummaryResponse.skew)), size: Number((0, import_viem7.formatEther)(marketSummaryResponse.size)), maxOpenInterest: Number((0, import_viem7.formatEther)(marketSummaryResponse.maxOpenInterest)), interestRate: Number((0, import_viem7.formatEther)(interestRate)), currentFundingRate: Number((0, import_viem7.formatEther)(marketSummaryResponse.currentFundingRate)), currentFundingVelocity: Number((0, import_viem7.formatEther)(marketSummaryResponse.currentFundingVelocity)) }; } /** * @name getSettlementStrategy * @description This function retrieves the settlement strategy for a given market by its ID and either Market ID or Name. * @param {number} settlementStrategyId - The ID of the settlement strategy to retrieve. * @param {MarketIdOrName} marketIdOrName - The unique identifier (ID or Name) of the market this settlement strategy belongs to. * @returns {SettlementStrategy} - An object containing the details of the retrieved settlement strategy, including its strategy type, delay times, associated contract addresses, feed ID, settlement reward, and whether it is disabled or not. */ async getSettlementStrategy(settlementStrategyId, marketIdOrName) { const { resolvedMarketId } = await this.resolveMarket(marketIdOrName); const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const settlementStrategy = await this.sdk.utils.callErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "getSettlementStrategy", args: [resolvedMarketId, settlementStrategyId] }); return { marketId: resolvedMarketId, strategyType: settlementStrategy.strategyType, settlementDelay: Number(settlementStrategy.settlementDelay), settlementWindowDuration: Number(settlementStrategy.settlementWindowDuration), priceVerificationContract: settlementStrategy.priceVerificationContract, feedId: settlementStrategy.feedId, settlementReward: Number((0, import_viem7.formatEther)(settlementStrategy.settlementReward ?? 0n)), disabled: settlementStrategy.disabled, commitmentPriceDelay: Number(settlementStrategy.commitmentPriceDelay) }; } /** * Fetch the settlement strategies for an array of market ids. Settlement strategies describe the * conditions under which an order can be settled. * @param marketIds Array of marketIds to fetch settlement strategy * @returns Settlement strategy array for markets */ async getSettlementStrategies(marketIds) { const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const settlementStrategies = []; const argsList = marketIds.map((marketId) => [marketId, 0]); const settlementStrategiesResponse = await this.sdk.utils.multicallErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "getSettlementStrategy", args: argsList }); settlementStrategiesResponse.forEach((strategy, index) => { settlementStrategies.push({ marketId: marketIds[index], strategyType: strategy.strategyType, settlementDelay: Number(strategy.settlementDelay), settlementWindowDuration: Number(strategy.settlementWindowDuration), priceVerificationContract: strategy.priceVerificationContract, feedId: strategy.feedId, settlementReward: Number((0, import_viem7.formatEther)(strategy.settlementReward ?? 0n)), disabled: strategy.disabled, commitmentPriceDelay: Number(strategy.commitmentPriceDelay) }); }); return settlementStrategies; } /** * Fetch funding parameters for an array of market ids. * @param marketIds Array of marketIds to fetch settlement strategy * @returns Funding Parameters array for markets */ async getFundingParameters(marketIds) { const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const fundingParams = []; const fundingParamsResponse = await this.sdk.utils.multicallErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "getFundingParameters", args: marketIds }); fundingParamsResponse.forEach((param, index) => { fundingParams.push({ marketId: marketIds[index], skewScale: Number((0, import_viem7.formatEther)(param[0])), maxFundingVelocity: Number((0, import_viem7.formatEther)(param[1])) }); }); return fundingParams; } /** * Gets the order fees of a market. * @param marketIds Array of market ids. * @return Order fees array for markets */ async getOrderFees(marketIds) { const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const orderFees = []; const orderFeesResponse = await this.sdk.utils.multicallErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "getOrderFees", args: marketIds }); orderFeesResponse.forEach((param, index) => { orderFees.push({ marketId: marketIds[index], makerFeeRatio: Number((0, import_viem7.formatEther)(param[0])), takerFeeRatio: Number((0, import_viem7.formatEther)(param[1])) }); }); return orderFees; } /** * Gets the max size (in value) of an array of marketIds. * @param marketIds Array of market ids. * @return Max market size in market USD value for each market */ async getMaxMarketValues(marketIds) { const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const maxMarketValues = []; const maxMarketValuesResponse = await this.sdk.utils.multicallErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "getMaxMarketValue", args: marketIds }); maxMarketValuesResponse.forEach((maxMarketValue, index) => { maxMarketValues.push({ marketId: marketIds[index], maxMarketValue: Number((0, import_viem7.formatEther)(maxMarketValue)) }); }); return maxMarketValues; } /** * Fetches the open order for an account. Optionally fetches the settlement strategy, * which can be useful for order settlement and debugging. * @param {bigint} accountId The id of the account. If not provided, the default account is used * @param {boolean} fetchSettlementStrategy Flag to indicate whether to fetch the settlement strategy * @returns {OrderData} The order data for the account */ async getOrder(accountId = this.defaultAccountId, fetchSettlementStrategy = true) { if (!accountId) throw new Error("Invalid account id"); const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const orderResponse = await this.sdk.utils.callErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "getOrder", args: [accountId] }); const orderReq = orderResponse.request; const orderData = { marketId: Number(orderReq.marketId), commitmentTime: Number(orderResponse.commitmentTime), accountId: orderReq.accountId, sizeDelta: Number((0, import_viem7.formatEther)(orderReq.sizeDelta)), settlementStrategyId: Number(orderReq.settlementStrategyId), acceptablePrice: Number(orderReq.acceptablePrice), trackingCode: orderReq.trackingCode, referrer: orderReq.referrer }; if (fetchSettlementStrategy) { const strategy = await this.getSettlementStrategy( Number(orderReq.settlementStrategyId), Number(orderReq.marketId) ); orderData.settlementStrategy = strategy; } return orderData; } /** * Fetch information about an account's margin requirements and balances. * Accounts must maintain an ``available_margin`` above the ``maintenance_margin_requirement`` * to avoid liquidation. Accounts with ``available_margin`` below the ``initial_margin_requirement`` * can not interact with their position unless they deposit more collateral. * @param {bigint} accountId The id of the account to fetch the margin info for. If not provided, the default account is used * @returns {CollateralData} The margin information for the account */ async getMarginInfo(accountId = this.defaultAccountId) { if (!accountId) throw new Error("Invalid account ID"); const marketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const functionNames = []; const argsList = []; functionNames.push("totalCollateralValue"); argsList.push([accountId]); functionNames.push("getAvailableMargin"); argsList.push([accountId]); functionNames.push("getWithdrawableMargin"); argsList.push([accountId]); functionNames.push("getRequiredMargins"); argsList.push([accountId]); const multicallResponse = await this.sdk.utils.multicallMultifunctionErc7412({ contractAddress: marketProxy.address, abi: marketProxy.abi, functionNames, args: argsList }); const totalCollateralValue = multicallResponse.at(0); const availableMargin = multicallResponse.at(1); const withdrawableMargin = multicallResponse.at(2); const requiredMarginsResponse = multicallResponse.at(3); const requiredInitialMargin = requiredMarginsResponse.at(0); const requiredMaintenanceMargin = requiredMarginsResponse.at(1); const maxLiquidationReward = requiredMarginsResponse.at(2); const collateralAmountsRecord = []; let debt = 0; if (this.isMulticollateralEnabled) { const fNames = []; const aList = []; fNames.push("getAccountCollateralIds"); aList.push([accountId]); fNames.push("debt"); aList.push([accountId]); const response = await this.sdk.utils.multicallMultifunctionErc7412({ contractAddress: marketProxy.address, abi: marketProxy.abi, functionNames: fNames, args: aList }); const collateralIds = response.at(0); debt = convertWeiToEther(response.at(1)); if (collateralIds.length != 0) { const inputs = collateralIds.map((id) => { return [accountId, id]; }); this.sdk.logger.info("inputs", inputs); const collateralAmounts = await this.sdk.utils.multicallErc7412({ contractAddress: marketProxy.address, abi: marketProxy.abi, functionName: "getCollateralAmount", args: inputs }); collateralIds.forEach((collateralId, index) => { collateralAmountsRecord[Number(collateralId)] = convertWeiToEther(collateralAmounts.at(index)); }); } } else { collateralAmountsRecord[0] = convertWeiToEther(totalCollateralValue); } const marginInfo = { totalCollateralValue: convertWeiToEther(totalCollateralValue), collateralBalances: collateralAmountsRecord, debt, availableMargin: convertWeiToEther(availableMargin), withdrawableMargin: convertWeiToEther(withdrawableMargin), initialMarginRequirement: convertWeiToEther(requiredInitialMargin), maintenanceMarginRequirement: convertWeiToEther(requiredMaintenanceMargin), maxLiquidationReward: convertWeiToEther(maxLiquidationReward) }; this.sdk.logger.info(`${marginInfo}marginInfo`); return marginInfo; } /** * @name buildModifyCollateral * @description This function builds the call to modify collateral in a perp market. It takes an amount, market ID or name, account ID, and collateral ID as parameters and returns an array of Call3Value objects. * @param {string | number} data.amount - The amount of the underlying asset to modify collateral for. * @param {string | number} data.marketIdOrName - The ID or name of the perp market where the collateral will be modified. * @param {string} data.accountId - The ID of the account whose collateral is being modified. * @param {string} data.collateralId - The ID of the collateral being modified. * @returns {Call3Value[]} - An array of Call3Value objects containing the target contract address, call data, value, requireSuccess flag, and other relevant information for executing the 'modifyCollateral' function on the market proxy contract. */ async buildModifyCollateral({ amount, collateralMarketIdOrName, accountId }) { const { resolvedMarketId: collateralMarketId, resolvedMarketName: collateralMarketName } = await this.sdk.spot.resolveMarket(collateralMarketIdOrName); this.sdk.logger.info(`Building ${amount} ${collateralMarketName} for account ${accountId}`); const synthCollateral = await this.sdk.spot.getMarket(collateralMarketId); const collateral = await this.sdk.contracts.getCollateralInstance( (synthCollateral.marketName ?? "Unresolved Market").replace("s", "") ); const zapContract = SYNTHETIX_ZAP[this.sdk.rpcConfig.chainId]; const formattedAmount = await this.formatSize(amount, amount > 0 ? collateralMarketId : 0); if (amount > 0) return { target: zapContract, callData: (0, import_viem7.encodeFunctionData)({ abi: SYNTHETIX_ZAP_ABI, functionName: "deposit", // all wrap versions have 18 decimals so setting collateralId default to 0 args: [accountId, collateral.address, synthCollateral.contractAddress, formattedAmount, collateralMarketId] }), value: 0n, requireSuccess: true }; return { target: zapContract, callData: (0, import_viem7.encodeFunctionData)({ abi: SYNTHETIX_ZAP_ABI, functionName: "withdraw", args: [accountId, collateral.address, formattedAmount, collateralMarketId] }), value: 0n, requireSuccess: true }; } /** * Fetch the balance of each collateral type for an account. * @param {bigint} accountId The id of the account to fetch the collateral balances for. If not provided, the default account is used. * @returns {Promise<number>} The balance of the account's collateral. */ async getCollateralBalances(accountId) { throw new Error("Not implemented " + accountId); } /** * Check if an `accountId` is eligible for liquidation. * @param accountId The id of the account to check. If not provided, the default account is used. * @returns {Promise<boolean>} A boolean indicating whether the account can be liquidated. */ async getCanLiquidate(accountId = this.defaultAccountId) { if (!accountId) throw new Error("Invalid account ID"); const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const canBeLiquidated = await this.sdk.utils.callErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "canLiquidate", args: [accountId] }); this.sdk.logger.info("canBeLiquidated", canBeLiquidated); return canBeLiquidated; } /** * Check if a batch of `accountId`'s are eligible for liquidation. * @param {string[]} accountIds An array of account ids * @returns {Promise<{ accountId: bigint; canLiquidate: boolean }[]>} An array of objects containing the account id and whether the account can be liquidated. */ async getCanLiquidates(accountIds = void 0) { const perpsMarketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); if (accountIds == void 0) { if (this.defaultAccountId != void 0) { accountIds = this.accountIds; } else { throw new Error("Invalid account ID"); } } const input = accountIds.map((accountId) => [accountId]); const canLiquidatesResponse = await this.sdk.utils.multicallErc7412({ contractAddress: perpsMarketProxy.address, abi: perpsMarketProxy.abi, functionName: "canLiquidate", args: input }); const canLiquidates = canLiquidatesResponse.map((response, index) => { return { accountId: accountIds.at(index) ?? 0n, canLiquidate: response }; }); this.sdk.logger.info("canLiquidates", canLiquidates); return canLiquidates; } /** * Fetch the position for a specified account and market. The result includes the unrealized * pnl since the last interaction with this position, any accrued funding, and the position size. * Provide either a ``marketId`` or a ``marketName``:: * @param {string | number} marketIdOrName - The identifier or name of the market for which the collateral is being modified * @param {bigint} accountId The id of the account to fetch the position for. If not provided, the default account is used. * @returns {OpenPositionData} An object containing the open position data for the specified market. */ async getOpenPosition(marketIdOrName, accountId = this.defaultAccountId) { if (!accountId) throw new Error("Account ID is required"); const marketProxy = await this.sdk.contracts.getPerpsMarketProxyInstance(); const { resolvedMarketId, resolvedMarketName } = await this.resolveMarket(marketIdOrName); const response = await this.sdk.utils.callErc7412({ contractAddress: marketProxy.address, abi: marketProxy.abi, functionName: "getOpenPosition", args: [accountId, resolvedMarketId] }); const openPositionData = { accountId, marketId: resolvedMarketId, marketName: resolvedMarketName, totalPnl: convertWeiToEther(response.at(0)), accruedFunding: convertWeiToEther(response.at(1)), positionSize: convertWeiToEther(response.at(2)), owedInterest: convertWeiToEther(response.at(3)) }; this.sdk.logger.info("openPositionData", openPositionData); return openPositionData; } /** * Fetch positions for an array of specified markets. The result includes the unrealized * pnl since the last interaction with this position, any accrued funding, and the position size. * Provide either an array of ``marketIds`` or a ``marketNames``:: * @param {MarketIdOrName} marketIdsOrNames - The ID or name of the Perpetual market to get a quote for. * @param {bigint} accountId The id of the account to fetch the position for. If not provided, the default account is used. * @returns {OpenPositionData[]} An array of objects containing the open position data for the specified markets. */ // NOTE: maybe is better use subgraph? async getOpenPositions(marketIdsOrNames, accountId = this.defaultAccountId) { if (!accountId) throw new Error("Account ID is required"); const marketProxy = await this.sdk.contracts.getPerps