diff --git a/package-lock.json b/package-lock.json index 51cdd6c99..73ef2269d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,12 +22,13 @@ "@types/lodash.camelcase": "^4.3.7", "@types/lodash.snakecase": "^4.1.7", "await-semaphore": "^0.1.3", + "body-parser": "^1.20.2", "cartonne": "^2.0.1", "conditional-type-checks": "^1.0.6", "cors": "^2.8.5", "dag-jose": "^4.0.0", "dotenv": "^16.0.3", - "ethers": "~5.7.2", + "ethers": "^6.2.3", "express": "^4.18.1", "fp-ts": "^2.13.1", "http-status-codes": "^2.2.0", @@ -38,10 +39,12 @@ "lodash.clonedeep": "^4.5.0", "lodash.snakecase": "^4.1.1", "lru-cache": "^7.17.0", + "merge-options": "^3.0.4", "morgan": "^1.10.0", "multiformats": "^11.0.1", "os-utils": "^0.0.14", "pg": "^8.8.0", + "prom-client": "^14.2.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.5.2", "tsm": "^2.2.2", @@ -233,6 +236,11 @@ "node": ">=4.0" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.0.tgz", + "integrity": "sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ==" + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -3494,29 +3502,6 @@ "hash.js": "1.1.7" } }, - "node_modules/@ethersproject/solidity": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz", - "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/sha2": "^5.7.0", - "@ethersproject/strings": "^5.7.0" - } - }, "node_modules/@ethersproject/strings": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", @@ -3563,26 +3548,6 @@ "@ethersproject/signing-key": "^5.7.0" } }, - "node_modules/@ethersproject/units": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz", - "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/logger": "^5.7.0" - } - }, "node_modules/@ethersproject/wallet": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", @@ -6074,7 +6039,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "dev": true, "funding": [ { "type": "individual", @@ -9077,8 +9041,7 @@ "node_modules/bintrees": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", - "dev": true + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" }, "node_modules/bip32": { "version": "2.0.5", @@ -9210,12 +9173,12 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -9223,7 +9186,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -12874,13 +12837,13 @@ } }, "node_modules/ethers": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", - "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.2.3.tgz", + "integrity": "sha512-l1Z/Yr+HrOk+7LTeYRHGMvYwVLGpTuVrT/kJ7Kagi3nekGISYILIby0f1ipV9BGzgERyy+w4emH+d3PhhcxIfA==", "funding": [ { "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + "url": "https://github.com/sponsors/ethers-io/" }, { "type": "individual", @@ -12888,36 +12851,56 @@ } ], "dependencies": { - "@ethersproject/abi": "5.7.0", - "@ethersproject/abstract-provider": "5.7.0", - "@ethersproject/abstract-signer": "5.7.0", - "@ethersproject/address": "5.7.0", - "@ethersproject/base64": "5.7.0", - "@ethersproject/basex": "5.7.0", - "@ethersproject/bignumber": "5.7.0", - "@ethersproject/bytes": "5.7.0", - "@ethersproject/constants": "5.7.0", - "@ethersproject/contracts": "5.7.0", - "@ethersproject/hash": "5.7.0", - "@ethersproject/hdnode": "5.7.0", - "@ethersproject/json-wallets": "5.7.0", - "@ethersproject/keccak256": "5.7.0", - "@ethersproject/logger": "5.7.0", - "@ethersproject/networks": "5.7.1", - "@ethersproject/pbkdf2": "5.7.0", - "@ethersproject/properties": "5.7.0", - "@ethersproject/providers": "5.7.2", - "@ethersproject/random": "5.7.0", - "@ethersproject/rlp": "5.7.0", - "@ethersproject/sha2": "5.7.0", - "@ethersproject/signing-key": "5.7.0", - "@ethersproject/solidity": "5.7.0", - "@ethersproject/strings": "5.7.0", - "@ethersproject/transactions": "5.7.0", - "@ethersproject/units": "5.7.0", - "@ethersproject/wallet": "5.7.0", - "@ethersproject/web": "5.7.1", - "@ethersproject/wordlists": "5.7.0" + "@adraffy/ens-normalize": "1.9.0", + "@noble/hashes": "1.1.2", + "@noble/secp256k1": "1.7.1", + "aes-js": "4.0.0-beta.3", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", + "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/ethers/node_modules/aes-js": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.3.tgz", + "integrity": "sha512-/xJX0/VTPcbc5xQE2VUP91y1xN8q/rDfhEzLm+vLc3hYvb5+qHCnpJRuFcrKn63zumK/sCwYYzhG8HP78JYSTA==" + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/ethr-did-registry": { @@ -13142,6 +13125,29 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -13155,6 +13161,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -14083,6 +14103,7 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz", "integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==", "dev": true, + "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -14440,6 +14461,7 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz", "integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==", "dev": true, + "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -23965,10 +23987,9 @@ "dev": true }, "node_modules/prom-client": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.1.tgz", - "integrity": "sha512-hFU32q7UZQ59bVJQGUtm3I2PrJ3gWvoCkilX9sF165ks1qflhugVCeK+S1JjJYHvyt3o5kj68+q3bchormjnzw==", - "dev": true, + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz", + "integrity": "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==", "dependencies": { "tdigest": "^0.1.1" }, @@ -24388,9 +24409,9 @@ "dev": true }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -26652,7 +26673,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", - "dev": true, "dependencies": { "bintrees": "1.0.2" } @@ -28156,6 +28176,11 @@ } } }, + "@adraffy/ens-normalize": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.0.tgz", + "integrity": "sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ==" + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -30552,19 +30577,6 @@ "hash.js": "1.1.7" } }, - "@ethersproject/solidity": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz", - "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==", - "requires": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/sha2": "^5.7.0", - "@ethersproject/strings": "^5.7.0" - } - }, "@ethersproject/strings": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", @@ -30591,16 +30603,6 @@ "@ethersproject/signing-key": "^5.7.0" } }, - "@ethersproject/units": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz", - "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==", - "requires": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/logger": "^5.7.0" - } - }, "@ethersproject/wallet": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", @@ -32564,8 +32566,7 @@ "@noble/secp256k1": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "dev": true + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -34982,8 +34983,7 @@ "bintrees": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", - "dev": true + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" }, "bip32": { "version": "2.0.5", @@ -35100,12 +35100,12 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -35113,7 +35113,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -37804,40 +37804,39 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "ethers": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", - "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", - "requires": { - "@ethersproject/abi": "5.7.0", - "@ethersproject/abstract-provider": "5.7.0", - "@ethersproject/abstract-signer": "5.7.0", - "@ethersproject/address": "5.7.0", - "@ethersproject/base64": "5.7.0", - "@ethersproject/basex": "5.7.0", - "@ethersproject/bignumber": "5.7.0", - "@ethersproject/bytes": "5.7.0", - "@ethersproject/constants": "5.7.0", - "@ethersproject/contracts": "5.7.0", - "@ethersproject/hash": "5.7.0", - "@ethersproject/hdnode": "5.7.0", - "@ethersproject/json-wallets": "5.7.0", - "@ethersproject/keccak256": "5.7.0", - "@ethersproject/logger": "5.7.0", - "@ethersproject/networks": "5.7.1", - "@ethersproject/pbkdf2": "5.7.0", - "@ethersproject/properties": "5.7.0", - "@ethersproject/providers": "5.7.2", - "@ethersproject/random": "5.7.0", - "@ethersproject/rlp": "5.7.0", - "@ethersproject/sha2": "5.7.0", - "@ethersproject/signing-key": "5.7.0", - "@ethersproject/solidity": "5.7.0", - "@ethersproject/strings": "5.7.0", - "@ethersproject/transactions": "5.7.0", - "@ethersproject/units": "5.7.0", - "@ethersproject/wallet": "5.7.0", - "@ethersproject/web": "5.7.1", - "@ethersproject/wordlists": "5.7.0" + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.2.3.tgz", + "integrity": "sha512-l1Z/Yr+HrOk+7LTeYRHGMvYwVLGpTuVrT/kJ7Kagi3nekGISYILIby0f1ipV9BGzgERyy+w4emH+d3PhhcxIfA==", + "requires": { + "@adraffy/ens-normalize": "1.9.0", + "@noble/hashes": "1.1.2", + "@noble/secp256k1": "1.7.1", + "aes-js": "4.0.0-beta.3", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", + "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==" + }, + "aes-js": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.3.tgz", + "integrity": "sha512-/xJX0/VTPcbc5xQE2VUP91y1xN8q/rDfhEzLm+vLc3hYvb5+qHCnpJRuFcrKn63zumK/sCwYYzhG8HP78JYSTA==" + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "requires": {} + } } }, "ethr-did-registry": { @@ -38019,6 +38018,25 @@ "vary": "~1.1.2" }, "dependencies": { + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -38031,6 +38049,17 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } } } }, @@ -46188,10 +46217,9 @@ "dev": true }, "prom-client": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.1.tgz", - "integrity": "sha512-hFU32q7UZQ59bVJQGUtm3I2PrJ3gWvoCkilX9sF165ks1qflhugVCeK+S1JjJYHvyt3o5kj68+q3bchormjnzw==", - "dev": true, + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz", + "integrity": "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==", "requires": { "tdigest": "^0.1.1" } @@ -46533,9 +46561,9 @@ "dev": true }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -48165,7 +48193,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", - "dev": true, "requires": { "bintrees": "1.0.2" } diff --git a/package.json b/package.json index 1d51da5ad..baa1b0cf4 100644 --- a/package.json +++ b/package.json @@ -59,12 +59,13 @@ "@types/lodash.camelcase": "^4.3.7", "@types/lodash.snakecase": "^4.1.7", "await-semaphore": "^0.1.3", + "body-parser": "^1.20.2", "cartonne": "^2.0.1", "conditional-type-checks": "^1.0.6", "cors": "^2.8.5", "dag-jose": "^4.0.0", "dotenv": "^16.0.3", - "ethers": "~5.7.2", + "ethers": "^6.2.3", "express": "^4.18.1", "fp-ts": "^2.13.1", "http-status-codes": "^2.2.0", @@ -75,9 +76,11 @@ "lodash.clonedeep": "^4.5.0", "lodash.snakecase": "^4.1.1", "lru-cache": "^7.17.0", + "merge-options": "^3.0.4", "morgan": "^1.10.0", "multiformats": "^11.0.1", "os-utils": "^0.0.14", + "prom-client": "^14.2.0", "pg": "^8.8.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.5.2", diff --git a/src/__tests__/ceramic_integration.test.ts b/src/__tests__/ceramic_integration.test.ts index df643abb9..99c9a8f37 100644 --- a/src/__tests__/ceramic_integration.test.ts +++ b/src/__tests__/ceramic_integration.test.ts @@ -49,6 +49,10 @@ process.env.NODE_ENV = 'test' const randomNumber = Math.floor(Math.random() * 10000) const TOPIC = `/ceramic/local/${randomNumber}` +// Workaround from https://stackoverflow.com/a/72416352/599991 +import dns from 'node:dns' +dns.setDefaultResultOrder('ipv4first') + /** * Create an IPFS instance */ @@ -90,7 +94,7 @@ async function makeCeramicCore( anchorServiceUrl, // TODO CDB-2317 Remove `indexing` config when Ceramic Core allows that indexing: { - db: "TODO", + db: 'TODO', allowQueriesBeforeHistoricalSync: false, disableComposedb: true, enableHistoricalSync: false, diff --git a/src/auth/index.ts b/src/auth/index.ts index 7cccf41a7..25767c830 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -16,50 +16,53 @@ function buildExpressMiddleware() { * Notice that the absense of a did header or body bypasses any checks below * this app will still work if the logice above is not in place. */ - return function(req: Request, _res: Response, next: NextFunction) { - if (req.headers) { - if (req.headers['did'] && req.body) { - if (Object.keys(req.body).length > 0) { - const digest = buildBodyDigest(req.headers['content-type'], req.body) - if (req.headers['digest'] == digest) { - return next() - } else { - throw Error('Body digest verification failed') - } - } - } + return function (req: Request, _res: Response, next: NextFunction) { + if (req.headers) { + if (req.headers['did'] && req.body) { + if (Object.keys(req.body).length > 0) { + const digest = buildBodyDigest(req.headers['content-type'], req.body) + if (req.headers['digest'] == digest) { + return next() + } else { + throw Error('Body digest verification failed') + } } - return next() + } } + return next() + } } function buildBodyDigest(contentType: string | undefined, body: any): string | undefined { - if (!body) return + if (!body) return - let hash: Uint8Array | undefined + let hash: Uint8Array | undefined - if (contentType) { - if (contentType.includes('application/vnd.ipld.car')) { - const carFactory = new CARFactory() - carFactory.codecs.add(DAG_JOSE) - console.log('Will build a car file from req.body', body) - try { - console.log('Will build a car file from req.body (as utf8 string)', u8a.toString(body, 'base64')) - } catch(e) { - console.log('Couldn\'t convert req.body to string: ', e) - } - const car = carFactory.fromBytes(body) - if (!car.roots[0]) throw Error('Missing CAR root') - return car.roots[0].toString() - } else if (contentType.includes('application/json')) { - hash = sha256.hash(u8a.fromString(JSON.stringify(body))) + if (contentType) { + if (contentType.includes('application/vnd.ipld.car')) { + const carFactory = new CARFactory() + carFactory.codecs.add(DAG_JOSE) + console.log('Will build a car file from req.body', body) + try { + console.log( + 'Will build a car file from req.body (as utf8 string)', + u8a.toString(body, 'base64') + ) + } catch (e) { + console.log("Couldn't convert req.body to string: ", e) } - } - - if (!hash) { - // Default to hashing stringified body + const car = carFactory.fromBytes(body) + if (!car.roots[0]) throw Error('Missing CAR root') + return car.roots[0].toString() + } else if (contentType.includes('application/json')) { hash = sha256.hash(u8a.fromString(JSON.stringify(body))) } + } - return `0x${u8a.toString(hash, 'base16')}` + if (!hash) { + // Default to hashing stringified body + hash = sha256.hash(u8a.fromString(JSON.stringify(body))) } + + return `0x${u8a.toString(hash, 'base16')}` +} diff --git a/src/models/transaction.ts b/src/models/transaction.ts index 9c1f517dd..270ae726d 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -2,12 +2,12 @@ export class Transaction { chain: string txHash: string blockNumber: number - blockTimestamp: number + blockTimestamp: Date - constructor(chain: string, txHash: string, blockNumber: number, blockTimestamp: number) { + constructor(chain: string, txHash: string, blockNumber: number, blockDate: Date) { this.chain = chain this.txHash = txHash this.blockNumber = blockNumber - this.blockTimestamp = blockTimestamp + this.blockTimestamp = blockDate } } diff --git a/src/server.ts b/src/server.ts index 07d285d4e..44c066314 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,9 +15,11 @@ export class CeramicAnchorServer extends Server { super(true) this.app.set('trust proxy', true) - this.app.use(bodyParser.raw({inflate: true, type: 'application/vnd.ipld.car'})) + this.app.use(bodyParser.raw({ inflate: true, type: 'application/vnd.ipld.car' })) this.app.use(bodyParser.json({ type: 'application/json' })) - this.app.use(bodyParser.urlencoded({ extended: true, type: 'application/x-www-form-urlencoded' })) + this.app.use( + bodyParser.urlencoded({ extended: true, type: 'application/x-www-form-urlencoded' }) + ) this.app.use(expressLoggers) if (config.requireAuth == true) { this.app.use(auth) diff --git a/src/services/__tests__/anchor-service.test.ts b/src/services/__tests__/anchor-service.test.ts index 8ded4516e..7e1d24e01 100644 --- a/src/services/__tests__/anchor-service.test.ts +++ b/src/services/__tests__/anchor-service.test.ts @@ -25,7 +25,7 @@ import { Utils } from '../../utils.js' import { validate as validateUUID } from 'uuid' import { TransactionRepository } from '../../repositories/transaction-repository.js' import type { BlockchainService } from '../blockchain/blockchain-service' -import type { Transaction } from '../../models/transaction.js' +import { Transaction } from '../../models/transaction.js' import { createInjector, Injector } from 'typed-inject' import { MetadataRepository } from '../../repositories/metadata-repository.js' import { randomString } from '@stablelib/random' @@ -43,8 +43,10 @@ export class MockEventProducerService implements EventProducerService { } } +const CHAIN_ID = 'impossible' + class FakeEthereumBlockchainService implements BlockchainService { - chainId = 'impossible' + chainId = CHAIN_ID connect(): Promise { throw new Error(`Failed to connect`) @@ -799,3 +801,148 @@ describe('anchor service', () => { }) }) }) + +const FAKE_TRANSACTION = new Transaction( + CHAIN_ID, + 'impossible', + 1, + new Date(Date.UTC(2000, 1, 1, 1, 1, 1, 0)) +) + +class FakeTransactionEthereumBlockchainService implements BlockchainService { + chainId = CHAIN_ID + + connect(): Promise { + return Promise.resolve() + } + + sendTransaction(): Promise { + return Promise.resolve(FAKE_TRANSACTION) + } +} + +describe('anchor service with fake transaction', () => { + jest.setTimeout(10000) + let ipfsService: IIpfsService + let metadataService: IMetadataService + let connection: Knex + let injector: Injector + let requestRepository: RequestRepository + let anchorService: AnchorService + + beforeAll(async () => { + connection = await createDbConnection() + injector = createInjector() + .provideValue('dbConnection', connection) + .provideValue( + 'config', + Object.assign({}, config, { + merkleDepthLimit: MERKLE_DEPTH_LIMIT, + minStreamCount: MIN_STREAM_COUNT, + readyRetryIntervalMS: READY_RETRY_INTERVAL_MS, + }) + ) + .provideClass('anchorRepository', AnchorRepository) + .provideClass('metadataRepository', MetadataRepository) + .provideFactory('requestRepository', RequestRepository.make) + .provideClass('transactionRepository', TransactionRepository) + .provideClass('blockchainService', FakeTransactionEthereumBlockchainService) + .provideClass('ipfsService', MockIpfsService) + .provideClass('eventProducerService', MockEventProducerService) + .provideClass('metadataService', MetadataService) + .provideClass('anchorService', AnchorService) + + ipfsService = injector.resolve('ipfsService') + await ipfsService.init() + requestRepository = injector.resolve('requestRepository') + anchorService = injector.resolve('anchorService') + metadataService = injector.resolve('metadataService') + }) + + beforeEach(async () => { + await clearTables(connection) + jest.restoreAllMocks() + await requestRepository.table.delete() + }) + + afterAll(async () => { + await connection.destroy() + }) + + test('create anchor records', async () => { + // Create pending requests + const requests: Request[] = [] + const numRequests = 4 + for (let i = 0; i < numRequests; i++) { + const genesisCID = await ipfsService.storeRecord({ + header: { + controllers: [`did:method:${randomString(32)}`], + }, + }) + const streamId = new StreamID(1, genesisCID) + await metadataService.fillFromIpfs(streamId) + const request = await createRequest(streamId.toString(), ipfsService, requestRepository) + requests.push(request) + } + requests.sort(function (a, b) { + return a.streamId.localeCompare(b.streamId) + }) + + await requestRepository.findAndMarkReady(0) + + const storeSpy = jest.spyOn(ipfsService, 'storeRecord') + + await anchorService.anchorRequests() + + expect(storeSpy).toBeCalledTimes(9) + expect(storeSpy).nthCalledWith( + 5, + expect.objectContaining({ + chainId: FAKE_TRANSACTION.chain, + blockNumber: FAKE_TRANSACTION.blockNumber, + blockTimestamp: 949366861000, + }) + ) + + const publishSpy = jest.spyOn(ipfsService, 'publishAnchorCommit') + const [candidates] = await anchorService._findCandidates(requests, 0) + const merkleTree = await anchorService._buildMerkleTree(candidates) + const ipfsProofCid = await ipfsService.storeRecord({}) + + const anchors = await anchorService._createAnchorCommits(ipfsProofCid, merkleTree) + + expect(candidates.length).toEqual(requests.length) + expect(anchors.length).toEqual(candidates.length) + + expect(publishSpy).toBeCalledTimes(anchors.length) + + // All requests are anchored, in a different order because of IpfsLeafCompare + expect(anchors.map((a) => a.requestId).sort()).toEqual(requests.map((r) => r.id).sort()) + for (const i in anchors) { + const anchor = anchors[i] + expectPresent(anchor) + expect(anchor.proofCid).toEqual(ipfsProofCid.toString()) + const request = requests.find((r) => r.id === anchor.requestId) + expectPresent(request) + expect(anchor.requestId).toEqual(request.id) + + const anchorRecord = await ipfsService.retrieveRecord(anchor.cid) + expect(anchorRecord.prev.toString()).toEqual(request.cid) + expect(anchorRecord.proof).toEqual(ipfsProofCid) + expect(anchorRecord.path).toEqual(anchor.path) + expect(publishSpy.mock.calls[i]).toEqual([ + anchorRecord, + StreamID.fromString(request.streamId), + ]) + } + + expectPresent(anchors[0]) + expect(anchors[0].path).toEqual('0/0') + expectPresent(anchors[1]) + expect(anchors[1].path).toEqual('0/1') + expectPresent(anchors[2]) + expect(anchors[2].path).toEqual('1/0') + expectPresent(anchors[3]) + expect(anchors[3].path).toEqual('1/1') + }) +}) diff --git a/src/services/anchor-service.ts b/src/services/anchor-service.ts index af209ee62..524862dde 100644 --- a/src/services/anchor-service.ts +++ b/src/services/anchor-service.ts @@ -404,7 +404,7 @@ export class AnchorService { if (this.includeBlockInfoInAnchorProof) { ipfsAnchorProof = { blockNumber: tx.blockNumber, - blockTimestamp: tx.blockTimestamp, + blockTimestamp: tx.blockTimestamp.getTime(), ...ipfsAnchorProof, } } diff --git a/src/services/blockchain/__tests__/__snapshots__/eth-bc-service.test.ts.snap b/src/services/blockchain/__tests__/__snapshots__/eth-bc-service.test.ts.snap index ac9314226..3a4bd2cfc 100644 --- a/src/services/blockchain/__tests__/__snapshots__/eth-bc-service.test.ts.snap +++ b/src/services/blockchain/__tests__/__snapshots__/eth-bc-service.test.ts.snap @@ -2,8 +2,10 @@ exports[`ETH service connected to ganache v0 should send CID to local ganache server 1`] = ` Transaction { + "blockNumber": 3, + "blockTimestamp": 2020-04-13T13:20:08.000Z, "chain": "eip155:1337", - "txHash": "0xf7f830ef89d55af5900d48db106d5bcfbc045aec70b29cde019d2cd1e58a7827", + "txHash": "0xd6e8a8fab6097270c8a52a85db47ef8c2ba1b9b60db76435f5d8a8a3a1d598be", } `; @@ -20,18 +22,9 @@ exports[`ETH service with mock wallet insufficient funds error 1`] = ` { "data": "0x0f017112205d7fcd1a0999befdb062e6762c1f0f902f729b98304a2ef539412f53360d3d6a", "from": "abcd1234", - "gasLimit": { - "hex": "0x2710", - "type": "BigNumber", - }, - "maxFeePerGas": { - "hex": "0x0834", - "type": "BigNumber", - }, - "maxPriorityFeePerGas": { - "hex": "0x044c", - "type": "BigNumber", - }, + "gasLimit": 10000n, + "maxFeePerGas": 2431n, + "maxPriorityFeePerGas": 1210n, "nonce": 5, "to": "abcd1234", } @@ -41,18 +34,9 @@ exports[`ETH service with mock wallet insufficient funds error 2`] = ` { "data": "0x0f017112205d7fcd1a0999befdb062e6762c1f0f902f729b98304a2ef539412f53360d3d6a", "from": "abcd1234", - "gasLimit": { - "hex": "0x2710", - "type": "BigNumber", - }, - "maxFeePerGas": { - "hex": "0x0834", - "type": "BigNumber", - }, - "maxPriorityFeePerGas": { - "hex": "0x044c", - "type": "BigNumber", - }, + "gasLimit": 10000n, + "maxFeePerGas": 2431n, + "maxPriorityFeePerGas": 1210n, "nonce": 5, "to": "abcd1234", } @@ -70,7 +54,7 @@ exports[`ETH service with mock wallet single transaction attempt 1`] = ` exports[`ETH service with mock wallet single transaction attempt 2`] = ` Transaction { "blockNumber": 54321, - "blockTimestamp": 54321000, + "blockTimestamp": 2000-02-01T00:00:00.000Z, "chain": "eip155:1337", "txHash": "0x12345abcde", } @@ -79,14 +63,8 @@ Transaction { exports[`ETH service with mock wallet single transaction attempt 3`] = ` { "data": "0x987654321", - "gasLimit": { - "hex": "0x2710", - "type": "BigNumber", - }, - "gasPrice": { - "hex": "0x03e8", - "type": "BigNumber", - }, + "gasLimit": 10000n, + "gasPrice": 1000n, "nonce": 5, "to": "abcd1234", } @@ -96,18 +74,9 @@ exports[`ETH service with mock wallet successful mocked transaction 1`] = ` { "data": "0x0f017112205d7fcd1a0999befdb062e6762c1f0f902f729b98304a2ef539412f53360d3d6a", "from": "abcd1234", - "gasLimit": { - "hex": "0x2710", - "type": "BigNumber", - }, - "maxFeePerGas": { - "hex": "0x07d0", - "type": "BigNumber", - }, - "maxPriorityFeePerGas": { - "hex": "0x03e8", - "type": "BigNumber", - }, + "gasLimit": 10000n, + "maxFeePerGas": 2310n, + "maxPriorityFeePerGas": 1100n, "nonce": 5, "to": "abcd1234", } @@ -117,18 +86,9 @@ exports[`setGasPrice EIP1559 transaction 1`] = ` { "data": "0x0f017112205d7fcd1a0999befdb062e6762c1f0f902f729b98304a2ef539412f53360d3d6a", "from": "abcd1234", - "gasLimit": { - "hex": "0x0a", - "type": "BigNumber", - }, - "maxFeePerGas": { - "hex": "0x07d0", - "type": "BigNumber", - }, - "maxPriorityFeePerGas": { - "hex": "0x03e8", - "type": "BigNumber", - }, + "gasLimit": 10n, + "maxFeePerGas": 2310n, + "maxPriorityFeePerGas": 1100n, "nonce": undefined, "to": "abcd1234", } @@ -138,18 +98,9 @@ exports[`setGasPrice EIP1559 transaction 2`] = ` { "data": "0x0f017112205d7fcd1a0999befdb062e6762c1f0f902f729b98304a2ef539412f53360d3d6a", "from": "abcd1234", - "gasLimit": { - "hex": "0x0a", - "type": "BigNumber", - }, - "maxFeePerGas": { - "hex": "0x0834", - "type": "BigNumber", - }, - "maxPriorityFeePerGas": { - "hex": "0x044c", - "type": "BigNumber", - }, + "gasLimit": 10n, + "maxFeePerGas": 2431n, + "maxPriorityFeePerGas": 1210n, "nonce": undefined, "to": "abcd1234", } @@ -159,18 +110,9 @@ exports[`setGasPrice EIP1559 transaction 3`] = ` { "data": "0x0f017112205d7fcd1a0999befdb062e6762c1f0f902f729b98304a2ef539412f53360d3d6a", "from": "abcd1234", - "gasLimit": { - "hex": "0x0a", - "type": "BigNumber", - }, - "maxFeePerGas": { - "hex": "0x08a2", - "type": "BigNumber", - }, - "maxPriorityFeePerGas": { - "hex": "0x04ba", - "type": "BigNumber", - }, + "gasLimit": 10n, + "maxFeePerGas": 2564n, + "maxPriorityFeePerGas": 1331n, "nonce": undefined, "to": "abcd1234", } @@ -180,14 +122,8 @@ exports[`setGasPrice legacy transaction 1`] = ` { "data": "0x0f017112205d7fcd1a0999befdb062e6762c1f0f902f729b98304a2ef539412f53360d3d6a", "from": "abcd1234", - "gasLimit": { - "hex": "0x0a", - "type": "BigNumber", - }, - "gasPrice": { - "hex": "0x03e8", - "type": "BigNumber", - }, + "gasLimit": 10n, + "gasPrice": 1100n, "nonce": undefined, "to": "abcd1234", } @@ -197,14 +133,8 @@ exports[`setGasPrice legacy transaction 2`] = ` { "data": "0x0f017112205d7fcd1a0999befdb062e6762c1f0f902f729b98304a2ef539412f53360d3d6a", "from": "abcd1234", - "gasLimit": { - "hex": "0x0a", - "type": "BigNumber", - }, - "gasPrice": { - "hex": "0x044c", - "type": "BigNumber", - }, + "gasLimit": 10n, + "gasPrice": 1210n, "nonce": undefined, "to": "abcd1234", } @@ -214,14 +144,8 @@ exports[`setGasPrice legacy transaction 3`] = ` { "data": "0x0f017112205d7fcd1a0999befdb062e6762c1f0f902f729b98304a2ef539412f53360d3d6a", "from": "abcd1234", - "gasLimit": { - "hex": "0x0a", - "type": "BigNumber", - }, - "gasPrice": { - "hex": "0x04ba", - "type": "BigNumber", - }, + "gasLimit": 10n, + "gasPrice": 1331n, "nonce": undefined, "to": "abcd1234", } diff --git a/src/services/blockchain/__tests__/eth-bc-service.test.ts b/src/services/blockchain/__tests__/eth-bc-service.test.ts index 91542fb2b..5e504c33b 100644 --- a/src/services/blockchain/__tests__/eth-bc-service.test.ts +++ b/src/services/blockchain/__tests__/eth-bc-service.test.ts @@ -3,19 +3,17 @@ import { afterAll, beforeAll, beforeEach, describe, expect, jest, test } from '@ import { CID } from 'multiformats/cid' import { config, Config } from 'node-config-ts' import { logger } from '../../../logger/index.js' -import { BigNumber, ethers } from 'ethers' +import { ethers } from 'ethers' import { BlockchainService } from '../blockchain-service.js' import { EthereumBlockchainService, MAX_RETRIES } from '../ethereum/ethereum-blockchain-service.js' -import { ErrorCode } from '@ethersproject/logger' import { readFile } from 'node:fs/promises' import cloneDeep from 'lodash.clonedeep' import { createInjector } from 'typed-inject' import type { GanacheServer } from '../../../__tests__/make-ganache.util.js' import { makeGanache } from '../../../__tests__/make-ganache.util.js' +import { expectPresent } from '../../../__tests__/expect-present.util.js' -const deployContract = async ( - provider: ethers.providers.JsonRpcProvider -): Promise => { +const deployContract = async (provider: ethers.JsonRpcProvider): Promise => { const wallet = new ethers.Wallet( config.blockchain.connectors.ethereum.account.privateKey, provider @@ -29,9 +27,9 @@ const deployContract = async ( const factory = new ethers.ContractFactory(contractData.abi, contractData.bytecode.object, wallet) const contract = await factory.deploy() - await contract.deployed() + const deployedContract = contract.waitForDeployment() - return contract + return deployedContract } describe('ETH service connected to ganache', () => { @@ -39,17 +37,17 @@ describe('ETH service connected to ganache', () => { let ganacheServer: GanacheServer let ethBc: BlockchainService let testConfig: Config - let providerForGanache: ethers.providers.JsonRpcProvider - let contract: ethers.Contract + let providerForGanache: ethers.JsonRpcProvider + let contract: ethers.BaseContract beforeAll(async () => { ganacheServer = await makeGanache() - providerForGanache = new ethers.providers.JsonRpcProvider(ganacheServer.url.href) + providerForGanache = new ethers.JsonRpcProvider(ganacheServer.url.href) contract = await deployContract(providerForGanache) testConfig = cloneDeep(config) testConfig.blockchain.connectors.ethereum.rpc.port = ganacheServer.port.toString() - testConfig.blockchain.connectors.ethereum.contractAddress = contract.address + testConfig.blockchain.connectors.ethereum.contractAddress = await contract.getAddress() testConfig.useSmartContractAnchors = false const injector = createInjector() @@ -66,8 +64,11 @@ describe('ETH service connected to ganache', () => { describe('v0', () => { test('should send CID to local ganache server', async () => { - const block = await providerForGanache.getBlock(await providerForGanache.getBlockNumber()) - const startTimestamp = block.timestamp + const ganacheBlockNumber = await providerForGanache.getBlockNumber() + const block = await providerForGanache.getBlock(ganacheBlockNumber) + expectPresent(block) + expectPresent(block.date) + const startBlockDate = block.date const startBlockNumber = block.number const cid = CID.parse('bafyreic5p7grucmzx363ayxgoywb6d4qf5zjxgbqjixpkokbf5jtmdj5ni') @@ -76,12 +77,8 @@ describe('ETH service connected to ganache', () => { // checking the timestamp + block number against the snapshot is too brittle since if the test runs slowly it // can be off slightly. So we test it manually here instead. - const blockTimestamp = tx.blockTimestamp - delete tx.blockTimestamp - const blockNumber = tx.blockNumber - delete tx.blockNumber - expect(blockTimestamp).toBeGreaterThan(startTimestamp) - expect(blockNumber).toBeGreaterThan(startBlockNumber) + expect(tx.blockNumber).toBeGreaterThan(startBlockNumber) + expect(tx.blockTimestamp > startBlockDate) expect(tx).toMatchSnapshot() }) @@ -93,35 +90,27 @@ describe('ETH service connected to ganache', () => { test('gas price increase math', () => { const gasEstimate = { - maxFeePerGas: BigNumber.from(2000), - maxPriorityFeePerGas: BigNumber.from(1000), - gasPrice: BigNumber.from(0), + maxFeePerGas: BigInt(2000), + maxPriorityFeePerGas: BigInt(1000), + gasPrice: BigInt(0), } - const firstRetry = BigNumber.from(1100) - // Note that this is not 1200. It needs to be 10% over the previous attempt's gas, - // not 20% over the gas estimate - const secondRetry = BigNumber.from(1210) - expect( - EthereumBlockchainService.increaseGasPricePerAttempt( - gasEstimate.maxPriorityFeePerGas, - 0, - undefined - ).toNumber() - ).toEqual(gasEstimate.maxPriorityFeePerGas.toNumber()) - expect( - EthereumBlockchainService.increaseGasPricePerAttempt( - gasEstimate.maxPriorityFeePerGas, - 1, - gasEstimate.maxPriorityFeePerGas - ).toNumber() - ).toEqual(firstRetry.toNumber()) + // on the first attempt, we add padding since `maxFeePerGas = maxPriorityFeePerGas + baseFee + δ` + const firstAttempt = EthereumBlockchainService.increaseGasPricePerAttempt( + gasEstimate.maxPriorityFeePerGas, + undefined + ) + expect(firstAttempt).toEqual(BigInt(1100n)) + const secondAttempt = EthereumBlockchainService.increaseGasPricePerAttempt( + gasEstimate.maxPriorityFeePerGas, + firstAttempt + ) + expect(secondAttempt).toEqual(1210n) expect( EthereumBlockchainService.increaseGasPricePerAttempt( gasEstimate.maxPriorityFeePerGas, - 2, - firstRetry - ).toNumber() - ).toEqual(secondRetry.toNumber()) + secondAttempt + ) + ).toEqual(1331n) }) }) @@ -136,8 +125,10 @@ describe('ETH service connected to ganache', () => { test('should anchor to contract', async () => { const block = await providerForGanache.getBlock('latest') - const startTimestamp = block.timestamp + expectPresent(block) const startBlockNumber = block.number + expectPresent(block.date) + const startBlockDate = block.date const cid = CID.parse('bafyreic5p7grucmzx363ayxgoywb6d4qf5zjxgbqjixpkokbf5jtmdj5ni') const ethBc = EthereumBlockchainService.make(testConfig) @@ -145,9 +136,11 @@ describe('ETH service connected to ganache', () => { const tx = await ethBc.sendTransaction(cid) expect(tx).toBeDefined() const txReceipt = await providerForGanache.getTransactionReceipt(tx.txHash) + expectPresent(txReceipt) const contractEvents = txReceipt.logs.map((log) => contract.interface.parseLog(log)) expect(contractEvents.length).toEqual(1) + expectPresent(contractEvents[0]) const didAnchorEvent = contractEvents[0] expect(didAnchorEvent.name).toEqual('DidAnchor') expect(didAnchorEvent.args['_root']).toEqual( @@ -155,8 +148,8 @@ describe('ETH service connected to ganache', () => { ) // checking the values against the snapshot is too brittle since ganache is time based so we test manually - expect(tx.blockTimestamp).toBeGreaterThan(startTimestamp) expect(tx.blockNumber).toBeGreaterThan(startBlockNumber) + expect(tx.blockTimestamp > startBlockDate) }) }) }) @@ -164,11 +157,11 @@ describe('ETH service connected to ganache', () => { describe('setGasPrice', () => { const cid = CID.parse('bafyreic5p7grucmzx363ayxgoywb6d4qf5zjxgbqjixpkokbf5jtmdj5ni') const feeData = { - maxFeePerGas: BigNumber.from(2000), - maxPriorityFeePerGas: BigNumber.from(1000), - gasPrice: BigNumber.from(1000), + maxFeePerGas: BigInt(2000), + maxPriorityFeePerGas: BigInt(1000), + gasPrice: BigInt(1000), } - const gasLimit = BigNumber.from(10) + const gasLimit = BigInt(10) const provider = { estimateGas: jest.fn(() => gasLimit), getNetwork: jest.fn(() => ({ chainId: '1337' })), @@ -193,8 +186,8 @@ describe('setGasPrice', () => { }) const ethBc = await buildBlockchainService(legacyProvider) const txData = await ethBc._buildTransactionRequest(cid) - for (const attempt of [0, 1, 2]) { - await ethBc.setGasPrice(txData, attempt) + for (const idx of [0, 1, 2]) { + await ethBc.setGasPrice(txData) expect(txData).toMatchSnapshot() } }) @@ -202,8 +195,8 @@ describe('setGasPrice', () => { test('EIP1559 transaction', async () => { const ethBc = await buildBlockchainService(provider) const txData = await ethBc._buildTransactionRequest(cid) - for (const attempt of [0, 1, 2]) { - await ethBc.setGasPrice(txData, attempt) + for (const idx of [0, 1, 2]) { + await ethBc.setGasPrice(txData) expect(txData).toMatchSnapshot() } }) @@ -245,8 +238,8 @@ describe('ETH service with mock wallet', () => { test('single transaction attempt', async () => { const nonce = 5 - const gasPrice = BigNumber.from(1000) - const gasEstimate = BigNumber.from(10 * 1000) + const gasPrice = BigInt(1000) + const gasEstimate = BigInt(10 * 1000) const txnResponse = { hash: '0x12345abcde', confirmations: 3, @@ -254,16 +247,14 @@ describe('ETH service with mock wallet', () => { chainId: '1337', } const txReceipt = { - byzantium: true, status: 1, blockHash: '0x54321', blockNumber: 54321, - transactionHash: txnResponse.hash, + hash: txnResponse.hash, } const block = { - timestamp: 54321000, + date: new Date(Date.UTC(2000, 1)), } - provider.getGasPrice.mockReturnValue(gasPrice) provider.estimateGas.mockReturnValue(gasEstimate) wallet.sendTransaction.mockReturnValue(txnResponse) @@ -282,19 +273,22 @@ describe('ETH service with mock wallet', () => { const tx = await ethBc._confirmTransactionSuccess(txResponse) expect(tx).toMatchSnapshot() - const txData = wallet.sendTransaction.mock.calls[0][0] + expectPresent(wallet.sendTransaction.mock.calls[0]) + const callData = wallet.sendTransaction.mock.calls[0] + expectPresent(callData[0]) + const txData = callData[0] expect(txData).toMatchSnapshot() }) test('successful mocked transaction', async () => { - const balance = BigNumber.from(10 * 1000 * 1000) + const balance = 10n * 1000n * 1000n const nonce = 5 const feeData = { - maxFeePerGas: BigNumber.from(2000), - maxPriorityFeePerGas: BigNumber.from(1000), - gasPrice: BigNumber.from(1000), + maxFeePerGas: 2000n, + maxPriorityFeePerGas: 1000n, + gasPrice: 1000n, } - const gasEstimate = BigNumber.from(10 * 1000) + const gasEstimate = 10n * 1000n const txResponse = { foo: 'bar' } const finalTransactionResult = { txHash: '0x12345' } @@ -327,14 +321,14 @@ describe('ETH service with mock wallet', () => { }) test('insufficient funds error', async () => { - const balance = BigNumber.from(10 * 1000 * 1000) + const balance = 10n * 1000n * 1000n const nonce = 5 - const gasPrice = BigNumber.from(1000) - const gasEstimate = BigNumber.from(10 * 1000) + const gasPrice = 1000n + const gasEstimate = 10n * 1000n const feeData = { - maxFeePerGas: BigNumber.from(2000), - maxPriorityFeePerGas: BigNumber.from(1000), - gasPrice: BigNumber.from(1000), + maxFeePerGas: 2000n, + maxPriorityFeePerGas: 1000n, + gasPrice: 1000n, } provider.getBalance.mockReturnValue(balance) @@ -348,8 +342,8 @@ describe('ETH service with mock wallet', () => { typeof ethBc._trySendTransaction > mockTrySendTransaction - .mockRejectedValueOnce({ code: ErrorCode.TIMEOUT }) - .mockRejectedValueOnce({ code: ErrorCode.INSUFFICIENT_FUNDS }) + .mockRejectedValueOnce({ code: 'TIMEOUT' }) + .mockRejectedValue({ code: 'INSUFFICIENT_FUNDS' }) const cid = CID.parse('bafyreic5p7grucmzx363ayxgoywb6d4qf5zjxgbqjixpkokbf5jtmdj5ni') await expect(ethBc.sendTransaction(cid)).rejects.toThrow( @@ -369,15 +363,15 @@ describe('ETH service with mock wallet', () => { }) test('timeout error', async () => { - const balance = BigNumber.from(10 * 1000 * 1000) + const balance = 10n * 1000n * 1000n const nonce = 5 - const gasPrice = BigNumber.from(1000) - const gasEstimate = BigNumber.from(10 * 1000) + const gasPrice = 1000n + const gasEstimate = 10n * 1000n const txResponse = { foo: 'bar' } const feeData = { - maxFeePerGas: BigNumber.from(2000), - maxPriorityFeePerGas: BigNumber.from(1000), - gasPrice: BigNumber.from(1000), + maxFeePerGas: 2000n, + maxPriorityFeePerGas: 1000n, + gasPrice: 1000n, } provider.getBalance.mockReturnValue(balance) @@ -395,7 +389,7 @@ describe('ETH service with mock wallet', () => { typeof ethBc._confirmTransactionSuccess > mockTrySendTransaction.mockReturnValue(txResponse) - mockConfirmTransactionSuccess.mockRejectedValue({ code: ErrorCode.TIMEOUT }) + mockConfirmTransactionSuccess.mockRejectedValue({ code: 'TIMEOUT' }) const cid = CID.parse('bafyreic5p7grucmzx363ayxgoywb6d4qf5zjxgbqjixpkokbf5jtmdj5ni') await expect(ethBc.sendTransaction(cid)).rejects.toThrow('Failed to send transaction') @@ -407,16 +401,16 @@ describe('ETH service with mock wallet', () => { test('nonce expired error', async () => { // test what happens if a transaction is submitted, waiting for it to be mined times out, but // then before the retry the original txn gets mined, causing a NONCE_EXPIRED error on the retry - const balance = BigNumber.from(10 * 1000 * 1000) + const balance = 10n * 1000n * 1000n const nonce = 5 - const gasPrice = BigNumber.from(1000) - const gasEstimate = BigNumber.from(10 * 1000) + const gasPrice = 1000n + const gasEstimate = 10n * 1000n const txResponses = [{ attempt: 1 }, { attempt: 2 }] const finalTransactionResult = { txHash: '0x12345' } const feeData = { - maxFeePerGas: BigNumber.from(2000), - maxPriorityFeePerGas: BigNumber.from(1000), - gasPrice: BigNumber.from(1000), + maxFeePerGas: 2000n, + maxPriorityFeePerGas: 1000n, + gasPrice: 1000n, } provider.getBalance.mockReturnValue(balance) @@ -436,16 +430,16 @@ describe('ETH service with mock wallet', () => { // Successfully submit transaction mockTrySendTransaction.mockReturnValueOnce(txResponses[0]) // Get timeout waiting for it to be mined - mockConfirmTransactionSuccess.mockRejectedValueOnce({ code: ErrorCode.TIMEOUT }) + mockConfirmTransactionSuccess.mockRejectedValueOnce({ code: 'TIMEOUT' }) // Retry the transaction, submit it successfully mockTrySendTransaction.mockReturnValueOnce(txResponses[1]) // Get timeout waiting for the second attempt as well - mockConfirmTransactionSuccess.mockRejectedValueOnce({ code: ErrorCode.TIMEOUT }) + mockConfirmTransactionSuccess.mockRejectedValueOnce({ code: 'TIMEOUT' }) // On third attempt we get a NONCE_EXPIRED error because the first attempt was actually mined correctly - mockTrySendTransaction.mockRejectedValueOnce({ code: ErrorCode.NONCE_EXPIRED }) + mockTrySendTransaction.mockRejectedValueOnce({ code: 'NONCE_EXPIRED' }) // Try to confirm the second attempt, get NONCE_EXPIRED because it was the first attempt that // was mined - mockConfirmTransactionSuccess.mockRejectedValueOnce({ code: ErrorCode.NONCE_EXPIRED }) + mockConfirmTransactionSuccess.mockRejectedValueOnce({ code: 'NONCE_EXPIRED' }) // Try to confirm the original attempt, succeed mockConfirmTransactionSuccess.mockReturnValueOnce(finalTransactionResult) diff --git a/src/services/blockchain/ethereum/ethereum-blockchain-service.ts b/src/services/blockchain/ethereum/ethereum-blockchain-service.ts index 41463d456..9f3ba7441 100644 --- a/src/services/blockchain/ethereum/ethereum-blockchain-service.ts +++ b/src/services/blockchain/ethereum/ethereum-blockchain-service.ts @@ -1,31 +1,31 @@ import type { CID } from 'multiformats/cid' import { base16 } from 'multiformats/bases/base16' -import { ErrorCode } from '@ethersproject/logger' -import { BigNumber, BigNumberish, Contract, ethers } from 'ethers' +import { + BigNumberish, + Contract, + ethers, + TransactionResponse, + TransactionRequest, + isError, + Network, +} from 'ethers' import { Config } from 'node-config-ts' import * as uint8arrays from 'uint8arrays' import { logger, logEvent, logMetric } from '../../../logger/index.js' import { Transaction } from '../../../models/transaction.js' import { BlockchainService } from '../blockchain-service.js' -import { - TransactionRequest, - TransactionResponse, - TransactionReceipt, -} from '@ethersproject/abstract-provider' import { Utils } from '../../../utils.js' const BASE_CHAIN_ID = 'eip155' -const TX_FAILURE = 0 const TX_SUCCESS = 1 const NUM_BLOCKS_TO_WAIT = 4 export const MAX_RETRIES = 3 -const POLLING_INTERVAL = 15 * 1000 // every 15 seconds const ABI = ['function anchorDagCbor(bytes32)'] class WrongChainIdError extends Error { - constructor(expected: number, actual: number) { + constructor(expected: bigint, actual: bigint) { super( `Chain ID of connected blockchain changed from ${caipChainId(expected)} to ${caipChainId( actual @@ -34,6 +34,13 @@ class WrongChainIdError extends Error { } } +/** + * Adds padding to our fees + */ +function addPadding(number: bigint): bigint { + return number + number / BigInt(10) +} + /** * Do up to +max+ attempts of an +operation+. Expect the +operation+ to return a defined value. * If no defined value is returned, iterate at most +max+ times. @@ -65,16 +72,20 @@ async function attempt( * @param txData - Transaction to write. * @param walletBalance - Available funds. */ -function handleInsufficientFundsError(txData: TransactionRequest, walletBalance: BigNumber): void { - const txCost = (txData.gasLimit as BigNumber).mul(txData.maxFeePerGas!) - if (txCost.gt(walletBalance)) { +function handleInsufficientFundsError(txData: TransactionRequest, walletBalance: bigint): void { + const gasLimit = Utils.checkNotNull(() => txData.gasLimit, 'No gas limit') + const maxFeePerGas = Utils.checkNotNull(() => txData.maxFeePerGas, 'No max fee per gas') + const txCost = BigInt(gasLimit) * BigInt(maxFeePerGas) + if (txCost > walletBalance) { logEvent.ethereum({ type: 'insufficientFunds', txCost: txCost, - balance: ethers.utils.formatUnits(walletBalance, 'gwei'), + balance: ethers.formatUnits(walletBalance, 'gwei'), }) - const errMsg = `Transaction cost is greater than our current balance. [txCost: ${txCost.toHexString()}, balance: ${walletBalance.toHexString()}]` + const errMsg = `Transaction cost is greater than our current balance. [txCost: ${txCost.toString( + 16 + )}, balance: ${walletBalance.toString(16)}]` logger.err(errMsg) throw new Error(errMsg) } @@ -84,7 +95,7 @@ function handleInsufficientFundsError(txData: TransactionRequest, walletBalance: * Represent chainId in CAIP format. * @param chainId - Numeric chain id. */ -function caipChainId(chainId: number) { +function caipChainId(chainId: bigint | number) { return `${BASE_CHAIN_ID}:${chainId}` } @@ -94,7 +105,7 @@ function caipChainId(chainId: number) { * @param actual - Chain id we received. * @param expected - Chain id we expect. */ -function assertSameChainId(actual: number, expected: number) { +function assertSameChainId(actual: bigint, expected: bigint) { if (actual != expected) { // TODO: This should be process-fatal throw new WrongChainIdError(expected, actual) @@ -115,20 +126,42 @@ function handleTimeoutError(transactionTimeoutSecs: number): void { function make(config: Config): EthereumBlockchainService { const ethereum = config.blockchain.connectors.ethereum const { host, port, url } = ethereum.rpc + let options + let network + try { + network = Network.from(ethereum.network) + options = { + staticNetwork: network, + } + } catch (e) { + logger.warn(`Network ${ethereum.network} is unknown, cannot use static network`) + } let provider if (url) { logger.imp(`Connecting ethereum provider to url: ${url}`) - provider = new ethers.providers.StaticJsonRpcProvider(url) + provider = new ethers.JsonRpcProvider(url, network, options) } else if (host && port) { logger.imp(`Connecting ethereum provider to host: ${host} and port ${port}`) - provider = new ethers.providers.StaticJsonRpcProvider(`${host}:${port}`) + const hostPort = `${host}:${port}` + provider = new ethers.JsonRpcProvider(hostPort, network, options) + } else if (network) { + logger.imp(`Connecting ethereum to etherscan provider for network ${ethereum.network}`) + const opts = { + alchemy: '-', + ankr: '-', + cloudflare: '-', + etherscan: ethereum.account.privateKey, + infura: '-', + quicknode: '-', + } + provider = ethers.getDefaultProvider(network, opts) } else { - logger.imp(`Connecting ethereum to default provider for network ${ethereum.network}`) - provider = ethers.getDefaultProvider(ethereum.network) + throw new Error( + `Cannot connect to ${ethereum.network}, it is an unknown network. Please provide url or host and port` + ) } - provider.pollingInterval = POLLING_INTERVAL const wallet = new ethers.Wallet(ethereum.account.privateKey, provider) return new EthereumBlockchainService(config, wallet) } @@ -138,12 +171,12 @@ make.inject = ['config'] as const * Ethereum blockchain service */ export class EthereumBlockchainService implements BlockchainService { - private _chainId: number | undefined + private _chainId: bigint | undefined private readonly network: string private readonly transactionTimeoutSecs: number private readonly contract: Contract private readonly overrideGasConfig: boolean - private readonly gasLimit: number + private readonly gasLimit: bigint private readonly useSmartContractAnchors: boolean private readonly contractAddress: string @@ -152,9 +185,9 @@ export class EthereumBlockchainService implements BlockchainService { const ethereumConfig = config.blockchain.connectors.ethereum this.network = ethereumConfig.network this.transactionTimeoutSecs = ethereumConfig.transactionTimeoutSecs - this.contract = new ethers.Contract(ethereumConfig.contractAddress, ABI) + this.contract = new ethers.Contract(ethereumConfig.contractAddress, ABI, this.wallet) this.overrideGasConfig = ethereumConfig.overrideGasConfig - this.gasLimit = ethereumConfig.gasLimit + this.gasLimit = BigInt(ethereumConfig.gasLimit) this.contractAddress = ethereumConfig.contractAddress } @@ -174,7 +207,8 @@ export class EthereumBlockchainService implements BlockchainService { * connected blockchain to ask for it. */ private async _loadChainId(): Promise { - const network = await this.wallet.provider.getNetwork() + const provider = Utils.checkNotNull(() => this.wallet.provider, 'No wallet provider') + const network = await provider.getNetwork() this._chainId = network.chainId } @@ -187,10 +221,18 @@ export class EthereumBlockchainService implements BlockchainService { * maxFeePerGas should equal to `maxPriorityFeePerGas` (our tip to a miner) plus `baseFee` (ETH burned according to current network conditions). * To estimate the current parameters, we use `getFeeData` function, which returns two of our parameters. * Here we _can_ calculate `baseFee`, but also we can avoid doing that. Remember, we increase just `maxPriorityFeePerGas`. - * Here we calculate a difference between previously sent `maxPriorityFeePerGas` and the increased one. It is our voluntary increase in gas price we agree to pay to mine our transaction. - * We just add the difference to a currently estimated `maxFeePerGas` so that we conform to the equality `maxFeePerGas = baseFee + maxPriorityFeePerGas`. + * Here we calculate a difference between previously sent `maxPriorityFeePerGas` and the increased one. It is our voluntary + * increase in gas price we agree to pay to mine our transaction. + * We just add the difference to a currently estimated `maxFeePerGas` so that we conform to the equality + * `maxFeePerGas = baseFee + maxPriorityFeePerGas`. * - * NB. EIP1559 now uses two components of gas cost: `baseFee` and `maxPriorityFeePerGas`. `maxPriorityFeePerGas` is a tip to a miner to include a transaction into a block. `baseFee` is a slowly changing amount of gas or ether that is going to be burned. `baseFee` is set by _network_. Since we do not know what `baseFee` will be, EIP1559 introduces `maxFeePerGas` which is an absolute maximum you are willing to pay for a transaction. `maxFeePerGas` must be `>= maxPriorityFeePerGas + baseFee`. The inequality here is to accommodate for changes in `baseFee`. If `maxFeePerGas` appears to be less than the sum, the transaction is underpriced. If it is greater than the sum (`maxFeePerGas = maxPriorityFeePerGas + baseFee + δ`): + * NB. EIP1559 now uses two components of gas cost: `baseFee` and `maxPriorityFeePerGas`. `maxPriorityFeePerGas` is a + * tip to a miner to include a transaction into a block. `baseFee` is a slowly changing amount of gas or ether that is + * going to be burned. `baseFee` is set by _network_. Since we do not know what `baseFee` will be, EIP1559 introduces + * `maxFeePerGas` which is an absolute maximum you are willing to pay for a transaction. `maxFeePerGas` must be + * `>= maxPriorityFeePerGas + baseFee`. The inequality here is to accommodate for changes in `baseFee`. If + * `maxFeePerGas` appears to be less than the sum, the transaction is underpriced. If it is greater than the sum + * (`maxFeePerGas = maxPriorityFeePerGas + baseFee + δ`): * - if `baseFee` changes up to `δ`, the transaction can be mined still; `δ` is like a safety buffer; * - transaction fee that is deducted from your wallet still equals `maxPriorityFeePerGas + baseFee`, no matter what `maxFeePerGas` you have set. * @@ -201,48 +243,47 @@ export class EthereumBlockchainService implements BlockchainService { * the gas price we set by a 10% multiple with each subsequent attempt * @private */ - async setGasPrice(txData: TransactionRequest, attempt: number): Promise { + async setGasPrice(txData: TransactionRequest): Promise { if (this.overrideGasConfig) { - txData.gasLimit = BigNumber.from(this.gasLimit) + txData.gasLimit = this.gasLimit logger.debug('Overriding Gas limit: ' + txData.gasLimit.toString()) return } - const feeData = await this.wallet.provider.getFeeData() + const walletProvider = Utils.checkNotNull(() => this.wallet.provider, 'No wallet provider') + const feeData = await Utils.checkNotNullAsync(() => walletProvider.getFeeData(), 'No fee data') // Add extra to gas price for each subsequent attempt const maxFeePerGas = feeData.maxFeePerGas const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas // Is EIP-1559 if (maxPriorityFeePerGas && maxFeePerGas) { - // When attempt 0, use currently estimated maxPriorityFeePerGas; otherwise use previous transaction maxPriorityFeePerGas - const prevPriorityFee = BigNumber.from( - txData.maxPriorityFeePerGas || feeData.maxPriorityFeePerGas - ) + const baseFee = maxFeePerGas - maxPriorityFeePerGas + const prevPriorityFee = BigInt(txData.maxPriorityFeePerGas || feeData.maxPriorityFeePerGas) const nextPriorityFee = EthereumBlockchainService.increaseGasPricePerAttempt( maxPriorityFeePerGas, - attempt, prevPriorityFee ) txData.maxPriorityFeePerGas = nextPriorityFee - const baseFee = maxFeePerGas.sub(maxPriorityFeePerGas) - txData.maxFeePerGas = baseFee.add(nextPriorityFee) + // must be greater than baseFee + maxPriorityFeePerGas + txData.maxFeePerGas = addPadding(baseFee + nextPriorityFee) logger.debug( - `Estimated maxPriorityFeePerGas: ${nextPriorityFee.toString()} wei; maxFeePerGas: ${txData.maxFeePerGas.toString()} wei` + `Estimated maxPriorityFeePerGas: ${txData.maxPriorityFeePerGas.toString()} wei; maxFeePerGas: ${txData.maxFeePerGas.toString()} wei` ) } else { - const feeDataGasPrice = feeData.gasPrice - if (!feeDataGasPrice) throw new Error(`Unavailable gas price for pre-EIP-1559 transaction`) + const feeDataGasPrice = Utils.checkNotNull( + () => feeData.gasPrice, + `Unavailable gas price for pre-EIP-1559 transaction` + ) // When attempt 0, use currently estimated gasPrice; otherwise use previous transaction gasPrice - const prevGasPrice = BigNumber.from(txData.gasPrice || feeData.gasPrice) + const prevGasPrice = BigInt(txData.gasPrice || feeDataGasPrice) txData.gasPrice = EthereumBlockchainService.increaseGasPricePerAttempt( feeDataGasPrice, - attempt, prevGasPrice ) logger.debug(`Estimated gasPrice: ${txData.gasPrice.toString()} wei`) } - txData.gasLimit = await this.wallet.provider.estimateGas(txData) + txData.gasLimit = await walletProvider.estimateGas(txData) logger.debug('Estimated Gas limit: ' + txData.gasLimit.toString()) } @@ -260,23 +301,13 @@ export class EthereumBlockchainService implements BlockchainService { */ static increaseGasPricePerAttempt( estimate: BigNumberish, - attempt: number, previousGas: BigNumberish | undefined - ): BigNumber { - // Try to increase an estimated gas price first - const estimateBN = BigNumber.from(estimate) - const increase = estimateBN.div(10).mul(attempt) // 10% increase per attempt - const increaseEstimate = estimateBN.add(increase) - - if (attempt == 0 || previousGas == undefined) { - return increaseEstimate + ): bigint { + if (previousGas == undefined) { + return addPadding(BigInt(estimate)) + } else { + return addPadding(BigInt(estimate > previousGas ? estimate : previousGas)) } - // Then try to increase a current transaction gas price - const previousGasBN = BigNumber.from(previousGas) - const increaseTransaction = previousGasBN.add(previousGasBN.div(10)) // +10% - - // Choose the bigger increase, either from current transaction or from increment - return increaseEstimate.gt(increaseTransaction) ? increaseEstimate : increaseTransaction } /** @@ -290,7 +321,8 @@ export class EthereumBlockchainService implements BlockchainService { async _buildTransactionRequest(rootCid: CID): Promise { logger.debug('Preparing ethereum transaction') - const baseNonce = await this.wallet.provider.getTransactionCount(this.wallet.address) + const provider = Utils.checkNotNull(() => this.wallet.provider, 'No wallet provider') + const baseNonce = await provider.getTransactionCount(this.wallet.address) if (!this.useSmartContractAnchors) { const rootStrHex = rootCid.toString(base16) @@ -307,7 +339,7 @@ export class EthereumBlockchainService implements BlockchainService { const hexEncoded = '0x' + uint8arrays.toString(rootCid.bytes.slice(4), 'base16') // @ts-ignore `anchorDagCbor` is a Solidity function - const transactionRequest = await this.contract.populateTransaction.anchorDagCbor(hexEncoded) + const transactionRequest = await this.contract.anchorDagCbor(hexEncoded) return { to: this.contractAddress, data: transactionRequest.data, @@ -321,23 +353,27 @@ export class EthereumBlockchainService implements BlockchainService { * @param txData */ async _trySendTransaction(txData: TransactionRequest): Promise { - logger.imp('Transaction data:' + JSON.stringify(txData)) + logger.imp( + 'Transaction data:' + + JSON.stringify(txData, (_, v) => (typeof v === 'bigint' ? v.toString() : v)) + ) logEvent.ethereum({ type: 'txRequest', tx: txData, }) logger.imp(`Sending transaction to Ethereum ${this.network} network...`) - const txResponse: TransactionResponse = await this.wallet.sendTransaction(txData) + const txResponse = await Utils.checkNotNullAsync( + () => this.wallet.sendTransaction(txData), + 'No transaction' + ) logEvent.ethereum({ type: 'txResponse', hash: txResponse.hash, blockNumber: txResponse.blockNumber, blockHash: txResponse.blockHash, - timestamp: txResponse.timestamp, - confirmations: txResponse.confirmations, from: txResponse.from, - raw: txResponse.raw, + raw: txResponse.data, }) if (!this._chainId) throw new Error(`No chainId available`) @@ -352,35 +388,38 @@ export class EthereumBlockchainService implements BlockchainService { */ async _confirmTransactionSuccess(txResponse: TransactionResponse): Promise { logger.imp(`Waiting to confirm transaction with hash ${txResponse.hash}`) - const txReceipt: TransactionReceipt = await this.wallet.provider.waitForTransaction( - txResponse.hash, - NUM_BLOCKS_TO_WAIT, - this.transactionTimeoutSecs * 1000 + const walletProvider = await Utils.checkNotNull( + () => this.wallet.provider, + 'No wallet provider' + ) + const txReceipt = await Utils.checkNotNullAsync( + () => + walletProvider.waitForTransaction( + txResponse.hash, + NUM_BLOCKS_TO_WAIT, + this.transactionTimeoutSecs * 1000 + ), + 'No transaction receipt' ) logEvent.ethereum({ type: 'txReceipt', tx: txReceipt, }) - const block = await this.wallet.provider.getBlock(txReceipt.blockHash) + const block = await Utils.checkNotNullAsync( + () => walletProvider.getBlock(txReceipt.blockHash), + 'No block found' + ) + const blockDate = Utils.checkNotNull(() => block.date, 'Block has no date') - const status = txReceipt.byzantium ? txReceipt.status : -1 - let statusMessage = status == TX_SUCCESS ? 'success' : 'failure' - if (!txReceipt.byzantium) { - statusMessage = 'unknown' - } + const statusMessage = txReceipt.status == TX_SUCCESS ? 'success' : 'failure' logger.imp( - `Transaction completed on Ethereum ${this.network} network. Transaction hash: ${txReceipt.transactionHash}. Status: ${statusMessage}.` + `Transaction completed on Ethereum ${this.network} network. Transaction hash: ${txReceipt.blockHash}. Status: ${statusMessage}.` ) - if (status == TX_FAILURE) { + if (txReceipt.status != TX_SUCCESS) { throw new Error('Transaction completed with a failure status') } - return new Transaction( - this.chainId, - txReceipt.transactionHash, - txReceipt.blockNumber, - block.timestamp - ) + return new Transaction(this.chainId, txReceipt.hash, txReceipt.blockNumber, blockDate) } /** @@ -413,31 +452,29 @@ export class EthereumBlockchainService implements BlockchainService { return this.withWalletBalance((walletBalance) => { return attempt(MAX_RETRIES, async (attemptNum) => { try { - await this.setGasPrice(txData, attemptNum) + await this.setGasPrice(txData) const txResponse = await this._trySendTransaction(txData) txResponses.push(txResponse) return await this._confirmTransactionSuccess(txResponse) } catch (err: any) { logger.err(err) - const { code } = err - switch (code) { - case ErrorCode.INSUFFICIENT_FUNDS: - return handleInsufficientFundsError(txData, walletBalance) - case ErrorCode.TIMEOUT: - return handleTimeoutError(this.transactionTimeoutSecs) - case ErrorCode.NONCE_EXPIRED: - // If this happens it most likely means that one of our previous attempts timed out, but - // then actually wound up being successfully mined - logEvent.ethereum({ - type: 'nonceExpired', - nonce: txData.nonce, - }) - if (attemptNum == 0 || txResponses.length == 0) { - throw err - } - return this._checkForPreviousTransactionSuccess(txResponses) - default: - return undefined + if (isError(err, 'INSUFFICIENT_FUNDS')) { + return handleInsufficientFundsError(txData, walletBalance) + } else if (isError(err, 'TIMEOUT')) { + return handleTimeoutError(this.transactionTimeoutSecs) + } else if (isError(err, 'NONCE_EXPIRED') || isNonceError(err)) { + // If this happens it most likely means that one of our previous attempts timed out, but + // then actually wound up being successfully mined + logEvent.ethereum({ + type: 'nonceExpired', + nonce: txData.nonce, + }) + if (attemptNum == 0 || txResponses.length == 0) { + throw err + } + return this._checkForPreviousTransactionSuccess(txResponses) + } else { + throw new Error(`Unhandled error in attempt: ${err}`) } } }) @@ -448,21 +485,32 @@ export class EthereumBlockchainService implements BlockchainService { * Report wallet balance before and after +operation+. * @param operation */ - private async withWalletBalance(operation: (balance: BigNumber) => Promise): Promise { - const startingWalletBalance = await this.wallet.provider.getBalance(this.wallet.address) + private async withWalletBalance(operation: (balance: bigint) => Promise): Promise { + const provider = Utils.checkNotNull(() => this.wallet.provider, 'No wallet provider') + const startingWalletBalance = await Utils.checkNotNullAsync( + () => provider.getBalance(this.wallet.address), + `No wallet balance for ${this.wallet.address}` + ) logMetric.ethereum({ type: 'walletBalance', - balance: ethers.utils.formatUnits(startingWalletBalance, 'gwei'), + balance: ethers.formatUnits(startingWalletBalance, 'gwei'), }) logger.debug(`Current wallet balance is ` + startingWalletBalance) const result = await operation(startingWalletBalance) - const endingWalletBalance = await this.wallet.provider.getBalance(this.wallet.address) + const endingWalletBalance = await Utils.checkNotNullAsync( + () => provider.getBalance(this.wallet.address), + `No wallet balance for ${this.wallet.address}` + ) logMetric.ethereum({ type: 'walletBalance', - balance: ethers.utils.formatUnits(endingWalletBalance, 'gwei'), + balance: ethers.formatUnits(endingWalletBalance, 'gwei'), }) return result } } + +function isNonceError(err: any): boolean { + return isError(err, 'UNKNOWN_ERROR') && err.message.includes('correct nonce') +} diff --git a/src/settings.ts b/src/settings.ts index 41bf72322..f63cce6f7 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,4 @@ export enum METRIC_NAMES { - // *******************************************************************// // Anchor Service (counts) @@ -39,7 +38,7 @@ export enum METRIC_NAMES { // Anchor Service (histograms) // request that moves from ready -> processing - READY_PROCESSING_MS = "ready_processing_ms", + READY_PROCESSING_MS = 'ready_processing_ms', // request that moves from created -> success CREATED_SUCCESS_MS = 'created_success_ms', @@ -58,5 +57,5 @@ export enum METRIC_NAMES { PIN_FAILED = 'pin_failed', // Request Controller - ANCHOR_REQUESTED = 'anchor_requested' + ANCHOR_REQUESTED = 'anchor_requested', } diff --git a/src/utils.ts b/src/utils.ts index a09b1ed1c..12791aa92 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -41,4 +41,29 @@ export class Utils { const cidVersion = 1 return CID.create(cidVersion, ETH_TX_CODE, multihash) } + + /** + * Check if a function returns value that is not null. If not null, return the value, otherwise throw an error + * @param func + * @param error + */ + static checkNotNull(func: () => T | null | undefined, error: string): T { + const value = func() + if (!value) throw new Error(error) + return value + } + + /** + * Check if a async function returns value that is not null. If not null, return the value, otherwise throw an error + * @param func + * @param error + */ + static async checkNotNullAsync( + func: () => Promise, + error: string + ): Promise { + const value = await func() + if (!value) throw new Error(error) + return value + } }