From c2206c814cfa9569a74ace0ee43ffe67a41595b1 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Tue, 12 Apr 2022 05:43:20 -0700 Subject: [PATCH] SDK - interchain gas calculator (#318) * Add interchainGasPaymaster to core deploy script and sdk test environment * Add interchainGasPaymaster to CoreContracts * First start at interchain gas estimation * Building, time for clean up * Move messages to their own dir, add mocha & chai * Rename UndispatchedMessage -> BaseMessage * Better setup for testing * More clean up, move mulBigAndFixed to utils * Clean * prettier * Update package-lock.json * Create test utils, clean up * Nits * TSDoc comments * Create InterchainGasCalculator * Add sdk test to github workflow * nits * Create message/index.ts, mv from -> origin, prettier * mkdir gas, rename source -> origin * Fix tests * Fix utils tests * Rename suggestedDestinationGasPrice -> suggestedGasPrice, convertDestinationWeiToOriginWei -> convertDomainNativeTokens * Comment on divUnsafe, prettier * add estimatePaymentFromMessage, rm InterchainGasPayingMessage * Move back to original AbacusMessage * prettier * rm AbacusMessage's from * Add JSDoc to estimatePaymentFromMessage * Renames * Move ParsedMessage and parseMessage back to message.ts * Rm suggested gas price multiplier * Fix test build * nit * prettier * Last comments --- .github/workflows/node.yml | 4 +- package-lock.json | 612 +++++++++++++++++++ typescript/infra/src/core/deploy.ts | 12 + typescript/sdk/.mocharc.json | 3 + typescript/sdk/package.json | 5 +- typescript/sdk/src/core/contracts.ts | 10 + typescript/sdk/src/core/environments/test.ts | 114 ++-- typescript/sdk/src/core/index.ts | 8 +- typescript/sdk/src/core/message.ts | 24 +- typescript/sdk/src/gas/calculator.ts | 264 ++++++++ typescript/sdk/src/gas/index.ts | 2 + typescript/sdk/src/gas/token-prices.ts | 13 + typescript/sdk/src/gas/utils.ts | 64 ++ typescript/sdk/src/index.ts | 7 + typescript/sdk/src/types.ts | 1 + typescript/sdk/test/gas/calculator.test.ts | 148 +++++ typescript/sdk/test/utils.test.ts | 49 ++ typescript/sdk/test/utils.ts | 103 ++++ typescript/sdk/tsconfig.json | 2 +- typescript/utils/src/utils.ts | 4 + 20 files changed, 1376 insertions(+), 73 deletions(-) create mode 100644 typescript/sdk/.mocharc.json create mode 100644 typescript/sdk/src/gas/calculator.ts create mode 100644 typescript/sdk/src/gas/index.ts create mode 100644 typescript/sdk/src/gas/token-prices.ts create mode 100644 typescript/sdk/src/gas/utils.ts create mode 100644 typescript/sdk/test/gas/calculator.test.ts create mode 100644 typescript/sdk/test/utils.test.ts create mode 100644 typescript/sdk/test/utils.ts diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 694111cd1..aaaca5fcd 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -83,7 +83,9 @@ jobs: path: ./* key: ${{ github.sha }} - - name: test + - name: sdk + run: npm --prefix ./typescript/sdk run test + - name: infra run: npm --prefix ./typescript/infra run test test-sol: diff --git a/package-lock.json b/package-lock.json index 4f489b348..8261cab8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3386,6 +3386,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -17199,6 +17205,18 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-url": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", @@ -18628,6 +18646,18 @@ "integrity": "sha1-DMj20OK2IrR5xA1JnEbWS3Vcb18=", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/native-promise-only": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", @@ -20677,6 +20707,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", @@ -24351,6 +24390,12 @@ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, + "node_modules/workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, "node_modules/world-calendars": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.3.tgz", @@ -24787,6 +24832,18 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "solidity/abacus-core": { "name": "@abacus-network/core", "version": "0.0.1", @@ -25112,10 +25169,12 @@ "@types/node": "^16.9.1", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", + "chai": "^4.3.6", "dotenv": "^10.0.0", "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", "fs": "0.0.1-security", + "mocha": "^9.2.2", "prettier": "^2.4.1", "typescript": "^4.4.3" } @@ -25126,6 +25185,308 @@ "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==", "dev": true }, + "typescript/sdk/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "typescript/sdk/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "typescript/sdk/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "typescript/sdk/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "typescript/sdk/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "typescript/sdk/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "typescript/sdk/node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "typescript/sdk/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "typescript/sdk/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "typescript/sdk/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "typescript/sdk/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "typescript/sdk/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "typescript/sdk/node_modules/minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "typescript/sdk/node_modules/mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "typescript/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "typescript/sdk/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "typescript/sdk/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "typescript/sdk/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "typescript/sdk/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "typescript/sdk/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "typescript/sdk/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "typescript/sdk/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "typescript/sdk/node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "typescript/utils": { "name": "@abacus-network/utils", "version": "0.0.7", @@ -25278,11 +25639,13 @@ "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "celo-ethers-provider": "0.0.0", + "chai": "^4.3.6", "dotenv": "^10.0.0", "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", "ethers": "^5.4.7", "fs": "0.0.1-security", + "mocha": "^9.2.2", "prettier": "^2.4.1", "typescript": "^4.4.3" }, @@ -25292,6 +25655,216 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz", "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==", "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } } } }, @@ -27958,6 +28531,12 @@ "eslint-visitor-keys": "^2.0.0" } }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, "@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -38654,6 +39233,12 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "is-url": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", @@ -39831,6 +40416,12 @@ "integrity": "sha1-DMj20OK2IrR5xA1JnEbWS3Vcb18=", "dev": true }, + "nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true + }, "native-promise-only": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", @@ -41430,6 +42021,15 @@ } } }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", @@ -44489,6 +45089,12 @@ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, "world-calendars": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.3.tgz", @@ -44837,6 +45443,12 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/typescript/infra/src/core/deploy.ts b/typescript/infra/src/core/deploy.ts index d07bc266a..9c61b9871 100644 --- a/typescript/infra/src/core/deploy.ts +++ b/typescript/infra/src/core/deploy.ts @@ -18,6 +18,7 @@ import { ValidatorManager__factory, Outbox__factory, Inbox__factory, + InterchainGasPaymaster__factory, } from '@abacus-network/core'; import { DeployEnvironment, RustConfig } from '../config'; import { CoreConfig } from './types'; @@ -64,6 +65,12 @@ export class AbacusCoreDeployer extends AbacusAppDeployer< [validatorManager.address], ); + const interchainGasPaymaster = await this.deployContract( + domain, + 'InterchainGasPaymaster', + new InterchainGasPaymaster__factory(signer), + ); + const xAppConnectionManager: XAppConnectionManager = await this.deployContract( domain, @@ -71,6 +78,10 @@ export class AbacusCoreDeployer extends AbacusAppDeployer< new XAppConnectionManager__factory(signer), ); await xAppConnectionManager.setOutbox(outbox.address, overrides); + await xAppConnectionManager.setInterchainGasPaymaster( + interchainGasPaymaster.address, + overrides, + ); const inboxes: Record> = {}; const inboxAddresses: Partial> = {}; @@ -114,6 +125,7 @@ export class AbacusCoreDeployer extends AbacusAppDeployer< upgradeBeaconController: upgradeBeaconController.address, xAppConnectionManager: xAppConnectionManager.address, validatorManager: validatorManager.address, + interchainGasPaymaster: interchainGasPaymaster.address, outbox: outbox.addresses, inboxes: inboxAddresses, }; diff --git a/typescript/sdk/.mocharc.json b/typescript/sdk/.mocharc.json new file mode 100644 index 000000000..e516df998 --- /dev/null +++ b/typescript/sdk/.mocharc.json @@ -0,0 +1,3 @@ +{ + "require": ["ts-node/register"] +} diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 6e24c69dc..b290505ab 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -8,18 +8,21 @@ "build": "tsc", "check": "tsc --noEmit", "prettier": "prettier --write ./src", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "test": "mocha --config .mocharc.json './test/**/*.test.ts'" }, "license": "MIT OR Apache-2.0", "devDependencies": { "@types/node": "^16.9.1", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", + "chai": "^4.3.6", "dotenv": "^10.0.0", "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", "fs": "0.0.1-security", "prettier": "^2.4.1", + "mocha": "^9.2.2", "typescript": "^4.4.3" }, "dependencies": { diff --git a/typescript/sdk/src/core/contracts.ts b/typescript/sdk/src/core/contracts.ts index ddb095ae8..1566ff21a 100644 --- a/typescript/sdk/src/core/contracts.ts +++ b/typescript/sdk/src/core/contracts.ts @@ -9,6 +9,8 @@ import { Outbox__factory, Inbox, Inbox__factory, + InterchainGasPaymaster, + InterchainGasPaymaster__factory, } from '@abacus-network/core'; import { types } from '@abacus-network/utils'; @@ -19,6 +21,7 @@ export type CoreContractAddresses = { upgradeBeaconController: types.Address; xAppConnectionManager: types.Address; validatorManager: types.Address; + interchainGasPaymaster: types.Address; outbox: ProxiedAddress; inboxes: Partial>; }; @@ -59,4 +62,11 @@ export class CoreContracts extends AbacusAppContracts { this.connection, ); } + + get interchainGasPaymaster(): InterchainGasPaymaster { + return InterchainGasPaymaster__factory.connect( + this.addresses.interchainGasPaymaster, + this.connection, + ); + } } diff --git a/typescript/sdk/src/core/environments/test.ts b/typescript/sdk/src/core/environments/test.ts index 6aa9e9b5b..a47f05bcb 100644 --- a/typescript/sdk/src/core/environments/test.ts +++ b/typescript/sdk/src/core/environments/test.ts @@ -1,8 +1,9 @@ export const addresses = { alfajores: { upgradeBeaconController: '0x5FbDB2315678afecb367f032d93F642f64180aa3', - xAppConnectionManager: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318', + xAppConnectionManager: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788', validatorManager: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512', + interchainGasPaymaster: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318', outbox: { proxy: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6', implementation: '0x0165878A594ca255338adfa4d48449f69242Eb8F', @@ -10,100 +11,103 @@ export const addresses = { }, inboxes: { kovan: { - proxy: '0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82', - implementation: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e', - beacon: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0', + proxy: '0x0B306BF915C4d645ff596e518fAf3F9669b97016', + implementation: '0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82', + beacon: '0x9A676e781A523b5d0C0e43731313A708CB607508', }, mumbai: { - proxy: '0x0B306BF915C4d645ff596e518fAf3F9669b97016', - implementation: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e', - beacon: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0', + proxy: '0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE', + implementation: '0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82', + beacon: '0x9A676e781A523b5d0C0e43731313A708CB607508', }, fuji: { - proxy: '0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE', - implementation: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e', - beacon: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0', + proxy: '0x3Aa5ebB10DC797CAC828524e59A333d0A371443c', + implementation: '0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82', + beacon: '0x9A676e781A523b5d0C0e43731313A708CB607508', }, }, }, kovan: { - upgradeBeaconController: '0x3Aa5ebB10DC797CAC828524e59A333d0A371443c', - xAppConnectionManager: '0xc5a5C42992dECbae36851359345FE25997F5C42d', - validatorManager: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', + upgradeBeaconController: '0x59b670e9fA9D0A427751Af201D676719a970857b', + xAppConnectionManager: '0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690', + validatorManager: '0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1', + interchainGasPaymaster: '0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E', outbox: { - proxy: '0x09635F643e140090A9A8Dcd712eD6285858ceBef', - implementation: '0x4A679253410272dd5232B3Ff7cF5dbB88f295319', - beacon: '0x7a2088a1bFc9d81c55368AE168C2C02570cB814F', + proxy: '0x67d269191c92Caf3cD7723F116c85e6E9bf55933', + implementation: '0x09635F643e140090A9A8Dcd712eD6285858ceBef', + beacon: '0xc5a5C42992dECbae36851359345FE25997F5C42d', }, inboxes: { alfajores: { - proxy: '0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB', - implementation: '0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E', - beacon: '0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690', + proxy: '0x851356ae760d987E095750cCeb3bC6014560891C', + implementation: '0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9', + beacon: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8', }, mumbai: { - proxy: '0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9', - implementation: '0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E', - beacon: '0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690', + proxy: '0x95401dc811bb5740090279Ba06cfA8fcF6113778', + implementation: '0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9', + beacon: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8', }, fuji: { - proxy: '0x851356ae760d987E095750cCeb3bC6014560891C', - implementation: '0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E', - beacon: '0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690', + proxy: '0x70e0bA845a1A0F2DA3359C97E0285013525FFC49', + implementation: '0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9', + beacon: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8', }, }, }, mumbai: { - upgradeBeaconController: '0x95401dc811bb5740090279Ba06cfA8fcF6113778', - xAppConnectionManager: '0x36C02dA8a0983159322a80FFE9F24b1acfF8B570', - validatorManager: '0x998abeb3E57409262aE5b751f60747921B33613E', + upgradeBeaconController: '0x99bbA657f2BbC93c02D617f8bA121cB8Fc104Acf', + xAppConnectionManager: '0xb7278A61aa25c888815aFC32Ad3cC52fF24fE575', + validatorManager: '0x0E801D84Fa97b50751Dbf25036d067dCf18858bF', + interchainGasPaymaster: '0x5f3f1dBD7B74C6B46e8c44f98792A1dAf8d69154', outbox: { - proxy: '0x5eb3Bc0a489C5A8288765d2336659EbCA68FCd00', - implementation: '0x8f86403A4DE0BB5791fa46B8e795C547942fE4Cf', - beacon: '0x9d4454B023096f34B160D6B654540c56A1F81688', + proxy: '0x1291Be112d480055DaFd8a610b7d1e203891C274', + implementation: '0x809d550fca64d94Bd9F66E60752A544199cfAC3D', + beacon: '0x4c5859f0F772848b2D91F1D83E2Fe57935348029', }, inboxes: { alfajores: { - proxy: '0x5f3f1dBD7B74C6B46e8c44f98792A1dAf8d69154', - implementation: '0x4c5859f0F772848b2D91F1D83E2Fe57935348029', - beacon: '0x1291Be112d480055DaFd8a610b7d1e203891C274', + proxy: '0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650', + implementation: '0x2bdCC0de6bE1f7D2ee689a0342D76F52E8EFABa3', + beacon: '0x7969c5eD335650692Bc04293B07F5BF2e7A673C0', }, kovan: { - proxy: '0xCD8a1C3ba11CF5ECfa6267617243239504a98d90', - implementation: '0x4c5859f0F772848b2D91F1D83E2Fe57935348029', - beacon: '0x1291Be112d480055DaFd8a610b7d1e203891C274', + proxy: '0xFD471836031dc5108809D173A067e8486B9047A3', + implementation: '0x2bdCC0de6bE1f7D2ee689a0342D76F52E8EFABa3', + beacon: '0x7969c5eD335650692Bc04293B07F5BF2e7A673C0', }, fuji: { - proxy: '0x2bdCC0de6bE1f7D2ee689a0342D76F52E8EFABa3', - implementation: '0x4c5859f0F772848b2D91F1D83E2Fe57935348029', - beacon: '0x1291Be112d480055DaFd8a610b7d1e203891C274', + proxy: '0x1429859428C0aBc9C2C47C8Ee9FBaf82cFA0F20f', + implementation: '0x2bdCC0de6bE1f7D2ee689a0342D76F52E8EFABa3', + beacon: '0x7969c5eD335650692Bc04293B07F5BF2e7A673C0', }, }, }, fuji: { - upgradeBeaconController: '0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650', - xAppConnectionManager: '0x1fA02b2d6A771842690194Cf62D91bdd92BfE28d', - validatorManager: '0xc351628EB244ec633d5f21fBD6621e1a683B1181', + upgradeBeaconController: '0x162A433068F51e18b7d13932F27e66a3f99E6890', + xAppConnectionManager: '0xDC11f7E700A4c898AE5CAddB1082cFfa76512aDD', + validatorManager: '0x922D6956C99E12DFeB3224DEA977D0939758A1Fe', + interchainGasPaymaster: '0xD8a5a9b31c3C0232E196d518E89Fd8bF83AcAd43', outbox: { - proxy: '0x5081a39b8A5f0E35a8D959395a630b68B74Dd30f', - implementation: '0x162A433068F51e18b7d13932F27e66a3f99E6890', - beacon: '0x922D6956C99E12DFeB3224DEA977D0939758A1Fe', + proxy: '0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2', + implementation: '0x4C4a2f8c81640e47606d3fd77B353E87Ba015584', + beacon: '0x21dF544947ba3E8b3c32561399E88B52Dc8b2823', }, inboxes: { alfajores: { - proxy: '0x21dF544947ba3E8b3c32561399E88B52Dc8b2823', - implementation: '0x04C89607413713Ec9775E14b954286519d836FEf', - beacon: '0x4C4a2f8c81640e47606d3fd77B353E87Ba015584', + proxy: '0x202CCe504e04bEd6fC0521238dDf04Bc9E8E15aB', + implementation: '0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7', + beacon: '0x0355B7B8cb128fA5692729Ab3AAa199C1753f726', }, kovan: { - proxy: '0xD8a5a9b31c3C0232E196d518E89Fd8bF83AcAd43', - implementation: '0x04C89607413713Ec9775E14b954286519d836FEf', - beacon: '0x4C4a2f8c81640e47606d3fd77B353E87Ba015584', + proxy: '0x172076E0166D1F9Cc711C77Adf8488051744980C', + implementation: '0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7', + beacon: '0x0355B7B8cb128fA5692729Ab3AAa199C1753f726', }, mumbai: { - proxy: '0x51A1ceB83B83F1985a81C295d1fF28Afef186E02', - implementation: '0x04C89607413713Ec9775E14b954286519d836FEf', - beacon: '0x4C4a2f8c81640e47606d3fd77B353E87Ba015584', + proxy: '0xBEc49fA140aCaA83533fB00A2BB19bDdd0290f25', + implementation: '0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7', + beacon: '0x0355B7B8cb128fA5692729Ab3AAa199C1753f726', }, }, }, diff --git a/typescript/sdk/src/core/index.ts b/typescript/sdk/src/core/index.ts index 33916fe01..e234a76ce 100644 --- a/typescript/sdk/src/core/index.ts +++ b/typescript/sdk/src/core/index.ts @@ -6,4 +6,10 @@ export { AnnotatedDispatch, AnnotatedLifecycleEvent, } from './events'; -export { AbacusMessage, AbacusStatus, MessageStatus } from './message'; +export { + AbacusMessage, + AbacusStatus, + MessageStatus, + ParsedMessage, + parseMessage, +} from './message'; diff --git a/typescript/sdk/src/core/message.ts b/typescript/sdk/src/core/message.ts index 55f9c1309..0c1ac4ea8 100644 --- a/typescript/sdk/src/core/message.ts +++ b/typescript/sdk/src/core/message.ts @@ -24,7 +24,7 @@ import { } from './events'; export type ParsedMessage = { - from: number; + origin: number; sender: string; nonce: number; destination: number; @@ -64,13 +64,13 @@ export type EventCache = { */ export function parseMessage(message: string): ParsedMessage { const buf = Buffer.from(arrayify(message)); - const from = buf.readUInt32BE(0); + const origin = buf.readUInt32BE(0); const sender = hexlify(buf.slice(4, 36)); const nonce = buf.readUInt32BE(36); const destination = buf.readUInt32BE(40); const recipient = hexlify(buf.slice(44, 76)); const body = hexlify(buf.slice(76)); - return { from, sender, nonce, destination, recipient, body }; + return { origin, sender, nonce, destination, recipient, body }; } /** @@ -89,8 +89,11 @@ export class AbacusMessage { this.core = core; this.message = parseMessage(dispatch.event.args.message); this.dispatch = dispatch; - this.outbox = core.mustGetContracts(this.message.from).outbox; - this.inbox = core.mustGetInbox(this.message.from, this.message.destination); + this.outbox = core.mustGetContracts(this.message.origin).outbox; + this.inbox = core.mustGetInbox( + this.message.origin, + this.message.destination, + ); this.cache = {}; } @@ -421,17 +424,10 @@ export class AbacusMessage { } /** - * The domain from which the message was sent - */ - get from(): number { - return this.message.from; - } - - /** - * The domain from which the message was sent. Alias for `from` + * The domain from which the message was sent. */ get origin(): number { - return this.from; + return this.message.origin; } /** diff --git a/typescript/sdk/src/gas/calculator.ts b/typescript/sdk/src/gas/calculator.ts new file mode 100644 index 000000000..d05d44c08 --- /dev/null +++ b/typescript/sdk/src/gas/calculator.ts @@ -0,0 +1,264 @@ +import { BigNumber, ethers, FixedNumber } from 'ethers'; +import { utils } from '@abacus-network/utils'; + +import { AbacusCore, ParsedMessage } from '..'; +import { convertDecimalValue, mulBigAndFixed } from './utils'; +import { DefaultTokenPriceGetter, TokenPriceGetter } from './token-prices'; + +/** + * A note on arithmetic: + * The ethers.BigNumber implementation behaves very similar to Solidity's + * number handling by not supporting decimals. To avoid adding another big + * number implementation as a dependency, we use ethers.FixedNumber, a + * fixed point implementation intended to model how Solidity's half-supported + * fixed point numbers work, see https://docs.soliditylang.org/en/v0.8.13/types.html#fixed-point-numbers). + * + * Generally, ceiling is used rather than floor here to err on the side of over- + * estimating amounts. + */ + +// If a domain doesn't specify how many decimals their native token has, 18 is used. +const DEFAULT_TOKEN_DECIMALS = 18; + +export interface InterchainGasCalculatorConfig { + /** + * A multiplier applied to the estimated origin token payment amount. + * This should be high enough to account for movements in token exchange + * rates and gas prices. + * @defaultValue 1.25 + */ + paymentEstimateMultiplier?: string; + /** + * An amount of additional gas to add to the estimated gas of processing a message. + * Only used when estimating a payment from a message. + * @defaultValue 50,000 + */ + messageGasEstimateBuffer?: string; + /** + * Used to get the native token prices of the origin and destination chains. + * @defaultValue An instance of DefaultTokenPriceGetter. + */ + tokenPriceGetter?: TokenPriceGetter; +} + +/** + * Calculates interchain gas payments. + */ +export class InterchainGasCalculator { + core: AbacusCore; + + tokenPriceGetter: TokenPriceGetter; + + paymentEstimateMultiplier: ethers.FixedNumber; + messageGasEstimateBuffer: ethers.BigNumber; + + constructor(core: AbacusCore, config?: InterchainGasCalculatorConfig) { + this.core = core; + + this.tokenPriceGetter = + config?.tokenPriceGetter ?? new DefaultTokenPriceGetter(); + + this.paymentEstimateMultiplier = FixedNumber.from( + config?.paymentEstimateMultiplier ?? '1.25', + ); + this.messageGasEstimateBuffer = BigNumber.from( + config?.messageGasEstimateBuffer ?? 50_000, + ); + } + + /** + * Calculates the estimated payment for an amount of gas on the destination chain, + * denominated in the native token of the origin chain. Considers the exchange + * rate between the native tokens of the origin and destination chains, and the + * suggested gas price on the destination chain. Applies the multiplier + * `paymentEstimateMultiplier`. + * @param originDomain The domain of the origin chain. + * @param destinationDomain The domain of the destination chain. + * @param destinationGas The amount of gas to pay for on the destination chain. + * @returns An estimated amount of origin chain tokens to cover gas costs of the + * message on the destination chain. + */ + async estimatePaymentForGasAmount( + originDomain: number, + destinationDomain: number, + destinationGas: BigNumber, + ): Promise { + const destinationGasPrice = await this.suggestedGasPrice(destinationDomain); + const destinationCostWei = destinationGas.mul(destinationGasPrice); + + // Convert from destination domain native tokens to origin domain native tokens. + const originCostWei = await this.convertBetweenNativeTokens( + destinationDomain, + originDomain, + destinationCostWei, + ); + + // Applies a multiplier + return mulBigAndFixed( + originCostWei, + this.paymentEstimateMultiplier, + true, // ceil + ); + } + + /** + * Calculates the estimated payment to process the message on its destination chain, + * denominated in the native token of the origin chain. The destination gas is + * determined by estimating the gas to process the provided message, which is then used + * to calculate the payment using {@link estimatePaymentForGasAmount}. + * @param message The parsed message to estimate payment for. + * @returns An estimated amount of origin chain tokens to cover gas costs of the + * message on the destination chain. + */ + async estimatePaymentForMessage(message: ParsedMessage) { + const destinationGas = await this.estimateGasForMessage(message); + return this.estimatePaymentForGasAmount( + message.origin, + message.destination, + destinationGas, + ); + } + + /** + * Using the exchange rates provided by tokenPriceGetter, returns the amount of + * `toDomain` native tokens equivalent in value to the provided `fromAmount` of + * `fromDomain` native tokens. Accounts for differences in the decimals of the tokens. + * @param fromDomain The domain whose native token is being converted from. + * @param toDomain The domain whose native token is being converted into. + * @param fromAmount The amount of `fromDomain` native tokens to convert from. + * @returns The amount of `toDomain` native tokens whose value is equivalent to + * `fromAmount` of `fromDomain` native tokens. + */ + async convertBetweenNativeTokens( + fromDomain: number, + toDomain: number, + fromAmount: BigNumber, + ): Promise { + // A FixedNumber that doesn't care what the decimals of the from/to + // tokens are -- it is just the amount of whole from tokens that a single + // whole to token is equivalent in value to. + const exchangeRate = await this.getExchangeRate(toDomain, fromDomain); + + // Apply the exchange rate to the amount. This does not yet account for differences in + // decimals between the two tokens. + const exchangeRateProduct = mulBigAndFixed( + fromAmount, + exchangeRate, + true, // ceil + ); + + // Converts exchangeRateProduct to having the correct number of decimals. + return convertDecimalValue( + exchangeRateProduct, + this.nativeTokenDecimals(fromDomain), + this.nativeTokenDecimals(toDomain), + ); + } + + /** + * @param baseDomain The domain whose native token is the base asset. + * @param quoteDomain The domain whose native token is the quote asset. + * @returns The exchange rate of the native tokens of the baseDomain and the quoteDomain. + * I.e. the number of whole quote tokens a single whole base token is equivalent + * in value to. + */ + async getExchangeRate( + baseDomain: number, + quoteDomain: number, + ): Promise { + const baseUsd = await this.tokenPriceGetter.getNativeTokenUsdPrice( + baseDomain, + ); + const quoteUsd = await this.tokenPriceGetter.getNativeTokenUsdPrice( + quoteDomain, + ); + + // This operation is called "unsafe" because of the unintuitive rounding that + // can occur due to fixed point arithmetic. We're not overly concerned about perfect + // precision because we're operating with fixed128x18, which has 18 decimals of + // precision, and gas payments are regardless expected to have a generous buffer to account + // for movements in native token prices or gas prices. + // For more details on FixedPoint arithmetic being "unsafe", see + // https://github.com/ethers-io/ethers.js/issues/1322#issuecomment-787430115. + return quoteUsd.divUnsafe(baseUsd); + } + + /** + * Gets a suggested gas price for a domain. + * @param domain The domain of the chain to estimate gas prices for. + * @returns The suggested gas price in wei on the destination chain. + */ + async suggestedGasPrice(domain: number): Promise { + const provider = this.core.mustGetProvider(domain); + return provider.getGasPrice(); + } + + /** + * Gets the number of decimals of the provided domain's native token. + * @param domain The domain. + * @returns The number of decimals of `domain`'s native token. + */ + nativeTokenDecimals(domain: number) { + return ( + this.core.mustGetDomain(domain).nativeTokenDecimals ?? + DEFAULT_TOKEN_DECIMALS + ); + } + + /** + * Estimates the amount of gas required to process a message on its destination chain. + * This does not assume the Inbox of the destination domain has a checkpoint that + * the message is included in. Therefore, we estimate the gas by summing: + * 1. The intrinsic gas cost of a transaction on the destination chain. + * 2. Any gas costs imposed by operations in the Inbox, including proving + * the message and logic surrounding the processing of a message. + * 3. The estimated gas consumption of a direct call to the `handle` + * function of the recipient address using the correct parameters and + * setting the `from` address of the transaction to the address of the inbox. + * 4. A buffer to account for inaccuracies in the above estimations. + * @returns The estimated gas required to process the message on the destination chain. + */ + async estimateGasForMessage(message: ParsedMessage): Promise { + const provider = this.core.mustGetProvider(message.destination); + const inbox = this.core.mustGetInbox(message.origin, message.destination); + + const handlerInterface = new ethers.utils.Interface([ + 'function handle(uint32,bytes32,bytes)', + ]); + // Estimates a direct call to the `handle` function of the recipient + // with the `from` address set to the inbox. + // This includes intrinsic gas, so no need to add it + const directHandleCallGas = await provider.estimateGas({ + to: utils.bytes32ToAddress(message.recipient), + from: inbox.address, + data: handlerInterface.encodeFunctionData('handle', [ + message.origin, + message.sender, + message.body, + ]), + }); + + // directHandleCallGas includes the intrinsic gas + return directHandleCallGas + .add(this.inboxProvingAndProcessingGas) + .add(this.messageGasEstimateBuffer); + } + + /** + * @returns A generous estimation of the gas consumption of all prove and process + * operations in Inbox.sol, excluding: + * 1. Intrinsic gas. + * 2. Any gas consumed within a `handle` function when processing a message once called. + */ + get inboxProvingAndProcessingGas() { + // This does not consider that different domains can possibly have different gas costs. + // Consider this being configurable for each domain, or investigate ways to estimate + // this over RPC. + // + // This number was arrived at by estimating the proving and processing of a message + // whose recipient contract included only an empty fallback function. The estimated + // gas cost was ~100,000 which includes the intrinsic cost, but 150,000 is chosen as + // a generous buffer. + return 150_000; + } +} diff --git a/typescript/sdk/src/gas/index.ts b/typescript/sdk/src/gas/index.ts new file mode 100644 index 000000000..2f47e6d7c --- /dev/null +++ b/typescript/sdk/src/gas/index.ts @@ -0,0 +1,2 @@ +export { InterchainGasCalculator } from './calculator'; +export { DefaultTokenPriceGetter, TokenPriceGetter } from './token-prices'; diff --git a/typescript/sdk/src/gas/token-prices.ts b/typescript/sdk/src/gas/token-prices.ts new file mode 100644 index 000000000..cdf110337 --- /dev/null +++ b/typescript/sdk/src/gas/token-prices.ts @@ -0,0 +1,13 @@ +import { FixedNumber } from 'ethers'; +import { NameOrDomain } from '../types'; + +export interface TokenPriceGetter { + getNativeTokenUsdPrice(domain: NameOrDomain): Promise; +} + +// TODO implement in following PR +export class DefaultTokenPriceGetter implements TokenPriceGetter { + getNativeTokenUsdPrice(_domain: NameOrDomain): Promise { + return Promise.resolve(FixedNumber.from('12.34')); + } +} diff --git a/typescript/sdk/src/gas/utils.ts b/typescript/sdk/src/gas/utils.ts new file mode 100644 index 000000000..b8a6ea2a6 --- /dev/null +++ b/typescript/sdk/src/gas/utils.ts @@ -0,0 +1,64 @@ +import { BigNumber, FixedNumber } from 'ethers'; + +/** + * Converts a BigNumber to a FixedNumber of the format fixed128x18. + * @param big The BigNumber to convert. + * @returns A FixedNumber representation of a BigNumber. + */ +export function bigToFixed(big: BigNumber): FixedNumber { + return FixedNumber.from(big.toString()); +} + +/** + * Converts a FixedNumber (of any format) to a BigNumber. + * @param fixed The FixedNumber to convert. + * @param ceil If true, the ceiling of fixed is used. Otherwise, the floor is used. + * @returns A BigNumber representation of a FixedNumber. + */ +export function fixedToBig( + fixed: FixedNumber, + ceil: boolean = false, +): BigNumber { + const fixedAsInteger = ceil ? fixed.ceiling() : fixed.floor(); + return BigNumber.from(fixedAsInteger.toFormat('fixed256x0').toString()); +} + +/** + * Multiplies a BigNumber by a FixedNumber, returning the BigNumber product. + * @param big The BigNumber to multiply. + * @param fixed The FixedNumber to multiply. + * @param ceil If true, the ceiling of the product is used. Otherwise, the floor is used. + * @returns The BigNumber product. + */ +export function mulBigAndFixed( + big: BigNumber, + fixed: FixedNumber, + ceil: boolean = false, +): BigNumber { + // Converts big to a FixedNumber, multiplies it by fixed, and converts the product back + // to a BigNumber. + return fixedToBig(fixed.mulUnsafe(bigToFixed(big)), ceil); +} + +/** + * Converts a value with `fromDecimals` decimals to a value with `toDecimals` decimals. + * Incurs a loss of precision when `fromDecimals` > `toDecimals`. + * @param value The value to convert. + * @param fromDecimals The number of decimals `value` has. + * @param toDecimals The number of decimals to convert `value` to. + * @returns `value` represented with `toDecimals` decimals. + */ +export function convertDecimalValue( + value: BigNumber, + fromDecimals: number, + toDecimals: number, +): BigNumber { + if (fromDecimals === toDecimals) { + return value; + } else if (fromDecimals > toDecimals) { + return value.div(10 ** (fromDecimals - toDecimals)); + } else { + // if (fromDecimals < toDecimals) + return value.mul(10 ** (toDecimals - fromDecimals)); + } +} diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 0f12cd486..81c3eac05 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -9,6 +9,8 @@ export { CoreContracts, CoreContractAddresses, MessageStatus, + ParsedMessage, + parseMessage, } from './core'; export { Annotated, @@ -33,4 +35,9 @@ export { export { AbacusAppContracts } from './contracts'; export { AbacusApp } from './app'; export { domains } from './domains'; +export { + DefaultTokenPriceGetter, + InterchainGasCalculator, + TokenPriceGetter, +} from './gas'; export { utils } from './utils'; diff --git a/typescript/sdk/src/types.ts b/typescript/sdk/src/types.ts index e33fc85d1..753605598 100644 --- a/typescript/sdk/src/types.ts +++ b/typescript/sdk/src/types.ts @@ -46,6 +46,7 @@ export type ChainName = MainnetChainNames | TestnetChainNames | TestChainNames; export interface Domain { id: number; name: ChainName; + nativeTokenDecimals?: number; paginate?: Pagination; } diff --git a/typescript/sdk/test/gas/calculator.test.ts b/typescript/sdk/test/gas/calculator.test.ts new file mode 100644 index 000000000..404c16841 --- /dev/null +++ b/typescript/sdk/test/gas/calculator.test.ts @@ -0,0 +1,148 @@ +import { expect } from 'chai'; +import { BigNumber, ethers, FixedNumber } from 'ethers'; + +import { utils } from '@abacus-network/utils'; + +import { AbacusCore, InterchainGasCalculator, ParsedMessage } from '../..'; +import { MockProvider, MockTokenPriceGetter, testAddresses } from '../utils'; + +describe('InterchainGasCalculator', () => { + const originDomain = 1; + const destinationDomain = 2; + + let core: AbacusCore; + let provider: MockProvider; + let tokenPriceGetter: MockTokenPriceGetter; + let calculator: InterchainGasCalculator; + + before(() => { + core = new AbacusCore(testAddresses); + provider = new MockProvider(); + core.registerProvider('test1', provider); + core.registerProvider('test2', provider); + + tokenPriceGetter = new MockTokenPriceGetter(); + // Origin domain token + tokenPriceGetter.setTokenPrice(originDomain, 10); + // Destination domain token + tokenPriceGetter.setTokenPrice(destinationDomain, 5); + }); + + beforeEach(() => { + calculator = new InterchainGasCalculator(core, { + tokenPriceGetter, + }); + }); + + afterEach(() => { + provider.clearMethodResolveValues(); + }); + + describe('estimatePaymentForGasAmount', () => { + it('estimates origin token payment from a specified destination gas amount', async () => { + const destinationGas = BigNumber.from(100_000); + + // Set destination gas price to 10 wei + provider.setMethodResolveValue('getGasPrice', BigNumber.from(10)); + + // Set paymentEstimateMultiplier to 1 just to test easily + calculator.paymentEstimateMultiplier = FixedNumber.from(1); + + const estimatedPayment = await calculator.estimatePaymentForGasAmount( + originDomain, + destinationDomain, + destinationGas, + ); + + // 100_000 dest gas * 10 gas price * ($5 per origin token / $10 per origin token) + expect(estimatedPayment.toNumber()).to.equal(500_000); + }); + }); + + describe('estimatePaymentForMessage', () => { + it('estimates origin token payment from a specified message', async () => { + // Set the estimated destination gas + const estimatedDestinationGas = 100_000; + calculator.estimateGasForMessage = () => Promise.resolve(BigNumber.from(estimatedDestinationGas)); + // Set destination gas price to 10 wei + calculator.suggestedGasPrice = (_) => Promise.resolve(BigNumber.from(10)); + // Set paymentEstimateMultiplier to 1 just to test easily + calculator.paymentEstimateMultiplier = FixedNumber.from(1); + + const zeroAddressBytes32 = utils.addressToBytes32(ethers.constants.AddressZero); + const message: ParsedMessage = { + origin: originDomain, + sender: zeroAddressBytes32, + nonce: 0, + destination: destinationDomain, + recipient: zeroAddressBytes32, + body: '0x12345678', + }; + + const estimatedPayment = await calculator.estimatePaymentForMessage(message); + + // 100_000 dest gas * 10 gas price * ($5 per origin token / $10 per origin token) + expect(estimatedPayment.toNumber()).to.equal(500_000); + }); + }); + + describe('convertBetweenNativeTokens', () => { + it('converts using the USD value of origin and destination native tokens', async () => { + const destinationWei = BigNumber.from('1000'); + const originWei = await calculator.convertBetweenNativeTokens( + destinationDomain, + originDomain, + destinationWei, + ); + + expect(originWei.toNumber()).to.equal(500); + }); + + it('considers when the origin token decimals > the destination token decimals', async () => { + calculator.nativeTokenDecimals = (domain: number) => { + if (domain === originDomain) { + return 20; + } + return 18; + }; + + const destinationWei = BigNumber.from('1000'); + const originWei = await calculator.convertBetweenNativeTokens( + destinationDomain, + originDomain, + destinationWei, + ); + + expect(originWei.toNumber()).to.equal(50000); + }); + + it('considers when the origin token decimals < the destination token decimals', async () => { + calculator.nativeTokenDecimals = (domain: number) => { + if (domain === originDomain) { + return 16; + } + return 18; + }; + + const destinationWei = BigNumber.from('1000'); + const originWei = await calculator.convertBetweenNativeTokens( + destinationDomain, + originDomain, + destinationWei, + ); + + expect(originWei.toNumber()).to.equal(5); + }) + }); + + describe('suggestedGasPrice', () => { + it('gets the gas price from the provider', async () => { + const gasPrice = 1000; + provider.setMethodResolveValue('getGasPrice', BigNumber.from(gasPrice)); + + expect( + (await calculator.suggestedGasPrice(destinationDomain)).toNumber() + ).to.equal(gasPrice); + }); + }); +}); diff --git a/typescript/sdk/test/utils.test.ts b/typescript/sdk/test/utils.test.ts new file mode 100644 index 000000000..03f7c191b --- /dev/null +++ b/typescript/sdk/test/utils.test.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; +import { BigNumber, FixedNumber } from 'ethers'; + +import { bigToFixed, fixedToBig, mulBigAndFixed } from '../src/gas/utils'; + +describe('utils', () => { + describe('bigToFixed', () => { + it('converts a BigNumber to a FixedNumber', () => { + const big = BigNumber.from('1234'); + const fixed = bigToFixed(big); + + expect(fixed.toUnsafeFloat()).to.equal(1234); + }); + }); + + describe('fixedToBig', () => { + it('converts a FixedNumber to a floored BigNumber', () => { + const fixed = FixedNumber.from('12.34'); + const big = fixedToBig(fixed); + + expect(big.toNumber()).to.equal(12); + }); + + it('converts a FixedNumber to a ceilinged BigNumber', () => { + const fixed = FixedNumber.from('12.34'); + const big = fixedToBig(fixed, true); + + expect(big.toNumber()).to.equal(13); + }); + }); + + describe('mulBigAndFixed', () => { + it('gets the floored product of a BigNumber and FixedNumber', () => { + const big = BigNumber.from('1000'); + const fixed = FixedNumber.from('1.2345'); + const product = mulBigAndFixed(big, fixed); + + expect(product.toNumber()).to.equal(1234); + }); + + it('gets the ceilinged product of a BigNumber and FixedNumber', () => { + const big = BigNumber.from('1000'); + const fixed = FixedNumber.from('1.2345'); + const product = mulBigAndFixed(big, fixed, true); + + expect(product.toNumber()).to.equal(1235); + }); + }); +}); diff --git a/typescript/sdk/test/utils.ts b/typescript/sdk/test/utils.ts new file mode 100644 index 000000000..e299cab5e --- /dev/null +++ b/typescript/sdk/test/utils.ts @@ -0,0 +1,103 @@ +import { ethers, FixedNumber } from 'ethers'; +import { NameOrDomain } from '../src'; + +const ZERO_ADDRESS = ethers.constants.AddressZero; + +export const testAddresses = { + test1: { + upgradeBeaconController: ZERO_ADDRESS, + xAppConnectionManager: ZERO_ADDRESS, + validatorManager: ZERO_ADDRESS, + interchainGasPaymaster: ZERO_ADDRESS, + outbox: { + proxy: ZERO_ADDRESS, + implementation: ZERO_ADDRESS, + beacon: ZERO_ADDRESS, + }, + inboxes: { + test2: { + proxy: ZERO_ADDRESS, + implementation: ZERO_ADDRESS, + beacon: ZERO_ADDRESS, + }, + }, + }, + test2: { + upgradeBeaconController: ZERO_ADDRESS, + xAppConnectionManager: ZERO_ADDRESS, + validatorManager: ZERO_ADDRESS, + interchainGasPaymaster: ZERO_ADDRESS, + outbox: { + proxy: ZERO_ADDRESS, + implementation: ZERO_ADDRESS, + beacon: ZERO_ADDRESS, + }, + inboxes: { + test1: { + proxy: ZERO_ADDRESS, + implementation: ZERO_ADDRESS, + beacon: ZERO_ADDRESS, + }, + }, + }, +}; + +const MOCK_NETWORK = { + name: 'MockNetwork', + chainId: 1337, +}; + +// A mock ethers Provider used for testing with mocked provider functionality +export class MockProvider extends ethers.providers.BaseProvider { + + private methodResolveValues: { [key: string]: any }; + + constructor() { + super(MOCK_NETWORK); + + this.methodResolveValues = {}; + } + + // Required to be implemented or the BaseProvider throws + async detectNetwork() { + return Promise.resolve(MOCK_NETWORK); + } + + perform(method: string, params: any): Promise { + const value = this.methodResolveValues[method] + if (value) { + return Promise.resolve(value); + } + + return super.perform(method, params); + } + + setMethodResolveValue(method: string, value: any) { + this.methodResolveValues[method] = value; + } + + clearMethodResolveValues() { + this.methodResolveValues = {}; + } +} + +// A mock TokenPriceGetter intended to be used by tests when mocking token prices +export class MockTokenPriceGetter { + private tokenPrices: { [domain: number]: FixedNumber } + + constructor() { + this.tokenPrices = {}; + } + + getNativeTokenUsdPrice(domain: NameOrDomain): Promise { + const price = this.tokenPrices[domain as number]; + if (price) { + return Promise.resolve(price); + } + throw Error(`No price for domain ${domain}`); + } + + setTokenPrice(domain: number, price: string | number) { + this.tokenPrices[domain] = FixedNumber.from(price); + } +} diff --git a/typescript/sdk/tsconfig.json b/typescript/sdk/tsconfig.json index 297e8273c..21c127ea1 100644 --- a/typescript/sdk/tsconfig.json +++ b/typescript/sdk/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "./dist/", "rootDir": "./src/" }, - "exclude": ["./node_modules/", "./dist/"], + "exclude": ["./node_modules/", "./dist/", "./test/"], "include": [ "./src/*.ts", "./src/**/*.ts" diff --git a/typescript/utils/src/utils.ts b/typescript/utils/src/utils.ts index 1c81c0a85..b64ec290c 100644 --- a/typescript/utils/src/utils.ts +++ b/typescript/utils/src/utils.ts @@ -34,6 +34,10 @@ export function addressToBytes32(address: Address): string { .toLowerCase(); } +export function bytes32ToAddress(bytes32: string): Address { + return ethers.utils.getAddress(bytes32.slice(-40)); +} + export const formatMessage = ( localDomain: Domain, senderAddr: Address,