diff --git a/config.example.yaml b/config.example.yaml index c94b2ea..d8a7569 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -33,13 +33,26 @@ global: 0.05 # Extra buffer allowed on top of the calculated required funds for # performing an underwrite. - maxUnderwriteAllowed: '10000000000000000000' # Maximum tokens to risk on a single underwrite - minUnderwriteReward: '10000000000' # Minimum acceptable token reward for an underwrite + maxUnderwriteAllowed: 500 # Maximum underwrite value (EXCLUDING THE COLLATERAL). + # In the 'pricingDenomination' specified on the Relayer. + minUnderwriteReward: 0.5 # Minimum underwrite reward desired. + # In the 'pricingDenomination' specified on the Relayer. + relativeMinUnderwriteReward: 0.1 # Minimum relative underwrite reward desired (w.r.t. the + # underwrite amount plus the tx cost). + profitabilityFactor: 1.0 # Profitiability evaluation adjustment factor. A larger + # factor implies a larger profitability guarantee. + lowTokenBalanceWarning: '1000000000000000000' # Token balance below which a 'low balance' warning is emitted. tokenBalanceUpdateInterval: 50 # Number of transactions after which to update the # Underwriter token balance from the rpc. + relayDeliveryCosts: # Average swap delivery costs for underwritten swaps. Used to estimate underwrites profitability + gasUsage: '21000' # The gas used. + # gasObserved: '21000' # The gas observed by the escrow contract. + # fee: '0' # The fee taken on transaction submission (relevant for L2 chains.) + # value: '0' # The value sent to the escrow as payment (e.g. to execute the return 'ack' cross-chain message). + # ! The following setting is here for illustrative purposes, but it MUST ALWAYS be applied on # ! a per-chain basis as it is CRITICAL to prevent the underwriter from being stolen from. # minMaxGasDelivery: 200000 @@ -105,6 +118,7 @@ chains: # The tokens that are to be undewritten tokens: - name: 'Wrapped gas' + tokenId: "ethereum" address: '0xE67ABDA0D43f7AC8f37876bBF00D1DFadbB93aaa' allowanceBuffer: '10000000000000000000' # Amount by which to 'buffer' the token approvals @@ -138,6 +152,7 @@ chains: minMaxGasDelivery: 200000 tokens: - name: 'WETH' + tokenId: "ethereum" address: '0x1BDD24840e119DC2602dCC587Dd182812427A5Cc' allowanceBuffer: '10000000000000000000' @@ -158,6 +173,7 @@ chains: minMaxGasDelivery: 200000 tokens: - name: 'WETH' + tokenId: "ethereum" address: '0x4200000000000000000000000000000000000006' allowanceBuffer: '10000000000000000000' @@ -177,6 +193,12 @@ endpoints: address: '0x0000000003b8C9BFeB9351933CFC301Eea92073F' - name: 'Amplified' address: '0x000000004aBe0D620b25b8B06B0712BDcff21899' + # Costs overrides (uncomment to enable) + # relayDeliveryCosts: + # gasUsage: '21000' + # gasObserved: '21000' + # fee: '0' + # value: '0' - name: 'Optimism Sepolia Testnet' amb: 'wormhole' diff --git a/package.json b/package.json index 41ff561..25fbbc1 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "pino": "^8.16.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", + "viem": "^2.16.2", "ws": "^8.16.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47e7809..62f2f12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,10 +13,10 @@ importers: version: 10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1) '@nestjs/core': specifier: ^10.0.0 - version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1) + version: 10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1) '@nestjs/platform-express': specifier: ^10.0.0 - version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3) + version: 10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.3.3) '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -50,6 +50,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.1 + viem: + specifier: ^2.16.2 + version: 2.17.1(typescript@5.4.2) ws: specifier: ^8.16.0 version: 8.16.0 @@ -59,13 +62,13 @@ importers: version: 10.3.2 '@nestjs/schematics': specifier: ^10.0.0 - version: 10.1.1(typescript@5.4.2) + version: 10.1.1(chokidar@3.6.0)(typescript@5.4.2) '@nestjs/testing': specifier: ^10.0.0 - version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/platform-express@10.3.3) + version: 10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.3.3)) '@typechain/ethers-v6': specifier: ^0.5.1 - version: 0.5.1(ethers@6.11.1)(typechain@8.3.2)(typescript@5.4.2) + version: 0.5.1(ethers@6.11.1)(typechain@8.3.2(typescript@5.4.2))(typescript@5.4.2) '@types/express': specifier: ^4.17.17 version: 4.17.21 @@ -83,7 +86,7 @@ importers: version: 8.5.10 '@typescript-eslint/eslint-plugin': specifier: ^6.10.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': specifier: ^6.10.0 version: 6.21.0(eslint@8.57.0)(typescript@5.4.2) @@ -95,10 +98,10 @@ importers: version: 9.1.0(eslint@8.57.0) eslint-plugin-prettier: specifier: ^5.0.1 - version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) + version: 5.1.3(@types/eslint@8.56.5)(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2) + version: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)) prettier: specifier: ^3.0.3 version: 3.2.5 @@ -110,10 +113,10 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.1 - version: 29.1.2(@babel/core@7.24.0)(jest@29.7.0)(typescript@5.4.2) + version: 29.1.2(@babel/core@7.24.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.0))(jest@29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)))(typescript@5.4.2) ts-loader: specifier: ^9.4.3 - version: 9.5.1(typescript@5.4.2)(webpack@5.90.3) + version: 9.5.1(typescript@5.4.2)(webpack@5.90.1) ts-node: specifier: ^10.9.1 version: 10.9.2(@types/node@20.11.25)(typescript@5.4.2) @@ -133,6 +136,9 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} + '@adraffy/ens-normalize@1.10.0': + resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} + '@adraffy/ens-normalize@1.10.1': resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} @@ -545,10 +551,17 @@ packages: '@noble/curves@1.2.0': resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + '@noble/curves@1.4.0': + resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} + '@noble/hashes@1.3.2': resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} engines: {node: '>= 16'} + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -574,6 +587,15 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@scure/base@1.1.7': + resolution: {integrity: sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==} + + '@scure/bip32@1.4.0': + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + + '@scure/bip39@1.3.0': + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -828,6 +850,17 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + abitype@1.0.5: + resolution: {integrity: sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -1811,6 +1844,11 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isows@1.0.4: + resolution: {integrity: sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -2973,6 +3011,14 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + viem@2.17.1: + resolution: {integrity: sha512-iLwFAfn7aWfvc1KY176YNTJQpPdepRhvaltae6TomZ+DU5M7LdASP2ywdAHw/rezdEmrH/ytwG2WWnjWioE0fA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -3004,16 +3050,6 @@ packages: webpack-cli: optional: true - webpack@5.90.3: - resolution: {integrity: sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -3057,6 +3093,18 @@ packages: utf-8-validate: optional: true + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + 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 + ws@8.5.0: resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==} engines: {node: '>=10.0.0'} @@ -3103,6 +3151,8 @@ snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} + '@adraffy/ens-normalize@1.10.0': {} + '@adraffy/ens-normalize@1.10.1': {} '@ampproject/remapping@2.3.0': @@ -3114,11 +3164,12 @@ snapshots: dependencies: ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) - chokidar: 3.6.0 jsonc-parser: 3.2.0 picomatch: 3.0.1 rxjs: 7.8.1 source-map: 0.7.4 + optionalDependencies: + chokidar: 3.6.0 '@angular-devkit/schematics-cli@17.1.2(chokidar@3.6.0)': dependencies: @@ -3412,7 +3463,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2)': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -3426,7 +3477,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3635,10 +3686,9 @@ snapshots: tslib: 2.6.2 uid: 2.0.2 - '@nestjs/core@10.3.3(@nestjs/common@10.3.3)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1)': + '@nestjs/core@10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1) - '@nestjs/platform-express': 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -3647,13 +3697,15 @@ snapshots: rxjs: 7.8.1 tslib: 2.6.2 uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.3.3) transitivePeerDependencies: - encoding - '@nestjs/platform-express@10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)': + '@nestjs/platform-express@10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.3.3)': dependencies: '@nestjs/common': 10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1) - '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1) body-parser: 1.20.2 cors: 2.8.5 express: 4.18.2 @@ -3673,7 +3725,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/schematics@10.1.1(typescript@5.4.2)': + '@nestjs/schematics@10.1.1(chokidar@3.6.0)(typescript@5.4.2)': dependencies: '@angular-devkit/core': 17.1.2(chokidar@3.6.0) '@angular-devkit/schematics': 17.1.2(chokidar@3.6.0) @@ -3684,19 +3736,26 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/testing@10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/platform-express@10.3.3)': + '@nestjs/testing@10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.3.3))': dependencies: '@nestjs/common': 10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1) - '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1) - '@nestjs/platform-express': 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1) tslib: 2.6.2 + optionalDependencies: + '@nestjs/platform-express': 10.3.3(@nestjs/common@10.3.3(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.3.3) '@noble/curves@1.2.0': dependencies: '@noble/hashes': 1.3.2 + '@noble/curves@1.4.0': + dependencies: + '@noble/hashes': 1.4.0 + '@noble/hashes@1.3.2': {} + '@noble/hashes@1.4.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3722,6 +3781,19 @@ snapshots: '@pkgr/core@0.1.1': {} + '@scure/base@1.1.7': {} + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 + + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -3740,7 +3812,7 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@typechain/ethers-v6@0.5.1(ethers@6.11.1)(typechain@8.3.2)(typescript@5.4.2)': + '@typechain/ethers-v6@0.5.1(ethers@6.11.1)(typechain@8.3.2(typescript@5.4.2))(typescript@5.4.2)': dependencies: ethers: 6.11.1 lodash: 4.17.21 @@ -3889,7 +3961,7 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.2) @@ -3904,6 +3976,7 @@ snapshots: natural-compare: 1.4.0 semver: 7.6.0 ts-api-utils: 1.2.1(typescript@5.4.2) + optionalDependencies: typescript: 5.4.2 transitivePeerDependencies: - supports-color @@ -3916,6 +3989,7 @@ snapshots: '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4 eslint: 8.57.0 + optionalDependencies: typescript: 5.4.2 transitivePeerDependencies: - supports-color @@ -3932,6 +4006,7 @@ snapshots: debug: 4.3.4 eslint: 8.57.0 ts-api-utils: 1.2.1(typescript@5.4.2) + optionalDependencies: typescript: 5.4.2 transitivePeerDependencies: - supports-color @@ -3948,6 +4023,7 @@ snapshots: minimatch: 9.0.3 semver: 7.6.0 ts-api-utils: 1.2.1(typescript@5.4.2) + optionalDependencies: typescript: 5.4.2 transitivePeerDependencies: - supports-color @@ -4053,6 +4129,10 @@ snapshots: '@xtuc/long@4.2.2': {} + abitype@1.0.5(typescript@5.4.2): + optionalDependencies: + typescript: 5.4.2 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -4077,7 +4157,7 @@ snapshots: aes-js@4.0.0-beta.5: {} ajv-formats@2.1.1(ajv@8.12.0): - dependencies: + optionalDependencies: ajv: 8.12.0 ajv-keywords@3.5.2(ajv@6.12.6): @@ -4458,15 +4538,16 @@ snapshots: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 + optionalDependencies: typescript: 5.3.3 - create-jest@29.7.0(@types/node@20.11.25)(ts-node@10.9.2): + create-jest@29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -4585,13 +4666,15 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5): + eslint-plugin-prettier@5.1.3(@types/eslint@8.56.5)(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5): dependencies: eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) prettier: 3.2.5 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 + optionalDependencies: + '@types/eslint': 8.56.5 + eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-scope@5.1.1: dependencies: @@ -5123,6 +5206,10 @@ snapshots: isexe@2.0.0: {} + isows@1.0.4(ws@8.17.1): + dependencies: + ws: 8.17.1 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -5204,16 +5291,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.11.25)(ts-node@10.9.2): + jest-cli@29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2) + create-jest: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -5223,12 +5310,11 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.11.25)(ts-node@10.9.2): + jest-config@29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)): dependencies: '@babel/core': 7.24.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.25 babel-jest: 29.7.0(@babel/core@7.24.0) chalk: 4.1.2 ci-info: 3.9.0 @@ -5248,6 +5334,8 @@ snapshots: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.11.25 ts-node: 10.9.2(@types/node@20.11.25)(typescript@5.4.2) transitivePeerDependencies: - babel-plugin-macros @@ -5330,7 +5418,7 @@ snapshots: jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - dependencies: + optionalDependencies: jest-resolve: 29.7.0 jest-regex-util@29.6.3: {} @@ -5474,12 +5562,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.11.25)(ts-node@10.9.2): + jest@29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2) + jest-cli: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -6208,15 +6296,6 @@ snapshots: terser: 5.29.1 webpack: 5.90.1 - terser-webpack-plugin@5.3.10(webpack@5.90.3): - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - jest-worker: 27.5.1 - schema-utils: 3.3.0 - serialize-javascript: 6.0.2 - terser: 5.29.1 - webpack: 5.90.3 - terser@5.29.1: dependencies: '@jridgewell/source-map': 0.3.5 @@ -6271,12 +6350,11 @@ snapshots: dependencies: typescript: 5.4.2 - ts-jest@29.1.2(@babel/core@7.24.0)(jest@29.7.0)(typescript@5.4.2): + ts-jest@29.1.2(@babel/core@7.24.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.0))(jest@29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)))(typescript@5.4.2): dependencies: - '@babel/core': 7.24.0 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2) + jest: 29.7.0(@types/node@20.11.25)(ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -6284,8 +6362,12 @@ snapshots: semver: 7.6.0 typescript: 5.4.2 yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.24.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.0) - ts-loader@9.5.1(typescript@5.4.2)(webpack@5.90.3): + ts-loader@9.5.1(typescript@5.4.2)(webpack@5.90.1): dependencies: chalk: 4.1.2 enhanced-resolve: 5.15.1 @@ -6293,7 +6375,7 @@ snapshots: semver: 7.6.0 source-map: 0.7.4 typescript: 5.4.2 - webpack: 5.90.3 + webpack: 5.90.1 ts-node@10.9.2(@types/node@20.11.25)(typescript@5.4.2): dependencies: @@ -6406,6 +6488,23 @@ snapshots: vary@1.1.2: {} + viem@2.17.1(typescript@5.4.2): + dependencies: + '@adraffy/ens-normalize': 1.10.0 + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + abitype: 1.0.5(typescript@5.4.2) + isows: 1.0.4(ws@8.17.1) + ws: 8.17.1 + optionalDependencies: + typescript: 5.4.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -6456,37 +6555,6 @@ snapshots: - esbuild - uglify-js - webpack@5.90.3: - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.5 - '@webassemblyjs/ast': 1.11.6 - '@webassemblyjs/wasm-edit': 1.11.6 - '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.11.3 - acorn-import-assertions: 1.9.0(acorn@8.11.3) - browserslist: 4.23.0 - chrome-trace-event: 1.0.3 - enhanced-resolve: 5.15.1 - es-module-lexer: 1.4.1 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(webpack@5.90.3) - watchpack: 2.4.0 - webpack-sources: 3.2.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -6528,6 +6596,8 @@ snapshots: ws@8.16.0: {} + ws@8.17.1: {} + ws@8.5.0: {} xtend@4.0.2: {} diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index f5688c5..db29249 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -125,6 +125,19 @@ const LISTENER_SCHEMA = { additionalProperties: false } +const RELAY_DELIVERY_COSTS_SCHEMA = { + $id: "relay-delivery-costs-schema", + type: "object", + properties: { + gasUsage: { $ref: "gas-field-schema" }, + gasObserved: { $ref: "gas-field-schema" }, + fee: { $ref: "uint256-field-schema" }, + value: { $ref: "uint256-field-schema" }, + }, + required: ["gasUsage"], + additionalProperties: false +} + const UNDERWRITER_GLOBAL_SCHEMA = { $id: "underwriter-global-schema", type: "object", @@ -150,10 +163,13 @@ const UNDERWRITER_GLOBAL_SCHEMA = { exclusiveMinimum: 0, maximum: 0.3 }, - maxUnderwriteAllowed: { $ref: "uint256-field-schema" }, - minUnderwriteReward: { $ref: "uint256-field-schema" }, + maxUnderwriteAllowed: { $ref: "positive-number-schema" }, + minUnderwriteReward: { $ref: "positive-number-schema" }, + relativeMinUnderwriteReward: { $ref: "positive-number-schema" }, + profitabilityFactor: { $ref: "positive-number-schema" }, lowTokenBalanceWarning: { $ref: "uint256-field-schema" }, tokenBalanceUpdateInterval: { $ref: "positive-number-schema" }, + relayDeliveryCosts: { $ref: "relay-delivery-costs-schema" }, }, additionalProperties: false } @@ -247,14 +263,17 @@ const TOKENS_SCHEMA = { type: "object", properties: { name: { $ref: "non-empty-string-schema" }, + tokenId: { $ref: "non-empty-string-schema" }, address: { $ref: "address-field-schema" }, - maxUnderwriteAllowed: { $ref: "uint256-field-schema" }, - minUnderwriteReward: { $ref: "uint256-field-schema" }, + maxUnderwriteAllowed: { $ref: "positive-number-schema" }, + minUnderwriteReward: { $ref: "positive-number-schema" }, + relativeMinUnderwriteReward: { $ref: "positive-number-schema" }, + profitabilityFactor: { $ref: "positive-number-schema" }, lowTokenBalanceWarning: { $ref: "uint256-field-schema" }, tokenBalanceUpdateInterval: { $ref: "positive-number-schema" }, allowanceBuffer: { $ref: "gas-field-schema" } }, - required: ["name", "address"], + required: ["name", "address", "tokenId"], additionalProperties: false }, minItems: 1 @@ -272,7 +291,6 @@ const CHAINS_SCHEMA = { resolver: { $ref: "non-empty-string-schema" }, tokens: { $ref: "tokens-schema" }, - blockDelay: { $ref: "positive-number-schema" }, monitor: { $ref: "monitor-schema" }, listener: { $ref: "listener-schema" }, underwriter: { $ref: "underwriter-schema" }, @@ -316,7 +334,8 @@ const ENDPOINTS_SCHEMA = { additionalProperties: false }, minItems: 1 - } + }, + relayDeliveryCosts: { $ref: "relay-delivery-costs-schema" }, }, required: ["name", "amb", "chainId", "factoryAddress", "interfaceAddress", "incentivesAddress", "channelsOnDestination", "vaultTemplates"], additionalProperties: false @@ -340,6 +359,7 @@ export function getConfigValidator(): AnyValidateFunction { ajv.addSchema(GLOBAL_SCHEMA); ajv.addSchema(MONITOR_SCHEMA); ajv.addSchema(LISTENER_SCHEMA); + ajv.addSchema(RELAY_DELIVERY_COSTS_SCHEMA); ajv.addSchema(UNDERWRITER_GLOBAL_SCHEMA); ajv.addSchema(UNDERWRITER_SCHEMA); ajv.addSchema(EXPIRER_SCHEMA); diff --git a/src/config/config.service.ts b/src/config/config.service.ts index 99dd289..c209a57 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -3,7 +3,7 @@ import { readFileSync } from 'fs'; import * as yaml from 'js-yaml'; import dotenv from 'dotenv'; import { getConfigValidator } from './config.schema'; -import { GlobalConfig, ChainConfig, AMBConfig, MonitorGlobalConfig, ListenerGlobalConfig, UnderwriterGlobalConfig, ExpirerGlobalConfig, WalletGlobalConfig, MonitorConfig, ListenerConfig, UnderwriterConfig, WalletConfig, ExpirerConfig, TokensConfig, EndpointConfig, VaultTemplateConfig } from './config.types'; +import { GlobalConfig, ChainConfig, AMBConfig, MonitorGlobalConfig, ListenerGlobalConfig, UnderwriterGlobalConfig, ExpirerGlobalConfig, WalletGlobalConfig, MonitorConfig, ListenerConfig, UnderwriterConfig, WalletConfig, ExpirerConfig, TokensConfig, EndpointConfig, VaultTemplateConfig, RelayDeliveryCosts } from './config.types'; @Injectable() @@ -93,7 +93,6 @@ export class ConfigService { port: parseInt(process.env['UNDERWRITER_PORT']), privateKey: rawGlobalConfig.privateKey, logLevel: rawGlobalConfig.logLevel, - blockDelay: rawGlobalConfig.blockDelay, monitor: this.formatMonitorGlobalConfig(rawGlobalConfig.monitor), listener: this.formatListenerGlobalConfig(rawGlobalConfig.listener), underwriter: this.formatUnderwriterGlobalConfig(rawGlobalConfig.underwriter), @@ -112,7 +111,6 @@ export class ConfigService { name: rawChainConfig.name, rpc: rawChainConfig.rpc, resolver: rawChainConfig.resolver ?? null, - blockDelay: rawChainConfig.blockDelay, tokens: this.formatTokensConfig(rawChainConfig.tokens), monitor: this.formatMonitorConfig(rawChainConfig.monitor), listener: this.formatListenerConfig(rawChainConfig.listener), @@ -189,6 +187,22 @@ export class ConfigService { }); } + let relayDeliveryCosts: RelayDeliveryCosts | undefined; + if (rawEndpointConfig.relayDeliveryCosts != undefined) { + relayDeliveryCosts = { + gasUsage: BigInt(rawEndpointConfig.relayDeliveryCosts.gasUsage), + gasObserved: rawEndpointConfig.relayDeliveryCosts.gasObserved != undefined + ? BigInt(rawEndpointConfig.relayDeliveryCosts.gasObserved) + : undefined, + fee: rawEndpointConfig.relayDeliveryCosts.fee != undefined + ? BigInt(rawEndpointConfig.relayDeliveryCosts.fee) + : undefined, + value: rawEndpointConfig.relayDeliveryCosts.value != undefined + ? BigInt(rawEndpointConfig.relayDeliveryCosts.value) + : undefined + }; + } + const currentEndpoints = endpointConfig.get(chainId) ?? []; @@ -210,6 +224,7 @@ export class ConfigService { incentivesAddress, channelsOnDestination, vaultTemplates, + relayDeliveryCosts, }); endpointConfig.set(chainId, currentEndpoints); } @@ -248,15 +263,22 @@ export class ConfigService { if (config.minRelayDeadlineDuration != undefined) { config.minRelayDeadlineDuration = BigInt(config.minRelayDeadlineDuration); } - if (config.maxUnderwriteAllowed != undefined) { - config.maxUnderwriteAllowed = BigInt(config.maxUnderwriteAllowed); - } - if (config.minUnderwriteReward != undefined) { - config.minUnderwriteReward = BigInt(config.minUnderwriteReward); - } if (config.lowTokenBalanceWarning != undefined) { config.lowTokenBalanceWarning = BigInt(config.lowTokenBalanceWarning); } + if (config.relayDeliveryCosts != undefined) { + const costs = config.relayDeliveryCosts; + costs.gasUsage = BigInt(costs.gasUsage); + if (costs.gasObserved != undefined) { + costs.gasObserved = BigInt(costs.gasObserved); + } + if (costs.fee != undefined) { + costs.fee = BigInt(costs.fee); + } + if (costs.value != undefined) { + costs.value = BigInt(costs.value); + } + } return config as UnderwriterGlobalConfig; } @@ -312,12 +334,6 @@ export class ConfigService { if (tokenConfig.allowanceBuffer != undefined) { tokenConfig.allowanceBuffer = BigInt(tokenConfig.allowanceBuffer); } - if (tokenConfig.maxUnderwriteAllowed != undefined) { - tokenConfig.maxUnderwriteAllowed = BigInt(tokenConfig.maxUnderwriteAllowed); - } - if (tokenConfig.minUnderwriteReward != undefined) { - tokenConfig.minUnderwriteReward = BigInt(tokenConfig.minUnderwriteReward); - } if (tokenConfig.lowTokenBalanceWarning != undefined) { tokenConfig.lowTokenBalanceWarning = BigInt(tokenConfig.lowTokenBalanceWarning); } diff --git a/src/config/config.types.ts b/src/config/config.types.ts index d573ec1..b6759d6 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -3,7 +3,6 @@ export interface GlobalConfig { port: number; privateKey: string; logLevel?: string; - blockDelay?: number; monitor: MonitorGlobalConfig; listener: ListenerGlobalConfig; underwriter: UnderwriterGlobalConfig; @@ -31,6 +30,14 @@ export interface ListenerGlobalConfig { export interface ListenerConfig extends ListenerGlobalConfig {} +export interface RelayDeliveryCosts { + gasUsage: bigint; + gasObserved?: bigint; + fee?: bigint; + value?: bigint; +} + + export interface UnderwriterGlobalConfig { enabled?: boolean; retryInterval?: number; @@ -43,10 +50,13 @@ export interface UnderwriterGlobalConfig { maxSubmissionDelay?: number; underwritingCollateral?: number; allowanceBuffer?: number; - maxUnderwriteAllowed?: bigint; - minUnderwriteReward?: bigint; + maxUnderwriteAllowed?: number; + minUnderwriteReward?: number; + relativeMinUnderwriteReward?: number; + profitabilityFactor?: number; lowTokenBalanceWarning?: bigint; tokenBalanceUpdateInterval?: number; + relayDeliveryCosts?: RelayDeliveryCosts; } export interface UnderwriterConfig extends UnderwriterGlobalConfig { @@ -101,7 +111,6 @@ export interface ChainConfig { name: string; rpc: string; resolver: string | null; - blockDelay?: number; tokens: TokensConfig, monitor: MonitorConfig; listener: ListenerConfig; @@ -112,9 +121,12 @@ export interface ChainConfig { export interface TokenConfig { + tokenId: string; allowanceBuffer?: bigint; - maxUnderwriteAllowed?: bigint; - minUnderwriteReward?: bigint; + maxUnderwriteAllowed?: number; + minUnderwriteReward?: number; + relativeMinUnderwriteReward?: number; + profitabilityFactor?: number; lowTokenBalanceWarning?: bigint; tokenBalanceUpdateInterval?: number; } @@ -131,6 +143,7 @@ export interface EndpointConfig { incentivesAddress: string; channelsOnDestination: Record; vaultTemplates: VaultTemplateConfig[]; + relayDeliveryCosts?: RelayDeliveryCosts; } export interface VaultTemplateConfig { diff --git a/src/resolvers/arbitrum.ts b/src/resolvers/arbitrum.ts index d115a13..36f2f0c 100644 --- a/src/resolvers/arbitrum.ts +++ b/src/resolvers/arbitrum.ts @@ -1,13 +1,22 @@ -import { JsonRpcProvider } from "ethers"; -import { Resolver, ResolverConfig } from "./resolver"; +import { Interface, JsonRpcProvider, TransactionRequest } from "ethers"; +import { GasEstimateComponents, Resolver, ResolverConfig } from "./resolver"; import pino from "pino"; import { tryErrorToString, wait } from "src/common/utils"; export const RESOLVER_TYPE_ARBITRUM = 'arbitrum'; +// Arbitrum NodeInterface docs: +// https://docs.arbitrum.io/build-decentralized-apps/nodeinterface/reference +const ARBITRUM_NODE_INTERFACE_ADDRESS = '0x00000000000000000000000000000000000000c8'; +const GAS_ESTIMATE_COMPONENTS_SIGNATURE = 'function gasEstimateComponents(address to, bool contractCreation, bytes calldata data) returns (uint64, uint64, uint256, uint256)'; + export class ArbitrumResolver extends Resolver { override readonly resolverType; + private arbitrumNodeInterface = new Interface([ + GAS_ESTIMATE_COMPONENTS_SIGNATURE + ]); + constructor( config: ResolverConfig, provider: JsonRpcProvider, @@ -22,6 +31,7 @@ export class ArbitrumResolver extends Resolver { this.resolverType = RESOLVER_TYPE_ARBITRUM; } + //TODO implement block number caching? override async getTransactionBlockNumber( observedBlockNumber: number ): Promise { @@ -64,6 +74,50 @@ export class ArbitrumResolver extends Resolver { throw new Error(`Failed to map an 'observedBlockNumber' to an 'l1BlockNumber'. Max tries reached.`); }; + override async estimateGas( + transactionRequest: TransactionRequest + ): Promise { + + // This function relies on Arbitrum's 'gasEstimateComponents' to estimate the gas components. + // https://github.com/OffchainLabs/nitro-contracts/blob/1cab72ff3dfcfe06ceed371a9db7a54a527e3bfb/src/node-interface/NodeInterface.sol#L84 + + // Set the requested transaction's 'to' and 'data' fields within the + // 'gasEstimateComponents' function arguments, as these fields will be replaced when + // calling the `gasEstimateComponents` function. + const transactionData = this.arbitrumNodeInterface.encodeFunctionData( + "gasEstimateComponents", + [ + transactionRequest.to, + false, // 'contractCreation' + transactionRequest.data ?? "0x" + ] + ); + + const result = await this.provider.call({ + ...transactionRequest, + to: ARBITRUM_NODE_INTERFACE_ADDRESS, // Replace the 'to' address with Arbitrum's NodeInterface address. + data: transactionData // Replace the tx data with the encoded function arguments above. + }); + + const decodedResult = this.arbitrumNodeInterface.decodeFunctionResult( + "gasEstimateComponents", + result + ); + + const gasEstimate: bigint = decodedResult[0]; + const l1GasEstimate: bigint = decodedResult[1]; + + if (l1GasEstimate > gasEstimate) { + throw new Error(`Error on 'gasEstimateComponents' call (Arbitrum): returned 'l1GasEstimate' is larger than 'gasEstimate'.`); + } + + return { + gasEstimate, + observedGasEstimate: gasEstimate - l1GasEstimate, + additionalFeeEstimate: 0n, + } + + } } export default ArbitrumResolver; diff --git a/src/resolvers/base-sepolia.ts b/src/resolvers/base-sepolia.ts new file mode 100644 index 0000000..abcafa6 --- /dev/null +++ b/src/resolvers/base-sepolia.ts @@ -0,0 +1,24 @@ +import pino from "pino"; +import { JsonRpcProvider } from "ethers"; +import { ResolverConfig } from "./resolver"; +import OPStackResolver from "./op-stack"; + +export const BASE_SEPOLIA_CHAIN_NAME = 'baseSepolia'; + +export class BaseSepoliaResolver extends OPStackResolver { + + constructor( + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + BASE_SEPOLIA_CHAIN_NAME, + config, + provider, + logger, + ); + } +} + +export default BaseSepoliaResolver; diff --git a/src/resolvers/base.ts b/src/resolvers/base.ts new file mode 100644 index 0000000..b856f6c --- /dev/null +++ b/src/resolvers/base.ts @@ -0,0 +1,24 @@ +import pino from "pino"; +import { JsonRpcProvider } from "ethers"; +import { ResolverConfig } from "./resolver"; +import OPStackResolver from "./op-stack"; + +export const BASE_CHAIN_NAME = 'base'; + +export class BaseResolver extends OPStackResolver { + + constructor( + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + BASE_CHAIN_NAME, + config, + provider, + logger, + ); + } +} + +export default BaseResolver; diff --git a/src/resolvers/op-stack.ts b/src/resolvers/op-stack.ts new file mode 100644 index 0000000..fa59446 --- /dev/null +++ b/src/resolvers/op-stack.ts @@ -0,0 +1,54 @@ +import { JsonRpcProvider, TransactionRequest } from "ethers"; +import { Resolver, ResolverConfig } from "./resolver"; +import pino from "pino"; +import { createPublicClient, http } from "viem"; +import { publicActionsL2 } from 'viem/op-stack' + +export const RESOLVER_TYPE_OP_STACK = 'op-stack'; + +export class OPStackResolver extends Resolver { + override readonly resolverType; + + private client: any; //TODO use VIEM types + + constructor( + chainName: string, + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + config, + provider, + logger, + ); + + this.resolverType = RESOLVER_TYPE_OP_STACK; + + this.client = this.loadClient( + chainName, + this.provider._getConnection().url //TODO the 'rpc' url should be added to the ResolverConfig + ); + } + + private loadClient( + chainName: string, + rpc: string, + ): any { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const chain = require(`viem/chains`)[chainName]; + + return createPublicClient({ + chain, + transport: http(rpc), + }).extend(publicActionsL2()); + } + + override async estimateAdditionalFee( + transactionRequest: TransactionRequest + ): Promise { + return this.client.estimateL1Fee(transactionRequest) + } +} + +export default OPStackResolver; diff --git a/src/resolvers/optimism-sepolia.ts b/src/resolvers/optimism-sepolia.ts new file mode 100644 index 0000000..4752cbc --- /dev/null +++ b/src/resolvers/optimism-sepolia.ts @@ -0,0 +1,24 @@ +import pino from "pino"; +import { JsonRpcProvider } from "ethers"; +import { ResolverConfig } from "./resolver"; +import OPStackResolver from "./op-stack"; + +export const OPTIMISM_SEPOLIA_CHAIN_NAME = 'optimismSepolia'; + +export class OptimismSepoliaResolver extends OPStackResolver { + + constructor( + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + OPTIMISM_SEPOLIA_CHAIN_NAME, + config, + provider, + logger, + ); + } +} + +export default OptimismSepoliaResolver; diff --git a/src/resolvers/optimism.ts b/src/resolvers/optimism.ts new file mode 100644 index 0000000..7a1f76c --- /dev/null +++ b/src/resolvers/optimism.ts @@ -0,0 +1,24 @@ +import pino from "pino"; +import { JsonRpcProvider } from "ethers"; +import { ResolverConfig } from "./resolver"; +import OPStackResolver from "./op-stack"; + +export const OPTIMISM_CHAIN_NAME = 'optimism'; + +export class OptimismResolver extends OPStackResolver { + + constructor( + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + OPTIMISM_CHAIN_NAME, + config, + provider, + logger, + ); + } +} + +export default OptimismResolver; diff --git a/src/resolvers/resolver.ts b/src/resolvers/resolver.ts index e1d50a4..14df765 100644 --- a/src/resolvers/resolver.ts +++ b/src/resolvers/resolver.ts @@ -1,4 +1,4 @@ -import { JsonRpcProvider } from "ethers"; +import { JsonRpcProvider, TransactionRequest } from "ethers"; import pino from "pino"; export const RESOLVER_TYPE_DEFAULT = 'default'; @@ -62,6 +62,12 @@ export async function loadResolverAsync( ); } +export interface GasEstimateComponents { + gasEstimate: bigint; // The overall gas usage (used to set the transaction 'gasLimit'). + observedGasEstimate: bigint; // The gas usage observed by the contract. + additionalFeeEstimate: bigint; // Any additional fee incurred by the transaction. +} + export class Resolver { readonly resolverType; @@ -78,4 +84,28 @@ export class Resolver { ): Promise { return new Promise((resolve) => resolve(observedBlockNumber)); }; + + async estimateGas( + transactionRequest: TransactionRequest + ): Promise { + const gasEstimatePromise = this.provider.estimateGas(transactionRequest); + const additionalFeeEstimatePromise = this.estimateAdditionalFee(transactionRequest); + + const [ + gasEstimate, + additionalFeeEstimate + ] = await Promise.all([gasEstimatePromise, additionalFeeEstimatePromise]); + + return { + gasEstimate, + observedGasEstimate: gasEstimate, + additionalFeeEstimate, + }; + } + + estimateAdditionalFee( + _transactionRequest: TransactionRequest + ): Promise { + return new Promise((resolve) => resolve(0n)); + } } diff --git a/src/underwriter/queues/discover-queue.ts b/src/underwriter/queues/discover-queue.ts index 8212122..95d8e7d 100644 --- a/src/underwriter/queues/discover-queue.ts +++ b/src/underwriter/queues/discover-queue.ts @@ -1,8 +1,8 @@ import { HandleOrderResult, ProcessingQueue } from "src/processing-queue/processing-queue"; -import { DiscoverOrder, EvalOrder } from "../underwriter.types"; +import { DiscoverOrder, EvalOrder, UnderwriterEndpointConfig } from "../underwriter.types"; import { JsonRpcProvider } from "ethers"; import pino from "pino"; -import { EndpointConfig, TokenConfig } from "src/config/config.types"; +import { TokenConfig } from "src/config/config.types"; import { calcUnderwriteIdentifier, tryErrorToString } from "src/common/utils"; import { CatalystFactory__factory, CatalystVaultCommon__factory } from "src/contracts"; import { Store } from "src/store/store.lib"; @@ -15,7 +15,7 @@ export class DiscoverQueue extends ProcessingQueue { constructor( private readonly chainId: string, - private readonly endpointConfigs: EndpointConfig[], + private readonly endpointConfigs: UnderwriterEndpointConfig[], private readonly tokens: Record, retryInterval: number, maxTries: number, @@ -70,7 +70,8 @@ export class DiscoverQueue extends ProcessingQueue { const result: EvalOrder = { ...order, - toAsset + toAsset, + relayDeliveryCosts: endpoint.relayDeliveryCosts, } return { result }; @@ -149,7 +150,7 @@ export class DiscoverQueue extends ProcessingQueue { } } - private async isVaultVaild(vaultAddress: string, interfaceAddress: string, endpointConfig: EndpointConfig): Promise { + private async isVaultVaild(vaultAddress: string, interfaceAddress: string, endpointConfig: UnderwriterEndpointConfig): Promise { const validCache = this.validatedVaults.get(vaultAddress); if (validCache != undefined) { diff --git a/src/underwriter/queues/eval-queue.ts b/src/underwriter/queues/eval-queue.ts index 3ccecca..86feee9 100644 --- a/src/underwriter/queues/eval-queue.ts +++ b/src/underwriter/queues/eval-queue.ts @@ -1,11 +1,11 @@ import { tryErrorToString } from 'src/common/utils'; import pino from "pino"; import { HandleOrderResult, ProcessingQueue } from "../../processing-queue/processing-queue"; -import { EvalOrder, UnderwriteOrder } from "../underwriter.types"; -import { TokenConfig } from "src/config/config.types"; +import { EvalOrder, UnderwriteOrder, UnderwriterTokenConfig } from "../underwriter.types"; import { CatalystVaultCommon__factory } from "src/contracts"; -import { JsonRpcProvider } from 'ethers'; +import { JsonRpcProvider, MaxUint256 } from 'ethers'; import { TokenHandler } from '../token-handler/token-handler'; +import { WalletInterface } from 'src/wallet/wallet.interface'; const DECIMAL_RESOLUTION = 1_000_000; const DECIMAL_RESOLUTION_BIGINT = BigInt(DECIMAL_RESOLUTION); @@ -17,15 +17,16 @@ export class EvalQueue extends ProcessingQueue { constructor( private enabled: boolean, private readonly chainId: string, - private readonly tokens: Record, + private readonly tokens: Record, retryInterval: number, maxTries: number, underwritingCollateral: number, - allowanceBuffer: number, + private readonly allowanceBuffer: number, private readonly maxUnderwriteDelay: number, private readonly minRelayDeadlineDuration: bigint, private readonly minMaxGasDelivery: bigint, private readonly tokenHandler: TokenHandler, + private readonly wallet: WalletInterface, private readonly provider: JsonRpcProvider, private readonly logger: pino.Logger ) { @@ -134,43 +135,89 @@ export class EvalQueue extends ProcessingQueue { const expectedReturn = await toVaultContract.calcReceiveAsset(order.toAsset, order.units); const toAssetAllowance = expectedReturn * this.effectiveAllowanceBuffer / DECIMAL_RESOLUTION_BIGINT; - // Verify the token balances involed are acceptable + // Set the maximum allowed gasLimit for the transaction. This will be checked on the + // 'underwrite' queue with an 'estimateGas' call. + // ! It is not possible to 'estimateGas' of the underwrite transaction at this point, as + // ! before doing it the allowance for underwriting must be set. The allowance for + // ! underwriting is set **after** the evaluation step, as the allowance amount is not + // ! known until the evaluation step completes. + + const relayFiatProfitEstimate = await this.querySwapRelayProfitEstimate( + this.chainId, + order.messageIdentifier, + order.relayDeliveryCosts.gasUsage, + order.relayDeliveryCosts.gasObserved, + order.relayDeliveryCosts.fee, + order.relayDeliveryCosts.value, + ); + + const underwriteFiatAmount = await this.getTokenValue( + this.chainId, + tokenConfig.tokenId, + expectedReturn + ); + + // Verify the underwrite value is allowed if ( tokenConfig.maxUnderwriteAllowed - && toAssetAllowance > tokenConfig.maxUnderwriteAllowed + && underwriteFiatAmount * (1 + this.allowanceBuffer) > tokenConfig.maxUnderwriteAllowed ) { this.logger.info( { swapId: order.swapIdentifier, swapTxHash: order.swapTxHash, toAsset: order.toAsset, - toAssetAllowance, + allowanceBuffer: this.allowanceBuffer, + underwriteAmount: underwriteFiatAmount, maxUnderwriteAllowed: tokenConfig.maxUnderwriteAllowed }, - "Skipping underwrite: underwrite exceed the 'maxUnderwriteAllowed' configuration." + "Skipping underwrite: underwrite exceeds the 'maxUnderwriteAllowed' configuration." ); return null; } - const expectedReward = (expectedReturn * order.underwriteIncentiveX16) >> 16n; - if ( - tokenConfig.minUnderwriteReward - && expectedReward < tokenConfig.minUnderwriteReward - ) { + const underwriteIncentiveShare = Number(order.underwriteIncentiveX16) / 2**16; + const rewardFiatAmount = underwriteFiatAmount * underwriteIncentiveShare; + + const gasPrice = await this.getGasPrice(this.chainId); + const maxGasLimit = await this.calcMaxGasLimit( + underwriteFiatAmount, + rewardFiatAmount, + gasPrice, + tokenConfig, + relayFiatProfitEstimate, + ); + + this.logger.info( + { + swapId: order.swapIdentifier, + swapTxHash: order.swapTxHash, + toAsset: order.toAsset, + underwriteAmount: expectedReturn, + underwriteFiatAmount, + underwriteIncentiveX16: order.underwriteIncentiveX16, + rewardFiatAmount, + gasPrice, + tokenConfig, + relayFiatProfitEstimate, + maxGasLimit, + }, + "Underwrite evaluation." + ) + + if (maxGasLimit <= 0n) { this.logger.info( { swapId: order.swapIdentifier, swapTxHash: order.swapTxHash, - toAsset: order.toAsset, - underwriteIncentiveX16: order.underwriteIncentiveX16, - expectedReward, - minUnderwriteReward: tokenConfig.minUnderwriteReward + maxGasLimit }, - "Skipping underwrite: expected underwrite reward is less than the 'minUnderwriteReward' configuration." + "Skipping underwrite: calculated maximum gas limit is 0 or negative." ); return null; } + // Verify the underwriter has enough assets to perform the underwrite const enoughBalanceToUnderwrite = await this.tokenHandler.hasEnoughBalance( toAssetAllowance, @@ -189,42 +236,19 @@ export class EvalQueue extends ProcessingQueue { return null; } - // Set the maximum allowed gasLimit for the transaction. This will be checked on the - // 'underwrite' queue with an 'estimateGas' call. - // ! It is not possible to 'estimateGas' of the underwrite transaction at this point, as - // ! before doing it the allowance for underwriting must be set. The allowance for - // ! underwriting is set **after** the evaluation step, as the allowance amount is not - // ! known until the evaluation step completes. - const maxGasLimit = null; //TODO - - //TODO add economical evaluation - if (true) { - await this.tokenHandler.registerBalanceUse( - toAssetAllowance, - order.toAsset - ); - - const result: UnderwriteOrder = { - ...order, - maxGasLimit, - toAssetAllowance, - } - return { result }; - } else { - this.logger.info( - { - fromVault: order.fromVault, - fromChainId: order.fromChainId, - swapTxHash: order.swapTxHash, - swapId: order.swapIdentifier, - try: retryCount + 1 - }, - `Dropping order on evaluation` - ); + await this.tokenHandler.registerBalanceUse( + toAssetAllowance, + order.toAsset + ); - return null; + const result: UnderwriteOrder = { + ...order, + maxGasLimit, + gasPrice, + toAssetAllowance, } + return { result }; } protected async handleFailedOrder(order: EvalOrder, retryCount: number, error: any): Promise { @@ -318,6 +342,159 @@ export class EvalQueue extends ProcessingQueue { // } + async calcMaxGasLimit( + underwriteFiatAmount: number, + rewardFiatAmount: number, + gasPrice: bigint, + tokenConfig: UnderwriterTokenConfig, + relayFiatProfitEstimate: number, + ): Promise { + + // Use the 'profitabilityFactor' to bias the profitability calculation: a larger factor + // implies a larger profitability guarantee. If set to '0', effectively disable the + // evaluation step. + const adjustedRewardFiatAmount = tokenConfig.profitabilityFactor == 0 + ? Infinity + : rewardFiatAmount / tokenConfig.profitabilityFactor; + + if (Math.floor(adjustedRewardFiatAmount * DECIMAL_RESOLUTION) == 0) { + return 0n; + } + + // Only take into account the relay profit if it's negative. + const relayFiatProfit = Math.min(relayFiatProfitEstimate, 0); + const maxFiatTxCostToBreakEven = adjustedRewardFiatAmount + relayFiatProfit; + + const gasFiatPrice = await this.getGasValue(this.chainId, gasPrice); + + + // TODO is the following logic enough? This logic would allow unrealistically large + // TODO `maxGasLimit`s. + // Compute the limit based on the 'minUnderwriteReward' + const maxGasLimitMinReward = ( + maxFiatTxCostToBreakEven - tokenConfig.minUnderwriteReward + ) / gasFiatPrice; + + // Compute the limit based on the 'relativeMinUnderwriteReward' + const maxGasLimitMinRelativeReward = ( + maxFiatTxCostToBreakEven - tokenConfig.relativeMinUnderwriteReward * underwriteFiatAmount + ) / (gasFiatPrice * (1 + tokenConfig.relativeMinUnderwriteReward)); + + const maxGasLimit = maxGasLimitMinReward < maxGasLimitMinRelativeReward + ? maxGasLimitMinReward + : maxGasLimitMinRelativeReward; + + return maxGasLimit == Infinity + ? MaxUint256 + : BigInt(Math.floor(maxGasLimit)); + } + + async getGasValue( + chainId: string, + amount: bigint, + ): Promise { + return this.queryRelayerAssetPrice(chainId, amount); + } + + async getTokenValue( + chainId: string, + tokenId: string, + amount: bigint + ): Promise { + return this.queryRelayerAssetPrice(chainId, amount, tokenId); + } + + private async queryRelayerAssetPrice( + chainId: string, + amount: bigint, + tokenId?: string, + ): Promise { + + if (amount == 0n) { + return 0; + } + + const relayerEndpoint = `http://${process.env['RELAYER_HOST']}:${process.env['RELAYER_PORT']}/getPrice?`; + + const queryParameters: Record = { + chainId, + amount: amount.toString(), + } + + if (tokenId != undefined) { + queryParameters['tokenId'] = tokenId; + } + + const res = await fetch(relayerEndpoint + new URLSearchParams(queryParameters)); + const priceResponse = (await res.json()); //TODO type + + if (priceResponse.price == undefined) { + this.logger.warn( + { + chainId, + tokenId, + amount, + }, + `Failed to query token value.` + ); + return 0; + } + + return priceResponse.price; + } + + private async getGasPrice(chainId: string): Promise { + const feeData = await this.wallet.getFeeData(chainId); + // If gas fee data is missing or incomplete, default the gas price to an extremely high + // value. + // ! Use 'gasPrice' over 'maxFeePerGas', as 'maxFeePerGas' defines the highest gas fee + // ! allowed, which does not necessarilly represent the real gas fee at which the + // ! transactions are going through. + const gasPrice = feeData?.gasPrice + ?? feeData?.maxFeePerGas + ?? MaxUint256; + + return gasPrice; + } + + private async querySwapRelayProfitEstimate( + chainId: string, + messageIdentifier: string, + gasEstimate: bigint, + observedGasEstimate: bigint, + additionalFeeEstimate: bigint, + value: bigint, + ): Promise { + + const relayerEndpoint = `http://${process.env['RELAYER_HOST']}:${process.env['RELAYER_PORT']}/evaluateDelivery?`; + + const queryParameters: Record = { + chainId, + messageIdentifier, + gasEstimate: gasEstimate.toString(), + observedGasEstimate: observedGasEstimate.toString(), + additionalFeeEstimate: additionalFeeEstimate.toString(), + value: value.toString(), + } + + try { + const res = await fetch(relayerEndpoint + new URLSearchParams(queryParameters)); + const evaluationResponse = (await res.json()); //TODO type + + return evaluationResponse.securedDeliveryFiatProfit; + } + catch (error) { + this.logger.error( + { + queryParameters, + error: tryErrorToString(error), + }, + `Failed to query swap relay profit estimate.` + ); + throw new Error(`Failed to query swap relay profit estimate.`); + } + } + // Management utils diff --git a/src/underwriter/queues/underwrite-queue.ts b/src/underwriter/queues/underwrite-queue.ts index b76ac1a..031462d 100644 --- a/src/underwriter/queues/underwrite-queue.ts +++ b/src/underwriter/queues/underwrite-queue.ts @@ -8,6 +8,7 @@ import { WalletInterface } from "src/wallet/wallet.interface"; import { encodeBytes65Address } from "src/common/decode.payload"; import fetch from "node-fetch"; import { tryErrorToString } from "src/common/utils"; +import { Resolver } from "src/resolvers/resolver"; export class UnderwriteQueue extends ProcessingQueue { @@ -16,6 +17,7 @@ export class UnderwriteQueue extends ProcessingQueue, retryInterval: number, maxTries: number, + private readonly resolver: Resolver, private readonly walletPublicKey: string, private readonly wallet: WalletInterface, private readonly provider: JsonRpcProvider, @@ -46,27 +48,49 @@ export class UnderwriteQueue extends ProcessingQueue order.maxGasLimit) { - // NOTE: the following error message is matched on the 'handleFailedOrder' handler below. - throw new Error('Skipping underwrite, \'gasLimit\' is larger than the set \'maxGasLimit\'.') + // Compensate the `maxGasLimit` with any fixed cost incurred by the transaction. + const fixedCostGasEquivalent = gasEstimateComponents.additionalFeeEstimate / order.gasPrice; + const effectiveGasLimit = order.maxGasLimit - fixedCostGasEquivalent; + + const logData = { + order, + gasEstimate: gasEstimateComponents.gasEstimate, + gasEstimateLimit: effectiveGasLimit, + additionalFee: gasEstimateComponents.additionalFeeEstimate, + fixedCostGasEquivalent, + }; + + if (gasEstimateComponents.gasEstimate > effectiveGasLimit) { + this.logger.info( + logData, + `Underwrite evaluation: skipping underwrite, transaction gas estimate is larger than the maximum calculated allowed limit.` + ); + return null; + } + else { + this.logger.info( + logData, + `Underwrite evaluation: execute underwrite.` + ); } - } - const txRequest: TransactionRequest = { - to: order.interfaceAddress, - data: txData, - gasLimit: order.gasLimit, - }; + order.gasLimit = gasEstimateComponents.gasEstimate; + } const txPromise = this.wallet.submitTransaction( this.chainId, @@ -114,15 +138,6 @@ export class UnderwriteQueue extends ProcessingQueue, + endpointConfigs: UnderwriterEndpointConfig[], ambs: Record, rpc: string, + resolver: string | null; retryInterval: number; processingInterval: number; maxTries: number; @@ -154,11 +163,17 @@ export class UnderwriterService implements OnModuleInit { const underwritingCollateral = globalUnderwriterConfig.underwritingCollateral ?? DEFAULT_UNDERWRITER_UNDERWRITING_COLLATERAL; const allowanceBuffer = globalUnderwriterConfig.allowanceBuffer ?? DEFAULT_UNDERWRITER_ALLOWANCE_BUFFER; const maxUnderwriteAllowed = globalUnderwriterConfig.maxUnderwriteAllowed; - const minUnderwriteReward = globalUnderwriterConfig.minUnderwriteReward; + const minUnderwriteReward = globalUnderwriterConfig.minUnderwriteReward ?? DEFAULT_UNDERWRITER_MIN_UNDERWRITE_REWARD; + const relativeMinUnderwriteReward = globalUnderwriterConfig.relativeMinUnderwriteReward ?? DEFAULT_UNDERWRITER_RELATIVE_MIN_UNDERWRITE_REWARD; + const profitabilityFactor = globalUnderwriterConfig.profitabilityFactor ?? DEFAULT_UNDERWRITER_PROFITABILITY_FACTOR; const lowTokenBalanceWarning = globalUnderwriterConfig.lowTokenBalanceWarning; const tokenBalanceUpdateInterval = globalUnderwriterConfig.tokenBalanceUpdateInterval ?? DEFAULT_UNDERWRITER_TOKEN_BALANCE_UPDATE_INTERVAL; const walletPublicKey = (new Wallet(this.configService.globalConfig.privateKey)).address; + const relayDeliveryCosts: RelayDeliveryCosts = globalUnderwriterConfig.relayDeliveryCosts ?? { + gasUsage: DEFAULT_UNDERWRITER_RELAY_DELIVERY_GAS_USAGE + }; + if (minRelayDeadlineDuration < MIN_ALLOWED_MIN_RELAY_DEADLINE_DURATION) { throw new Error( `Invalid 'minRelayDeadlineDuration' global configuration. Value set is less than allowed (set: ${minRelayDeadlineDuration}, minimum: ${MIN_ALLOWED_MIN_RELAY_DEADLINE_DURATION}).` @@ -179,9 +194,12 @@ export class UnderwriterService implements OnModuleInit { allowanceBuffer, maxUnderwriteAllowed, minUnderwriteReward, + relativeMinUnderwriteReward, + profitabilityFactor, lowTokenBalanceWarning, tokenBalanceUpdateInterval, walletPublicKey, + relayDeliveryCosts } } @@ -198,6 +216,38 @@ export class UnderwriterService implements OnModuleInit { return undefined; } + const endpointConfigs: UnderwriterEndpointConfig[] = chainEndpointConfigs.map(endpointConfig => { + + const endpointRelayDeliveryCosts = endpointConfig.relayDeliveryCosts; + const chainRelayDeliveryCosts = chainConfig.underwriter.relayDeliveryCosts; + const globalRelayDeliveryCosts = defaultConfig.relayDeliveryCosts; + + const costs = endpointRelayDeliveryCosts ?? chainRelayDeliveryCosts ?? globalRelayDeliveryCosts; + const gasObserved = costs.gasObserved ?? costs.gasUsage; + + if (gasObserved > costs.gasUsage) { + this.loggerService.warn( + { + endpointName: endpointConfig.name, + gasUsage: costs.gasUsage.toString(), + gasObserved: gasObserved.toString(), + }, + `Invalid derived relay delivery costs configuration: 'gasObserved' is larger than 'gasUsage'. Skipping chain.` + ); + return undefined; + } + + return { + ...endpointConfig, + relayDeliveryCosts: { + gasUsage: costs.gasUsage, + gasObserved: costs.gasObserved ?? costs.gasUsage, + fee: costs.fee ?? 0n, + value: costs.value ?? 0n, + } + }; + }).filter((config): config is UnderwriterEndpointConfig => config != undefined); + const chainUnderwriterConfig = chainConfig.underwriter; const minRelayDeadlineDuration = chainUnderwriterConfig.minRelayDeadlineDuration ?? defaultConfig.minRelayDeadlineDuration; @@ -212,9 +262,10 @@ export class UnderwriterService implements OnModuleInit { chainId, chainName: chainConfig.name, tokens: this.loadTokensConfig(chainConfig, defaultConfig), - endpointConfigs: chainEndpointConfigs, + endpointConfigs, ambs, rpc: chainConfig.rpc, + resolver: chainConfig.resolver, retryInterval: chainUnderwriterConfig.retryInterval ?? defaultConfig.retryInterval, processingInterval: @@ -251,7 +302,7 @@ export class UnderwriterService implements OnModuleInit { private loadTokensConfig( chainConfig: ChainConfig, defaultConfig: DefaultUnderwriterWorkerData - ): TokensConfig { + ): Record { const chainUnderwriterConfig = chainConfig.underwriter; // Token-specific config can be specified in three places. The hierarchy of the config to @@ -260,28 +311,40 @@ export class UnderwriterService implements OnModuleInit { // - chain > underwriter > ${config} // - global > underwriter > ${config} - const finalConfig: TokensConfig = {}; + const finalConfig: Record = {}; for (const [tokenAddress, chainTokenConfig] of Object.entries(chainConfig.tokens)) { - finalConfig[tokenAddress] = { ...chainTokenConfig }; - - finalConfig[tokenAddress]!.maxUnderwriteAllowed ??= - chainUnderwriterConfig.maxUnderwriteAllowed - ?? defaultConfig.maxUnderwriteAllowed; + finalConfig[tokenAddress] = { + + tokenId: chainTokenConfig.tokenId, + + maxUnderwriteAllowed: chainTokenConfig.maxUnderwriteAllowed + ?? chainUnderwriterConfig.maxUnderwriteAllowed + ?? defaultConfig.maxUnderwriteAllowed, + + minUnderwriteReward: chainTokenConfig.minUnderwriteReward + ?? chainUnderwriterConfig.minUnderwriteReward + ?? defaultConfig.minUnderwriteReward, + + relativeMinUnderwriteReward: chainTokenConfig.relativeMinUnderwriteReward + ?? chainUnderwriterConfig.relativeMinUnderwriteReward + ?? defaultConfig.relativeMinUnderwriteReward, + + profitabilityFactor: chainTokenConfig.profitabilityFactor + ?? chainUnderwriterConfig.profitabilityFactor + ?? defaultConfig.profitabilityFactor, + + lowTokenBalanceWarning: chainTokenConfig.lowTokenBalanceWarning + ?? chainUnderwriterConfig.lowTokenBalanceWarning + ?? defaultConfig.lowTokenBalanceWarning, + + tokenBalanceUpdateInterval: chainTokenConfig.tokenBalanceUpdateInterval + ?? chainUnderwriterConfig.tokenBalanceUpdateInterval + ?? defaultConfig.tokenBalanceUpdateInterval, - finalConfig[tokenAddress]!.minUnderwriteReward ??= - chainUnderwriterConfig.minUnderwriteReward - ?? defaultConfig.minUnderwriteReward; - - finalConfig[tokenAddress]!.lowTokenBalanceWarning ??= - chainUnderwriterConfig.lowTokenBalanceWarning - ?? defaultConfig.lowTokenBalanceWarning; - - finalConfig[tokenAddress]!.tokenBalanceUpdateInterval ??= - chainUnderwriterConfig.tokenBalanceUpdateInterval - ?? defaultConfig.tokenBalanceUpdateInterval; + }; } - return finalConfig; + return finalConfig as Record; } private initiateIntervalStatusLog(): void { diff --git a/src/underwriter/underwriter.types.ts b/src/underwriter/underwriter.types.ts index d0df453..a56b75a 100644 --- a/src/underwriter/underwriter.types.ts +++ b/src/underwriter/underwriter.types.ts @@ -1,5 +1,35 @@ import { TransactionReceipt, TransactionResponse } from "ethers"; +import { VaultTemplateConfig } from "src/config/config.types"; +export interface UnderwriterTokenConfig { + tokenId: string; + allowanceBuffer?: bigint; + maxUnderwriteAllowed?: number; + minUnderwriteReward: number; + relativeMinUnderwriteReward: number; + profitabilityFactor: number; + lowTokenBalanceWarning?: bigint; + tokenBalanceUpdateInterval?: number; +} + +export interface UnderwriterEndpointConfig { + name: string; + amb: string; + chainId: string; + factoryAddress: string; + interfaceAddress: string; + incentivesAddress: string; + channelsOnDestination: Record; + vaultTemplates: VaultTemplateConfig[]; + relayDeliveryCosts: UnderwriterRelayDeliveryCostsConfig; +} + +export interface UnderwriterRelayDeliveryCostsConfig { + gasUsage: bigint; + gasObserved: bigint; + fee: bigint; + value: bigint; +} export interface DiscoverOrder { // ! These are unsafe until the DiscoverQueue validates the order @@ -36,10 +66,12 @@ export interface DiscoverOrder { export interface EvalOrder extends DiscoverOrder { toAsset: string; + relayDeliveryCosts: UnderwriterRelayDeliveryCostsConfig; } export interface UnderwriteOrder extends EvalOrder { - maxGasLimit: bigint | null; + maxGasLimit: bigint; + gasPrice: bigint; gasLimit?: bigint; toAssetAllowance: bigint; } diff --git a/src/underwriter/underwriter.worker.ts b/src/underwriter/underwriter.worker.ts index e11eb72..ce119fa 100644 --- a/src/underwriter/underwriter.worker.ts +++ b/src/underwriter/underwriter.worker.ts @@ -3,16 +3,17 @@ import pino, { LoggerOptions } from "pino"; import { parentPort, workerData } from 'worker_threads'; import { UnderwriterWorkerCommand, UnderwriterWorkerCommandId, UnderwriterWorkerData } from "./underwriter.service"; import { tryErrorToString, wait } from "src/common/utils"; -import { AMBConfig, EndpointConfig, TokenConfig } from "src/config/config.types"; +import { AMBConfig } from "src/config/config.types"; import { STATUS_LOG_INTERVAL } from "src/logger/logger.service"; import { Store } from "src/store/store.lib"; import { SwapDescription } from "src/store/store.types"; -import { DiscoverOrder, NewOrder, UnderwriteOrder, UnderwriteOrderResult } from "./underwriter.types"; +import { DiscoverOrder, NewOrder, UnderwriteOrder, UnderwriteOrderResult, UnderwriterEndpointConfig, UnderwriterTokenConfig } from "./underwriter.types"; import { EvalQueue } from "./queues/eval-queue"; import { UnderwriteQueue } from "./queues/underwrite-queue"; import { TokenHandler } from "./token-handler/token-handler"; import { WalletInterface } from "src/wallet/wallet.interface"; import { DiscoverQueue } from "./queues/discover-queue"; +import { Resolver, loadResolver } from "src/resolvers/resolver"; class UnderwriterWorker { @@ -26,8 +27,10 @@ class UnderwriterWorker { private readonly chainId: string; private readonly chainName: string; - private readonly endpoints: EndpointConfig[]; - private readonly tokens: Record; + private readonly resolver: Resolver; + + private readonly endpoints: UnderwriterEndpointConfig[]; + private readonly tokens: Record; private readonly ambs: Record; private readonly wallet: WalletInterface; @@ -56,6 +59,12 @@ class UnderwriterWorker { ); this.provider = this.initializeProvider(this.config.rpc); + this.resolver = loadResolver( + this.config.resolver, + this.provider, + this.logger + ); + this.wallet = new WalletInterface(this.config.walletPort); this.tokenHandler = new TokenHandler( @@ -83,6 +92,7 @@ class UnderwriterWorker { this.config.minRelayDeadlineDuration, this.config.minMaxGasDelivery, this.tokenHandler, + this.resolver, this.config.walletPublicKey, this.wallet, this.store, @@ -121,8 +131,8 @@ class UnderwriterWorker { private initializeQueues( enabled: boolean, chainId: string, - endpointConfigs: EndpointConfig[], - tokens: Record, + endpointConfigs: UnderwriterEndpointConfig[], + tokens: Record, ambs: Record, retryInterval: number, maxTries: number, @@ -132,6 +142,7 @@ class UnderwriterWorker { minRelayDeadlineDuration: bigint, minMaxGasDelivery: bigint, tokenHandler: TokenHandler, + resolver: Resolver, walletPublicKey: string, wallet: WalletInterface, store: Store, @@ -161,6 +172,7 @@ class UnderwriterWorker { minRelayDeadlineDuration, minMaxGasDelivery, tokenHandler, + wallet, provider, logger ); @@ -170,6 +182,7 @@ class UnderwriterWorker { ambs, retryInterval, maxTries, + resolver, walletPublicKey, wallet, provider,