From b3b53b0b074f9f1bb71565eea473e7ed208f8c80 Mon Sep 17 00:00:00 2001 From: Sudeep Biswas Date: Wed, 7 Feb 2024 16:25:42 +0530 Subject: [PATCH 1/2] Add function to batch settle all pending orders --- package-lock.json | 331 +++++++++++++++++- package.json | 3 + src/common/constants.ts | 3 + src/contract-logic/order-manager/index.ts | 12 + .../order-manager/settlement.ts | 99 ++++++ src/contract-logic/parifi-utils/index.ts | 12 + src/gelato/index.ts | 19 + src/pyth/index.ts | 2 + src/subgraph/common/types.ts | 5 + test/contract-logic/parifi-utils.test.ts | 19 + 10 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 src/contract-logic/order-manager/settlement.ts create mode 100644 src/contract-logic/parifi-utils/index.ts create mode 100644 src/gelato/index.ts create mode 100644 test/contract-logic/parifi-utils.test.ts diff --git a/package-lock.json b/package-lock.json index 25b501b..8e19160 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,12 @@ "version": "0.0.8", "license": "ISC", "dependencies": { + "@gelatonetwork/relay-sdk": "^5.5.5", + "@parifi/references": "^0.2.4", "axios": "^1.6.7", "decimal.js": "^10.4.3", "dotenv": "^16.4.1", + "ethers": "^6.10.0", "graphql": "^16.8.1", "graphql-request": "^6.1.0" }, @@ -24,6 +27,11 @@ "typescript": "^5.3.3" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -1036,6 +1044,97 @@ "node": ">=12" } }, + "node_modules/@gelatonetwork/relay-sdk": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/@gelatonetwork/relay-sdk/-/relay-sdk-5.5.5.tgz", + "integrity": "sha512-aLiN8CmWBTei5JMoSg3LHX3MmB+IoaM+Rw1kkm9x/ECrS6FhWZH1WU76+xvmeIVnbncEUc3xNOQfz7UAwUZI0w==", + "dependencies": { + "axios": "0.27.2", + "ethers": "6.7.0", + "isomorphic-ws": "^5.0.0", + "ws": "^8.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@gelatonetwork/relay-sdk/node_modules/@adraffy/ens-normalize": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", + "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==" + }, + "node_modules/@gelatonetwork/relay-sdk/node_modules/@noble/hashes": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", + "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@gelatonetwork/relay-sdk/node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" + }, + "node_modules/@gelatonetwork/relay-sdk/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/@gelatonetwork/relay-sdk/node_modules/ethers": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.0.tgz", + "integrity": "sha512-pxt5hK82RNwcTX2gOZP81t6qVPVspnkpeivwEgQuK9XUvbNtghBnT8GNIb/gPh+WnVSfi8cXC9XlfT8sqc6D6w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.9.2", + "@noble/hashes": "1.1.2", + "@noble/secp256k1": "1.7.1", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@gelatonetwork/relay-sdk/node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@graphql-typed-document-node/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", @@ -1496,6 +1595,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1531,6 +1663,14 @@ "node": ">= 8" } }, + "node_modules/@parifi/references": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@parifi/references/-/references-0.2.4.tgz", + "integrity": "sha512-fVkGIeF49ZxxyZel5lDlfo2WfcTtYdcSVWLPNLcwabN7DR53182JXNy6IqwoPPe/SlxHJ1eYGxHEMYWZFhWT0A==", + "dependencies": { + "viem": "^1.14.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1710,6 +1850,39 @@ "win32" ] }, + "node_modules/@scure/base": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", + "dependencies": { + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1854,6 +2027,34 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/abitype": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.9.8.tgz", + "integrity": "sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.19.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2695,6 +2896,58 @@ "node": ">=4" } }, + "node_modules/ethers": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.10.0.tgz", + "integrity": "sha512-nMNwYHzs6V1FR3Y4cdfxSQmNgZsRj1RiTU25JwvnJLmyzw9z3SKxNc2XKDuiXXo/v9ds5Mp9m6HBabgYQQ26tA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3210,6 +3463,28 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isows": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.3.tgz", + "integrity": "sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -5365,6 +5640,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/tsup": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.0.1.tgz", @@ -5439,7 +5719,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5498,6 +5778,35 @@ "node": ">=10.12.0" } }, + "node_modules/viem": { + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/viem/-/viem-1.21.4.tgz", + "integrity": "sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@scure/bip32": "1.3.2", + "@scure/bip39": "1.2.1", + "abitype": "0.9.8", + "isows": "1.0.3", + "ws": "8.13.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -5649,6 +5958,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 8d1fdf7..7d7a818 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,12 @@ "typescript": "^5.3.3" }, "dependencies": { + "@gelatonetwork/relay-sdk": "^5.5.5", + "@parifi/references": "^0.2.4", "axios": "^1.6.7", "decimal.js": "^10.4.3", "dotenv": "^16.4.1", + "ethers": "^6.10.0", "graphql": "^16.8.1", "graphql-request": "^6.1.0" } diff --git a/src/common/constants.ts b/src/common/constants.ts index ce3ccd2..c97a236 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -8,3 +8,6 @@ export const WAD = new Decimal(10).pow(18); // 10^18 export const EMPTY_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; export const PRICE_FEED_DECIMALS = 8; export const DECIMAL_10 = new Decimal(10); +export const DECIMAL_ZERO = new Decimal(0); +export const DEFAULT_BATCH_COUNT = 10; + diff --git a/src/contract-logic/order-manager/index.ts b/src/contract-logic/order-manager/index.ts index 2c725c9..a82c467 100644 --- a/src/contract-logic/order-manager/index.ts +++ b/src/contract-logic/order-manager/index.ts @@ -3,6 +3,18 @@ import { Decimal } from 'decimal.js'; import { DEVIATION_PRECISION_MULTIPLIER, MAX_FEE, PRECISION_MULTIPLIER } from '../../common/constants'; import { getAccruedBorrowFeesInMarket, getMarketUtilization } from '../data-fabric'; import { convertMarketAmountToCollateral } from '../price-feed'; +import { Chain } from '@parifi/references'; +import { contracts as parifiContracts } from '@parifi/references'; +import { Contract, ethers } from 'ethers'; + +// Returns an Order Manager contract instance without signer +export const getOrderManagerInstance = (chain: Chain): Contract => { + try { + return new ethers.Contract(parifiContracts[chain].OrderManager.address, parifiContracts[chain].OrderManager.abi); + } catch (error) { + throw error; + } +}; // Return the Profit or Loss for a position in USD // `normalizedMarketPrice` is the price of market with 8 decimals diff --git a/src/contract-logic/order-manager/settlement.ts b/src/contract-logic/order-manager/settlement.ts new file mode 100644 index 0000000..82157db --- /dev/null +++ b/src/contract-logic/order-manager/settlement.ts @@ -0,0 +1,99 @@ +import Decimal from 'decimal.js'; +import { BatchExecute, Order, getAllPendingOrders } from '../../subgraph'; +import { Chain, DECIMAL_ZERO, DEFAULT_BATCH_COUNT, PRECISION_MULTIPLIER } from '../../common'; +import { getVaaPriceUpdateData } from '../../pyth'; +import { AxiosInstance } from 'axios'; +import { getParifiUtilsInstance } from '../parifi-utils'; +import { executeTxUsingGelato } from '../../gelato'; +import { contracts as parifiContracts } from '@parifi/references'; + +// Returns true if the price of market is within the range configured in order struct +// The function can be used to check if a pending order can be settled or not +export const checkIfOrderCanBeSettled = (order: Order, normalizedMarketPrice: Decimal): boolean => { + const isLimitOrder = order.isLimitOrder; + const triggerAbove = order.triggerAbove; + const isLong = order.isLong; + // Return false if any of the fields is undefined + if (isLimitOrder === undefined || triggerAbove === undefined || isLong === undefined) { + return false; + } + + // Return false if any of the fields is undefined + if (order.expectedPrice === undefined || order.maxSlippage === undefined) { + return false; + } + const expectedPrice = new Decimal(order.expectedPrice); + const maxSlippage = new Decimal(order.maxSlippage); + + if (isLimitOrder) { + // If its a limit order, check if the limit price is reached, either above or below + // depending on the triggerAbove flag + if ( + (triggerAbove && normalizedMarketPrice < expectedPrice) || + (!triggerAbove && normalizedMarketPrice > expectedPrice) + ) { + return false; + } + } else { + // Market Orders + // Check if current market price is within slippage range + if (expectedPrice != DECIMAL_ZERO) { + const upperLimit = expectedPrice.mul(PRECISION_MULTIPLIER.add(maxSlippage)).div(PRECISION_MULTIPLIER); + const lowerLimit = expectedPrice.mul(PRECISION_MULTIPLIER.sub(maxSlippage)).div(PRECISION_MULTIPLIER); + + if ((isLong && normalizedMarketPrice > upperLimit) || (!isLong && normalizedMarketPrice < lowerLimit)) { + return false; + } + } + } + return true; +}; + +export const batchSettlePendingOrdersUsingGelato = async ( + chainId: Chain, + gelatoKey: string, + pythClient: AxiosInstance, +): Promise<{ ordersCount: number }> => { + const currentTimestamp = Math.floor(Date.now() / 1000); + const pendingOrders = await getAllPendingOrders(chainId, currentTimestamp, DEFAULT_BATCH_COUNT); + if (pendingOrders.length == 0) return { ordersCount: 0 }; + + const priceIds: string[] = []; + + // Populate the price ids array to fetch price update data + pendingOrders.forEach((order) => { + if (order.market?.pyth?.id) { + priceIds.push(order.market.pyth.id); + } + }); + + // Get Price update data from Pyth + const priceUpdateData = await getVaaPriceUpdateData(priceIds, pythClient); + + // Populate batched orders for settlement + const batchedOrders: BatchExecute[] = []; + pendingOrders.forEach((order) => { + if (order.id) { + batchedOrders.push({ + id: order.id, + priceUpdateData: priceUpdateData, + }); + console.log("Order ID available for settlement:", order.id) + } + }); + + // Encode transaction data + if (batchedOrders.length != 0) { + const parifiUtils = getParifiUtilsInstance(chainId); + const { data: encodedTxData } = await parifiUtils.batchSettleOrders.populateTransaction(batchedOrders); + + const taskId = await executeTxUsingGelato( + parifiContracts[chainId].ParifiUtils.address, + chainId, + gelatoKey, + encodedTxData, + ); + console.log("Task ID:", taskId) + } + return { ordersCount: batchedOrders.length }; +}; diff --git a/src/contract-logic/parifi-utils/index.ts b/src/contract-logic/parifi-utils/index.ts new file mode 100644 index 0000000..4b41937 --- /dev/null +++ b/src/contract-logic/parifi-utils/index.ts @@ -0,0 +1,12 @@ +import { Contract, ethers } from 'ethers'; +import { Chain } from '@parifi/references'; +import { contracts as parifiContracts } from '@parifi/references'; + +// Returns an Order Manager contract instance without signer +export const getParifiUtilsInstance = (chain: Chain): Contract => { + try { + return new ethers.Contract(parifiContracts[chain].ParifiUtils.address, parifiContracts[chain].ParifiUtils.abi); + } catch (error) { + throw error; + } +}; diff --git a/src/gelato/index.ts b/src/gelato/index.ts new file mode 100644 index 0000000..e79d8a7 --- /dev/null +++ b/src/gelato/index.ts @@ -0,0 +1,19 @@ +import { GelatoRelay, SponsoredCallRequest } from '@gelatonetwork/relay-sdk'; +import { Chain } from '@parifi/references'; + +export const executeTxUsingGelato = async ( + targetContractAddress: string, + chainId: Chain, + gelatoKey: string, + encodedTxData: string, +): Promise => { + const request: SponsoredCallRequest = { + chainId: BigInt(chainId.toString()), + target: targetContractAddress, + data: encodedTxData, + }; + + const relay = new GelatoRelay(); + const { taskId } = await relay.sponsoredCall(request, gelatoKey); + return taskId; +}; diff --git a/src/pyth/index.ts b/src/pyth/index.ts index 986d22c..5abefe7 100644 --- a/src/pyth/index.ts +++ b/src/pyth/index.ts @@ -77,3 +77,5 @@ export const normalizePythPriceForParifi = (pythPrice: number, pythExponent: num return new Decimal(pythPrice).div(adjustedFactor); } }; + +export const \ No newline at end of file diff --git a/src/subgraph/common/types.ts b/src/subgraph/common/types.ts index de33fa4..0cb7400 100644 --- a/src/subgraph/common/types.ts +++ b/src/subgraph/common/types.ts @@ -417,4 +417,9 @@ export interface PythData { // " Last updated timestamp " lastUpdatedTimestamp?: string +} + +export interface BatchExecute { + id: string; + priceUpdateData: string[]; } \ No newline at end of file diff --git a/test/contract-logic/parifi-utils.test.ts b/test/contract-logic/parifi-utils.test.ts new file mode 100644 index 0000000..3a881c0 --- /dev/null +++ b/test/contract-logic/parifi-utils.test.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { Chain } from '../../src'; +import { getPythClient } from '../../src/pyth'; +import { batchSettleOrdersUsingGelato } from '../../src/contract-logic/order-manager/settlement'; + +const chainId = Chain.ARBITRUM_SEPOLIA; + +describe('Parifi Utils tests', () => { + it('should settle orders in batch using Parifi Utils', async () => { + // To test the batch settle functionality, create some orders manually using the interface + const pythClient = await getPythClient(); + + if (pythClient) { + const orderCount = await batchSettleOrdersUsingGelato(chainId, process.env.GELATO_KEY ?? '', pythClient); + console.log('Orders processed: ', orderCount); + } + }); +}); + From 7a73f2e1eecc84ace6b319c33d3417a43708b475 Mon Sep 17 00:00:00 2001 From: Sudeep Biswas Date: Thu, 8 Feb 2024 00:49:52 +0530 Subject: [PATCH 2/2] Add price fetching logic from Pyth --- .../order-manager/settlement.ts | 37 ++++++++++++++----- src/pyth/index.ts | 35 ++++++++++++++++-- src/pyth/pythMapper.ts | 20 ++++++++++ src/subgraph/common/types.ts | 23 ++++++++++++ test/contract-logic/parifi-utils.test.ts | 5 +-- 5 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 src/pyth/pythMapper.ts diff --git a/src/contract-logic/order-manager/settlement.ts b/src/contract-logic/order-manager/settlement.ts index 82157db..5cf92b6 100644 --- a/src/contract-logic/order-manager/settlement.ts +++ b/src/contract-logic/order-manager/settlement.ts @@ -1,7 +1,7 @@ import Decimal from 'decimal.js'; import { BatchExecute, Order, getAllPendingOrders } from '../../subgraph'; import { Chain, DECIMAL_ZERO, DEFAULT_BATCH_COUNT, PRECISION_MULTIPLIER } from '../../common'; -import { getVaaPriceUpdateData } from '../../pyth'; +import { getLatestPricesFromPyth, getVaaPriceUpdateData, normalizePythPriceForParifi } from '../../pyth'; import { AxiosInstance } from 'axios'; import { getParifiUtilsInstance } from '../parifi-utils'; import { executeTxUsingGelato } from '../../gelato'; @@ -67,18 +67,36 @@ export const batchSettlePendingOrdersUsingGelato = async ( } }); - // Get Price update data from Pyth + // Get Price update data and latest prices from Pyth const priceUpdateData = await getVaaPriceUpdateData(priceIds, pythClient); + const pythLatestPrices = await getLatestPricesFromPyth(priceIds, pythClient); - // Populate batched orders for settlement + // Populate batched orders for settlement for orders that can be settled const batchedOrders: BatchExecute[] = []; + pendingOrders.forEach((order) => { if (order.id) { - batchedOrders.push({ - id: order.id, - priceUpdateData: priceUpdateData, - }); - console.log("Order ID available for settlement:", order.id) + // Pyth returns price id without '0x' at the start, hence the price id from order + // needs to be formatted + const orderPriceId = order.market?.pyth?.id ?? '0x'; + const formattedPriceId = orderPriceId.startsWith('0x') ? orderPriceId.substring(2) : orderPriceId; + + const assetPrice = pythLatestPrices.find((pythPrice) => pythPrice.id === formattedPriceId); + const normalizedMarketPrice = normalizePythPriceForParifi( + parseInt(assetPrice?.price.price ?? '0'), + assetPrice?.price.expo ?? 0, + ); + + if (checkIfOrderCanBeSettled(order, normalizedMarketPrice)) { + batchedOrders.push({ + id: order.id, + priceUpdateData: priceUpdateData, + }); + // We need these console logs for feedback to Tenderly actions and other scripts + console.log('Order ID available for settlement:', order.id); + } else { + console.log('Order ID not available for settlement because of price mismatch:', order.id); + } } }); @@ -93,7 +111,8 @@ export const batchSettlePendingOrdersUsingGelato = async ( gelatoKey, encodedTxData, ); - console.log("Task ID:", taskId) + // We need these console logs for feedback to Tenderly actions and other scripts + console.log('Task ID:', taskId); } return { ordersCount: batchedOrders.length }; }; diff --git a/src/pyth/index.ts b/src/pyth/index.ts index 5abefe7..ddc95fd 100644 --- a/src/pyth/index.ts +++ b/src/pyth/index.ts @@ -1,6 +1,8 @@ import axios, { AxiosInstance } from 'axios'; import { PRICE_FEED_DECIMALS, getUniqueValuesFromArray } from '../common'; import Decimal from 'decimal.js'; +import { PythPriceResponse } from '../subgraph'; +import { mapPythPriceResponseToInterface } from './pythMapper'; // Returns a Pyth client object based on the params provided export const getPythClient = async ( @@ -46,7 +48,7 @@ export const getPythClient = async ( // The function accepts an array of priceIds and returns the priceUpdateData // for them from Pyth -export async function getVaaPriceUpdateData(priceIds: string[], pythClient: AxiosInstance): Promise { +export const getVaaPriceUpdateData = async (priceIds: string[], pythClient: AxiosInstance): Promise => { const uniquePriceIds = getUniqueValuesFromArray(priceIds); let priceUpdateData: string[] = []; @@ -64,7 +66,7 @@ export async function getVaaPriceUpdateData(priceIds: string[], pythClient: Axio } } return priceUpdateData.map((vaa) => '0x' + Buffer.from(vaa, 'base64').toString('hex')); -} +}; // Pyth currently uses different exponents for supported assets. Parifi uses all price feeds with 8 decimals // This function converts the price from Pyth to a format Parifi uses with 8 decimals. @@ -78,4 +80,31 @@ export const normalizePythPriceForParifi = (pythPrice: number, pythExponent: num } }; -export const \ No newline at end of file +// Get latest prices from Pyth for priceIds +// The prices returned are in Pyth structure which needs to be normalized +// before using for any Parifi functions +export const getLatestPricesFromPyth = async ( + priceIds: string[], + pythClient: AxiosInstance, +): Promise => { + const uniquePriceIds = getUniqueValuesFromArray(priceIds); + + const pythPriceResponses: PythPriceResponse[] = []; + + if (pythClient) { + try { + const response = await pythClient.get('/api/latest_price_feeds', { + params: { + ids: uniquePriceIds, + verbose: false, + binary: false, + }, + }); + return mapPythPriceResponseToInterface(response.data); + } catch (error) { + console.log('Error fetching latest prices from Pyth', error); + throw error; + } + } + return pythPriceResponses; +}; diff --git a/src/pyth/pythMapper.ts b/src/pyth/pythMapper.ts new file mode 100644 index 0000000..e78af3a --- /dev/null +++ b/src/pyth/pythMapper.ts @@ -0,0 +1,20 @@ +import { PythPriceResponse } from '../subgraph'; + +// Function to map the pyth latest price response to interface +export const mapPythPriceResponseToInterface = (response: any[]): PythPriceResponse[] => { + return response.map((item) => ({ + id: item.id || '', + price: { + price: item.price.price || '0', + conf: item.price.conf || '0', + expo: item.price.expo || 0, + publish_time: item.price.publish_time || 0, + }, + ema_price: { + price: item.ema_price.price || '0', + conf: item.ema_price.conf || '0', + expo: item.ema_price.expo || 0, + publish_time: item.ema_price.publish_time || 0, + }, + })); +}; diff --git a/src/subgraph/common/types.ts b/src/subgraph/common/types.ts index 0cb7400..a5cf7e2 100644 --- a/src/subgraph/common/types.ts +++ b/src/subgraph/common/types.ts @@ -385,6 +385,29 @@ export interface Token { //////////////////////////////////////////////////////////////// //////////////////////// PYTH //////////////////////////// //////////////////////////////////////////////////////////////// + + +// Pyth price data interface for prices received from Pyth +export interface PythPrice { + price: string + + conf: string + + expo: number + + publish_time: number +} + +// Interface for response received for Pyth Price data +export interface PythPriceResponse { + // Pyth Price ID + id? :string + + price: PythPrice + + ema_price: PythPrice +} + export interface PriceFeedSnapshot { //" Price ID + Timestamp " id?: string diff --git a/test/contract-logic/parifi-utils.test.ts b/test/contract-logic/parifi-utils.test.ts index 3a881c0..4024b74 100644 --- a/test/contract-logic/parifi-utils.test.ts +++ b/test/contract-logic/parifi-utils.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config'; import { Chain } from '../../src'; import { getPythClient } from '../../src/pyth'; -import { batchSettleOrdersUsingGelato } from '../../src/contract-logic/order-manager/settlement'; +import { batchSettlePendingOrdersUsingGelato } from '../../src/contract-logic/order-manager/settlement'; const chainId = Chain.ARBITRUM_SEPOLIA; @@ -11,9 +11,8 @@ describe('Parifi Utils tests', () => { const pythClient = await getPythClient(); if (pythClient) { - const orderCount = await batchSettleOrdersUsingGelato(chainId, process.env.GELATO_KEY ?? '', pythClient); + const orderCount = await batchSettlePendingOrdersUsingGelato(chainId, process.env.GELATO_KEY ?? '', pythClient); console.log('Orders processed: ', orderCount); } }); }); -