From e9f96941871fde4570176f136ade02361fb74e1d Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Thu, 26 Sep 2024 18:56:17 +1000 Subject: [PATCH 01/10] feat(account): add AccountMetamaskFactory --- package-lock.json | 309 ++++++++++++++++++++++++++++++++- package.json | 2 + src/account/Metamask.ts | 90 ++++++++++ src/account/MetamaskFactory.ts | 122 +++++++++++++ src/index-browser.ts | 2 + src/utils/errors.ts | 5 +- 6 files changed, 519 insertions(+), 11 deletions(-) create mode 100644 src/account/Metamask.ts create mode 100644 src/account/MetamaskFactory.ts diff --git a/package-lock.json b/package-lock.json index d4faa8f8d6..429d99a1e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,11 @@ "@azure/core-rest-pipeline": "^1.16.1", "@babel/runtime-corejs3": "^7.24.7", "@ledgerhq/hw-transport": "^6.31.0", + "@metamask/providers": "^17.2.0", "@scure/bip39": "^1.3.0", "@types/json-bigint": "^1.0.4", "@types/node": "^20.14.10", + "@types/readable-stream": "^4.0.15", "@types/sha.js": "^2.4.4", "@types/webextension-polyfill": "^0.10.7", "@types/websocket": "^1.0.10", @@ -2519,6 +2521,53 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@ethereumjs/common": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", + "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", + "dependencies": { + "@ethereumjs/util": "^8.1.0", + "crc-32": "^1.2.0" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethereumjs/tx": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz", + "integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==", + "dependencies": { + "@ethereumjs/common": "^3.2.0", + "@ethereumjs/rlp": "^4.0.1", + "@ethereumjs/util": "^8.1.0", + "ethereum-cryptography": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2845,6 +2894,147 @@ "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.12.0.tgz", "integrity": "sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA==" }, + "node_modules/@metamask/json-rpc-engine": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-9.0.3.tgz", + "integrity": "sha512-efeRXW7KaL0BJcAeudSGhzu6sD3hMpxx9nl3V+Yemm1bsyc66yVUhYPR+XH+Y6ZvB2p05ywgvd1Ev5PBwFzr/g==", + "dependencies": { + "@metamask/rpc-errors": "^6.3.1", + "@metamask/safe-event-emitter": "^3.0.0", + "@metamask/utils": "^9.1.0" + }, + "engines": { + "node": "^18.18 || >=20" + } + }, + "node_modules/@metamask/json-rpc-middleware-stream": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-8.0.3.tgz", + "integrity": "sha512-x0rh4EzzLtkpBi7adrAZ2qSAXBwk4knARZdR1j5YOyXYN7r0AeoTiTgmw7pfrUIF62x2si+WAOMm9R1hWNteGw==", + "dependencies": { + "@metamask/json-rpc-engine": "^9.0.3", + "@metamask/safe-event-emitter": "^3.0.0", + "@metamask/utils": "^9.1.0", + "readable-stream": "^3.6.2" + }, + "engines": { + "node": "^18.18 || >=20" + } + }, + "node_modules/@metamask/object-multiplex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@metamask/object-multiplex/-/object-multiplex-2.0.0.tgz", + "integrity": "sha512-+ItrieVZie3j2LfYE0QkdW3dsEMfMEp419IGx1zyeLqjRZ14iQUPRO0H6CGgfAAoC0x6k2PfCAGRwJUA9BMrqA==", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.2" + }, + "engines": { + "node": "^16.20 || ^18.16 || >=20" + } + }, + "node_modules/@metamask/providers": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/@metamask/providers/-/providers-17.2.0.tgz", + "integrity": "sha512-99EIsZo1vIuA7Wc9ruWOd9LGr0GCqEY9lR0/hcjasUZH31MGUe0H/0NdMcz2tRXhsYRvt6M+2lsM4dDG1+atRw==", + "dependencies": { + "@metamask/json-rpc-engine": "^9.0.1", + "@metamask/json-rpc-middleware-stream": "^8.0.1", + "@metamask/object-multiplex": "^2.0.0", + "@metamask/rpc-errors": "^6.3.1", + "@metamask/safe-event-emitter": "^3.1.1", + "@metamask/utils": "^9.0.0", + "detect-browser": "^5.2.0", + "extension-port-stream": "^4.1.0", + "fast-deep-equal": "^3.1.3", + "is-stream": "^2.0.0", + "readable-stream": "^3.6.2" + }, + "engines": { + "node": "^18.18 || >=20" + } + }, + "node_modules/@metamask/providers/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@metamask/rpc-errors": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-6.3.1.tgz", + "integrity": "sha512-ugDY7cKjF4/yH5LtBaOIKHw/AiGGSAmzptAUEiAEGr/78LwuzcXAxmzEQfSfMIfI+f9Djr8cttq1pRJJKfTuCg==", + "dependencies": { + "@metamask/utils": "^9.0.0", + "fast-safe-stringify": "^2.0.6" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/safe-event-emitter": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-3.1.1.tgz", + "integrity": "sha512-ihb3B0T/wJm1eUuArYP4lCTSEoZsClHhuWyfo/kMX3m/odpqNcPfsz5O2A3NT7dXCAgWPGDQGPqygCpgeniKMw==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@metamask/superstruct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@metamask/superstruct/-/superstruct-3.1.0.tgz", + "integrity": "sha512-N08M56HdOgBfRKkrgCMZvQppkZGcArEop3kixNEtVbJKm6P9Cfg0YkI6X0s1g78sNrj2fWUwvJADdZuzJgFttA==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/utils": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.2.1.tgz", + "integrity": "sha512-/u663aUaB6+Xe75i3Mt/1cCljm41HDYIsna5oBrwGvgkY2zH7/9k9Zjd706cxoAbxN7QgLSVAReUiGnuxCuXrQ==", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@metamask/superstruct": "^3.1.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/utils/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/utils/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", @@ -2886,6 +3076,17 @@ "dev": true, "optional": true }, + "node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", @@ -2946,6 +3147,19 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", @@ -3059,6 +3273,14 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -3110,6 +3332,11 @@ "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==", "dev": true }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, "node_modules/@types/node": { "version": "20.14.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", @@ -3133,6 +3360,20 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/readable-stream": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz", + "integrity": "sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/@types/sha.js": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/sha.js/-/sha.js-2.4.4.tgz", @@ -5721,6 +5962,17 @@ "typescript": ">=4" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -6000,6 +6252,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-browser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", + "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==" + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -7116,6 +7373,17 @@ "node": ">=0.10.0" } }, + "node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -7178,11 +7446,21 @@ "type": "^2.7.2" } }, + "node_modules/extension-port-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/extension-port-stream/-/extension-port-stream-4.2.0.tgz", + "integrity": "sha512-i5IgiPVMVrHN+Zx8PRjvFsOw8L1A3sboVwPZghDjW9Yp1BMmBDE6mCcTNu4xMXPYduBOwI3CBK7wd72LcOyD6g==", + "dependencies": { + "readable-stream": "^3.6.2 || ^4.4.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -7212,6 +7490,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -9725,6 +10008,11 @@ "node": ">= 8" } }, + "node_modules/micro-ftch": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", + "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==" + }, "node_modules/micromatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", @@ -10734,7 +11022,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -11083,6 +11370,14 @@ "node": ">=4" } }, + "node_modules/pony-cause": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", + "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -11453,7 +11748,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -12361,7 +12655,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -13228,8 +13521,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utila": { "version": "0.4.0", @@ -13753,8 +14045,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "3.0.3", diff --git a/package.json b/package.json index b92123da05..fe780a0d54 100644 --- a/package.json +++ b/package.json @@ -61,9 +61,11 @@ "@azure/core-rest-pipeline": "^1.16.1", "@babel/runtime-corejs3": "^7.24.7", "@ledgerhq/hw-transport": "^6.31.0", + "@metamask/providers": "^17.2.0", "@scure/bip39": "^1.3.0", "@types/json-bigint": "^1.0.4", "@types/node": "^20.14.10", + "@types/readable-stream": "^4.0.15", "@types/sha.js": "^2.4.4", "@types/webextension-polyfill": "^0.10.7", "@types/websocket": "^1.0.10", diff --git a/src/account/Metamask.ts b/src/account/Metamask.ts new file mode 100644 index 0000000000..0318641c28 --- /dev/null +++ b/src/account/Metamask.ts @@ -0,0 +1,90 @@ +import type { BaseProvider } from '@metamask/providers'; +import AccountBase from './Base'; +import { Encoded } from '../utils/encoder'; +import { ArgumentError, InternalError, NotImplementedError } from '../utils/errors'; + +export const snapId = 'npm:@aeternity-snap/plugin'; + +export async function invokeSnap( + provider: BaseProvider, + method: string, + params: unknown, + key: string, +): Promise { + const response = await provider.request<{ [k in string]: unknown }>({ + method: 'wallet_invokeSnap', + params: { snapId, request: { method, params } }, + }); + if (response == null) throw new InternalError('Empty MetaMask response'); + if (!(key in response)) { + throw new InternalError(`Key ${key} missed in response ${JSON.stringify(response)}`); + } + return response[key] as R; +} + +/** + * Account connected to Aeternity Snap for MetaMask + * https://www.npmjs.com/package/\@aeternity-snap/plugin + */ +export default class AccountMetamask extends AccountBase { + readonly provider: BaseProvider; + + readonly index: number; + + override readonly address: Encoded.AccountAddress; + + /** + * @param address - Address of account + */ + constructor(provider: BaseProvider, index: number, address: Encoded.AccountAddress) { + super(); + this.provider = provider; + this.index = index; + this.address = address; + } + + // eslint-disable-next-line class-methods-use-this + override async sign(): Promise { + throw new NotImplementedError('RAW signing using MetaMask'); + } + + // eslint-disable-next-line class-methods-use-this + override async signTypedData(): Promise { + throw new NotImplementedError('Typed data signing using MetaMask'); + } + + // eslint-disable-next-line class-methods-use-this + override async signDelegation(): Promise { + throw new NotImplementedError('signing delegation using MetaMask'); + } + + // eslint-disable-next-line class-methods-use-this + override async signTransaction( + tx: Encoded.Transaction, + { innerTx, networkId }: { innerTx?: boolean; networkId?: string } = {}, + ): Promise { + if (innerTx != null) throw new NotImplementedError('innerTx option in AccountMetamask'); + if (networkId == null) throw new ArgumentError('networkId', 'provided', networkId); + + return invokeSnap( + this.provider, + 'signTransaction', + { derivationPath: [`${this.index}'`, "0'", "0'"], tx, networkId }, + 'signedTx', + ); + } + + // eslint-disable-next-line class-methods-use-this + override async signMessage(message: string): Promise { + const signature = await invokeSnap( + this.provider, + 'signMessage', + { + derivationPath: [`${this.index}'`, "0'", "0'"], + message: Buffer.from(message).toString('base64'), + }, + 'signature', + ); + return Buffer.from(signature, 'base64'); + } +} diff --git a/src/account/MetamaskFactory.ts b/src/account/MetamaskFactory.ts new file mode 100644 index 0000000000..d2d72ec0b5 --- /dev/null +++ b/src/account/MetamaskFactory.ts @@ -0,0 +1,122 @@ +import type { BaseProvider } from '@metamask/providers'; +import { InternalError, UnsupportedPlatformError, UnsupportedVersionError } from '../utils/errors'; +import { Encoded } from '../utils/encoder'; +import semverSatisfies from '../utils/semver-satisfies'; +import AccountBaseFactory from './BaseFactory'; +import AccountMetamask, { invokeSnap, snapId } from './Metamask'; + +const snapMinVersion = '0.0.9'; +const snapMaxVersion = '0.1.0'; + +interface SnapDetails { + blocked: boolean; + enabled: boolean; + id: typeof snapId; + version: string; + initialPermissions: Record; +} + +/** + * A factory class that generates instances of AccountMetamask. + */ +export default class AccountMetamaskFactory extends AccountBaseFactory { + readonly provider: BaseProvider; + + /** + * @param provider - Connection to MetaMask to use + */ + constructor(provider?: BaseProvider) { + super(); + if (provider != null) { + this.provider = provider; + return; + } + if (window == null) { + throw new UnsupportedPlatformError( + 'Window object not found, you can run AccountMetamaskFactory only in browser or setup a provider', + ); + } + if (!('ethereum' in window) || window.ethereum == null) { + throw new UnsupportedPlatformError( + '`ethereum` object not found, you can run AccountMetamaskFactory only with Metamask enabled or setup a provider', + ); + } + this.provider = window.ethereum as BaseProvider; + } + + /** + * It throws an exception if MetaMask has an incompatible version. + */ + async #ensureMetamaskSupported(): Promise { + const version = await this.provider.request({ method: 'web3_clientVersion' }); + if (version == null) throw new InternalError("Can't get Ethereum Provider version"); + const metamaskPrefix = 'MetaMask/v'; + if (!version.startsWith(metamaskPrefix)) { + throw new UnsupportedPlatformError(`Expected Metamask, got ${version} instead`); + } + const args = [version.slice(metamaskPrefix.length), '12.2.4'] as const; + if (!semverSatisfies(...args)) throw new UnsupportedVersionError('Metamask', ...args); + } + + #ensureReadyPromise?: Promise; + + /** + * Request MetaMask to install Aeternity snap. + */ + async installSnap(): Promise { + await this.#ensureMetamaskSupported(); + const details = (await this.provider.request({ + method: 'wallet_requestSnaps', + params: { [snapId]: { version: snapMinVersion } }, + })) as { [key in typeof snapId]: SnapDetails }; + this.#ensureReadyPromise = Promise.resolve(); + return details[snapId]; + } + + /** + * It throws an exception if MetaMask or Aeternity snap has an incompatible version or is not + * installed. + */ + async ensureReady(): Promise { + const snapVersion = await this.getSnapVersion(); + const args = [snapVersion, snapMinVersion, snapMaxVersion] as const; + if (!semverSatisfies(...args)) + throw new UnsupportedVersionError('Aeternity snap in MetaMask', ...args); + this.#ensureReadyPromise = Promise.resolve(); + } + + async #ensureReady(): Promise { + this.#ensureReadyPromise ??= this.ensureReady(); + return this.#ensureReadyPromise; + } + + /** + * @returns the version of snap installed in MetaMask + */ + async getSnapVersion(): Promise { + await this.#ensureMetamaskSupported(); + const snaps = (await this.provider.request({ method: 'wallet_getSnaps' })) as Record< + string, + { version: string } + >; + const version = snaps[snapId]?.version; + if (version == null) + throw new UnsupportedPlatformError('Aeternity snap is not installed to MetaMask'); + return version; + } + + /** + * Get an instance of AccountMetaMask for a given account index. + * @param accountIndex - Index of account + */ + async initialize(accountIndex: number): Promise { + await this.#ensureReady(); + const address = await invokeSnap( + this.provider, + 'getPublicKey', + { derivationPath: [`${accountIndex}'`, "0'", "0'"] }, + 'publicKey', + ); + return new AccountMetamask(this.provider, accountIndex, address); + } +} diff --git a/src/index-browser.ts b/src/index-browser.ts index cb524fb80e..c69bc2e24f 100644 --- a/src/index-browser.ts +++ b/src/index-browser.ts @@ -115,6 +115,8 @@ export { default as AccountMnemonicFactory } from './account/MnemonicFactory'; export { default as AccountGeneralized } from './account/Generalized'; export { default as AccountLedger } from './account/Ledger'; export { default as AccountLedgerFactory } from './account/LedgerFactory'; +export { default as AccountMetamask } from './account/Metamask'; +export { default as AccountMetamaskFactory } from './account/MetamaskFactory'; export { default as CompilerBase } from './contract/compiler/Base'; export { default as CompilerHttp } from './contract/compiler/Http'; export { default as Channel } from './channel/Contract'; diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 67eb6ab049..8eb69db460 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -238,9 +238,10 @@ export class NotImplementedError extends BaseError { * @category exception */ export class UnsupportedVersionError extends BaseError { - constructor(dependency: string, version: string, geVersion: string, ltVersion: string) { + constructor(dependency: string, version: string, geVersion: string, ltVersion?: string) { super( - `Unsupported ${dependency} version ${version}. Supported: >= ${geVersion} < ${ltVersion}`, + `Unsupported ${dependency} version ${version}. Supported: >= ${geVersion}` + + (ltVersion == null ? '' : ` < ${ltVersion}`), ); this.name = 'UnsupportedVersionError'; } From 0c6d074339c38f0dc8686a79e25d95ee4e4e7a4c Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Fri, 27 Sep 2024 15:35:35 +1000 Subject: [PATCH 02/10] docs(aepp): extract ConnectFrame and ConnectLedger --- examples/browser/aepp/src/Connect.vue | 245 +----------------- .../aepp/src/components/ConnectFrame.vue | 180 +++++++++++++ .../aepp/src/components/ConnectLedger.vue | 87 +++++++ 3 files changed, 275 insertions(+), 237 deletions(-) create mode 100644 examples/browser/aepp/src/components/ConnectFrame.vue create mode 100644 examples/browser/aepp/src/components/ConnectLedger.vue diff --git a/examples/browser/aepp/src/Connect.vue b/examples/browser/aepp/src/Connect.vue index 06b5116927..bf2fe7e81c 100644 --- a/examples/browser/aepp/src/Connect.vue +++ b/examples/browser/aepp/src/Connect.vue @@ -1,247 +1,18 @@ diff --git a/examples/browser/aepp/src/components/ConnectFrame.vue b/examples/browser/aepp/src/components/ConnectFrame.vue new file mode 100644 index 0000000000..ca2a17755a --- /dev/null +++ b/examples/browser/aepp/src/components/ConnectFrame.vue @@ -0,0 +1,180 @@ + + + diff --git a/examples/browser/aepp/src/components/ConnectLedger.vue b/examples/browser/aepp/src/components/ConnectLedger.vue new file mode 100644 index 0000000000..a296a2a9aa --- /dev/null +++ b/examples/browser/aepp/src/components/ConnectLedger.vue @@ -0,0 +1,87 @@ + + + From 5eb78fbbf4186abcec12c9d9ccb067875be73cf7 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Fri, 27 Sep 2024 17:26:07 +1000 Subject: [PATCH 03/10] docs(aepp): add connect via MetaMask --- examples/browser/aepp/src/Connect.vue | 4 +- .../aepp/src/components/ConnectMetamask.vue | 105 ++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 examples/browser/aepp/src/components/ConnectMetamask.vue diff --git a/examples/browser/aepp/src/Connect.vue b/examples/browser/aepp/src/Connect.vue index bf2fe7e81c..9aa857b329 100644 --- a/examples/browser/aepp/src/Connect.vue +++ b/examples/browser/aepp/src/Connect.vue @@ -2,6 +2,7 @@ @@ -10,9 +11,10 @@ diff --git a/examples/browser/aepp/src/components/ConnectMetamask.vue b/examples/browser/aepp/src/components/ConnectMetamask.vue new file mode 100644 index 0000000000..d96f5b667b --- /dev/null +++ b/examples/browser/aepp/src/components/ConnectMetamask.vue @@ -0,0 +1,105 @@ + + + From bffa9aca573e37bad54df567cae2a7dc20ecd3aa Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 2 Oct 2024 12:10:19 +1000 Subject: [PATCH 04/10] test(account): add unit and integration tests of AccountMetamaskFactory --- .gitignore | 1 + package-lock.json | 624 ++++++++++++++++++++++++++++++++++++- package.json | 3 + test/unit/metamask.ts | 305 ++++++++++++++++++ tooling/fetch-metamask.mjs | 18 ++ 5 files changed, 938 insertions(+), 13 deletions(-) create mode 100644 test/unit/metamask.ts create mode 100644 tooling/fetch-metamask.mjs diff --git a/.gitignore b/.gitignore index efd248cee1..c1c33ff466 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ site /tooling/autorest/compiler-swagger.yaml /tooling/autorest/middleware-openapi.yaml /test/environment/ledger/browser +/test/assets /bin diff --git a/package-lock.json b/package-lock.json index 429d99a1e6..b37504fc2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,11 +74,13 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-rulesdir": "^0.2.2", "eslint-plugin-tsdoc": "^0.3.0", + "extract-zip": "^2.0.1", "html-webpack-plugin": "^5.6.0", "mocha": "^10.6.0", "npm-run-all2": "^6.2.3", "nyc": "^17.0.0", "prettier": "^3.3.3", + "puppeteer-core": "^23.4.1", "sinon": "^18.0.0", "source-map": "^0.7.4", "standard-version": "^9.5.0", @@ -3139,6 +3141,65 @@ "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", "dev": true }, + "node_modules/@puppeteer/browsers": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", + "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==", + "dev": true, + "dependencies": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/@scure/base": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", @@ -3225,6 +3286,12 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3430,6 +3497,16 @@ "@types/node": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz", @@ -4193,6 +4270,18 @@ "node": "*" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/autorest": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/autorest/-/autorest-3.7.1.tgz", @@ -4221,6 +4310,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true + }, "node_modules/babel-loader": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", @@ -4378,6 +4473,53 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz", + "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz", + "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz", + "integrity": "sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==", + "dev": true, + "optional": true, + "dependencies": { + "b4a": "^1.6.6", + "streamx": "^2.20.0" + } + }, "node_modules/base-x": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", @@ -4402,6 +4544,15 @@ } ] }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -4567,6 +4718,15 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4799,6 +4959,20 @@ "node": ">=6.0" } }, + "node_modules/chromium-bidi": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz", + "integrity": "sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==", + "dev": true, + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -6045,6 +6219,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -6112,11 +6295,11 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6252,6 +6435,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/detect-browser": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", @@ -6284,6 +6481,12 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1342118", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1342118.tgz", + "integrity": "sha512-75fMas7PkYNDTmDyb6PRJCH7ILmHLp+BhrZGeMsa4bCh40DTxgCz2NRy5UDzII4C5KuD0oBMZ9vXKhEl6UD/3w==", + "dev": true + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -6785,6 +6988,37 @@ "node": ">=0.8.0" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -7457,11 +7691,52 @@ "node": ">=12.0.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -7513,6 +7788,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -7672,6 +7956,20 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -7911,6 +8209,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dev": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/git-raw-commits": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", @@ -8853,6 +9166,25 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -9479,6 +9811,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -9547,6 +9885,18 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -10127,6 +10477,12 @@ "node": ">=0.10.0" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -10263,12 +10619,6 @@ "node": ">=10" } }, - "node_modules/mocha/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 - }, "node_modules/mocha/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10372,9 +10722,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/napi-build-utils": { "version": "1.0.2", @@ -10394,6 +10744,15 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -11118,6 +11477,38 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-hash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", @@ -11249,6 +11640,12 @@ "node": "*" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -11473,6 +11870,49 @@ "node": ">=8" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -11501,6 +11941,23 @@ "node": ">=6" } }, + "node_modules/puppeteer-core": { + "version": "23.4.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.4.1.tgz", + "integrity": "sha512-uCxGtn8VE9PlKhdFJX/zZySi9K3Ufr3qUZe28jxJoZUqiMJOi+SFh2zhiFDSjWqZIDkc0FtnaCC+rewW3MYXmg==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "2.4.0", + "chromium-bidi": "0.6.5", + "debug": "^4.3.7", + "devtools-protocol": "0.0.1342118", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -11532,6 +11989,12 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -12361,6 +12824,44 @@ "node": ">=6" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -12651,6 +13152,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/streamx": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -12990,6 +13505,15 @@ "node": "*" } }, + "node_modules/text-decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz", + "integrity": "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -13297,6 +13821,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -13389,6 +13919,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -13446,6 +14010,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -13485,6 +14058,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, "node_modules/usb": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/usb/-/usb-2.9.0.tgz", @@ -14204,6 +14783,16 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -14224,6 +14813,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index fe780a0d54..601065ea70 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "lint:types": "tsc -p tsconfig.tests.json", "format": "prettier . --write", "test": "mocha './test/unit/' './test/integration/'", + "test:assets": "node tooling/fetch-metamask.mjs", "test:integration": "mocha './test/integration/'", "test:unit": "mocha './test/unit/'", "test:watch": "mocha './test/unit/' './test/integration/' --watch", @@ -125,11 +126,13 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-rulesdir": "^0.2.2", "eslint-plugin-tsdoc": "^0.3.0", + "extract-zip": "^2.0.1", "html-webpack-plugin": "^5.6.0", "mocha": "^10.6.0", "npm-run-all2": "^6.2.3", "nyc": "^17.0.0", "prettier": "^3.3.3", + "puppeteer-core": "^23.4.1", "sinon": "^18.0.0", "source-map": "^0.7.4", "standard-version": "^9.5.0", diff --git a/test/unit/metamask.ts b/test/unit/metamask.ts new file mode 100644 index 0000000000..d88035bd34 --- /dev/null +++ b/test/unit/metamask.ts @@ -0,0 +1,305 @@ +import puppeteer, { Page } from 'puppeteer-core'; +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import type { BaseProvider } from '@metamask/providers'; +import '..'; +import { + AccountMetamask, + AccountMetamaskFactory, + buildTx, + Tag, + unpackTx, + verify, + verifyMessage, + decode, + hash, +} from '../../src'; +import { assertNotNull } from '../utils'; + +const compareWithExtension = false; // switch to true for manual testing +// MetaMask should be initialized with mnemonic: +// eye quarter chapter suit cruel scrub verify stuff volume control learn dust +type Message = { request: object } | { resolve: unknown } | { reject: object }; +let page: Page; + +async function instructTester(action?: string): Promise { + if (!compareWithExtension) return; + await page.evaluate( + (t) => (document.body.innerHTML = t), + action != null ? `Press ${action.toUpperCase()}!` : 'Running tests...', + ); +} + +async function initProvider( + messageQueue: Message[], + fakeQueue: boolean = false, +): Promise { + after(() => expect(messageQueue).to.have.lengthOf(0)); + return { + async request(actualRequest: unknown) { + const expectedRequest = messageQueue.shift(); + assertNotNull(expectedRequest); + if (!('request' in expectedRequest)) + throw new Error(`Expected request, got ${JSON.stringify(expectedRequest)} instead`); + expect(actualRequest).to.be.eql(expectedRequest.request); + + const expectedResponse = messageQueue.shift(); + assertNotNull(expectedResponse); + if ('request' in expectedResponse) + throw new Error( + `Expected reject or resolve, got ${JSON.stringify(expectedRequest)} instead`, + ); + + if (compareWithExtension && !fakeQueue) { + const actualResponse = await page.evaluate(async (req): Promise => { + // @ts-expect-error executed in a browser + return window.ethereum.request(req).then( + (resolve: unknown) => ({ resolve }), + (reject: unknown) => ({ reject }), + ); + }, expectedRequest.request); + expect(actualResponse).to.be.eql(expectedResponse); + } + + // eslint-disable-next-line @typescript-eslint/no-throw-literal + if ('reject' in expectedResponse) throw expectedResponse.reject; + return expectedResponse.resolve; + }, + } as unknown as BaseProvider; +} + +describe('Aeternity Snap for MetaMask', function () { + this.timeout(compareWithExtension ? 60000 : 300); + + before(async () => { + if (!compareWithExtension) return; + + const metamaskDir = './test/assets/metamask/'; + const browser = await puppeteer.launch({ + executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + headless: false, + args: [`--disable-extensions-except=${metamaskDir}`, `--load-extension=${metamaskDir}`], + userDataDir: './test/assets/chrome-user-data', + }); + await new Promise((resolve) => setTimeout(resolve, 2000)); + [page] = await browser.pages(); + await page.goto('https://google.com/404'); + await page.evaluate(() => alert('Press OK when MetaMask is ready')); + await instructTester(); + + after(async () => browser.close()); + }); + + afterEach(async (): Promise => instructTester()); + + describe('factory', () => { + const snapDetails = { + blocked: false, + enabled: true, + id: 'npm:@aeternity-snap/plugin', + initialPermissions: { + snap_manageState: {}, + snap_dialog: {}, + 'endowment:rpc': { dapps: true, snaps: false }, + 'endowment:network-access': {}, + snap_getBip32Entropy: [{ path: ['m', "44'", "457'"], curve: 'ed25519' }], + }, + version: '0.0.9', + }; + + const versionChecks = [ + { request: { method: 'web3_clientVersion' } }, + { resolve: 'MetaMask/v12.3.1' }, + { request: { method: 'wallet_getSnaps' } }, + { resolve: { 'npm:@aeternity-snap/plugin': snapDetails } }, + ]; + + it('installs snap', async () => { + const provider = await initProvider([ + ...versionChecks.slice(0, 2), + { + request: { + method: 'wallet_requestSnaps', + params: { 'npm:@aeternity-snap/plugin': { version: '0.0.9' } }, + }, + }, + { + resolve: { + 'npm:@aeternity-snap/plugin': snapDetails, + }, + }, + ]); + const factory = new AccountMetamaskFactory(provider); + expect(await factory.installSnap()).to.be.eql(snapDetails); + }); + + it('gets snap version', async () => { + const provider = await initProvider([...versionChecks, ...versionChecks.slice(2, 2)]); + const factory = new AccountMetamaskFactory(provider); + expect(await factory.getSnapVersion()).to.be.equal('0.0.9'); + }); + + it('ensures that snap version is compatible', async () => { + const provider = await initProvider( + [ + ...versionChecks.slice(0, 3), + { resolve: { 'npm:@aeternity-snap/plugin': { ...snapDetails, version: '1.0.0' } } }, + ], + true, + ); + const factory = new AccountMetamaskFactory(provider); + await expect(factory.initialize(42)).to.be.rejectedWith( + 'Unsupported Aeternity snap in MetaMask version 1.0.0. Supported: >= 0.0.9 < 0.1.0', + ); + }); + + const getPublicKeyRequest = { + request: { + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:@aeternity-snap/plugin', + request: { method: 'getPublicKey', params: { derivationPath: ["42'", "0'", "0'"] } }, + }, + }, + }; + + it('initializes an account', async () => { + const provider = await initProvider([ + ...versionChecks, + getPublicKeyRequest, + { resolve: { publicKey: 'ak_2HteeujaJzutKeFZiAmYTzcagSoRErSXpBFV179xYgqT4teakv' } }, + ]); + const factory = new AccountMetamaskFactory(provider); + await instructTester('approve'); + const account = await factory.initialize(42); + expect(account).to.be.instanceOf(AccountMetamask); + expect(account.address).to.be.equal('ak_2HteeujaJzutKeFZiAmYTzcagSoRErSXpBFV179xYgqT4teakv'); + expect(account.index).to.be.equal(42); + }); + + it('initializes an account rejected', async () => { + const provider = await initProvider([ + ...versionChecks, + getPublicKeyRequest, + { reject: { code: 4001, message: 'User rejected the request.' } }, + ]); + const factory = new AccountMetamaskFactory(provider); + await instructTester('reject'); + await expect(factory.initialize(42)).to.be.rejectedWith('User rejected the request.'); + }); + }); + + describe('account', () => { + const address = 'ak_2swhLkgBPeeADxVTAVCJnZLY5NZtCFiM93JxsEaMuC59euuFRQ'; + it('fails on calling raw signing', async () => { + const provider = await initProvider([]); + const account = new AccountMetamask(provider, 0, address); + await expect(account.sign()).to.be.rejectedWith('RAW signing using MetaMask'); + }); + + const transaction = buildTx({ + tag: Tag.SpendTx, + senderId: address, + recipientId: address, + amount: 1.23e18, + nonce: 10, + }); + + const signTransactionRequest = { + request: { + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:@aeternity-snap/plugin', + request: { + method: 'signTransaction', + params: { + derivationPath: ["0'", "0'", "0'"], + networkId: 'ae_uat', + tx: 'tx_+FkMAaEB915T9XgiInpYtGMJXW2rZXyrgEV0vmLeC+H5UnnQkDehAfdeU/V4IiJ6WLRjCV1tq2V8q4BFdL5i3gvh+VJ50JA3iBER1nuxuwAAhg9MNiAIAAAKgKZ39DI=', + }, + }, + }, + }, + }; + + it('signs transaction', async () => { + const provider = await initProvider([ + signTransactionRequest, + { + resolve: { + publicKey: 'ak_2swhLkgBPeeADxVTAVCJnZLY5NZtCFiM93JxsEaMuC59euuFRQ', + signedTx: + 'tx_+KMLAfhCuED4aPHGzpufKzrsvsBMantciuMPXA6H288X+5lmPMIuQapu210e41Z4FkyD1b3IzYzvMIs+z5b1Pzy9YXMgQewNuFv4WQwBoQH3XlP1eCIieli0YwldbatlfKuARXS+Yt4L4flSedCQN6EB915T9XgiInpYtGMJXW2rZXyrgEV0vmLeC+H5UnnQkDeIERHWe7G7AACGD0w2IAgAAAqAja1dTA==', + }, + }, + ]); + const account = new AccountMetamask(provider, 0, address); + const networkId = 'ae_uat'; + await instructTester('approve'); + const signedTransaction = await account.signTransaction(transaction, { networkId }); + expect(signedTransaction).to.satisfy((t: string) => t.startsWith('tx_')); + const { + signatures: [signature], + } = unpackTx(signedTransaction, Tag.SignedTx); + const hashedTx = Buffer.concat([Buffer.from(networkId), hash(decode(transaction))]); + expect(verify(hashedTx, signature, address)).to.be.equal(true); + }); + + it('signs transaction rejected', async () => { + const provider = await initProvider([ + signTransactionRequest, + { reject: { code: 4001, message: 'User rejected the request.' } }, + ]); + const account = new AccountMetamask(provider, 0, address); + await instructTester('reject'); + await expect( + account.signTransaction(transaction, { networkId: 'ae_uat' }), + ).to.be.rejectedWith('User rejected the request.'); + }); + + const message = 'test-message,'.repeat(3); + const signMessageRequest = { + request: { + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:@aeternity-snap/plugin', + request: { + method: 'signMessage', + params: { + derivationPath: ["0'", "0'", "0'"], + message: 'dGVzdC1tZXNzYWdlLHRlc3QtbWVzc2FnZSx0ZXN0LW1lc3NhZ2Us', + }, + }, + }, + }, + }; + + it('signs message', async () => { + const provider = await initProvider([ + signMessageRequest, + { + resolve: { + publicKey: 'ak_2swhLkgBPeeADxVTAVCJnZLY5NZtCFiM93JxsEaMuC59euuFRQ', + signature: + 'eDl+GGBY8niDW44+hmlg5EGNwenwCzokI/V8FgIciHIBGeuzNzoTYRLKocn/Y4cAkgZGWessZB3Wd2fxXIA1DA==', + }, + }, + ]); + const account = new AccountMetamask(provider, 0, address); + await instructTester('approve'); + const signature = await account.signMessage(message); + expect(signature).to.be.instanceOf(Uint8Array); + expect(verifyMessage(message, signature, address)).to.be.equal(true); + }); + + it('signs message rejected', async () => { + const provider = await initProvider([ + signMessageRequest, + { reject: { code: 4001, message: 'User rejected the request.' } }, + ]); + const account = new AccountMetamask(provider, 0, address); + await instructTester('reject'); + await expect(account.signMessage(message)).to.be.rejectedWith('User rejected the request.'); + }); + }); +}); diff --git a/tooling/fetch-metamask.mjs b/tooling/fetch-metamask.mjs new file mode 100644 index 0000000000..af73e2396b --- /dev/null +++ b/tooling/fetch-metamask.mjs @@ -0,0 +1,18 @@ +import { writeFileSync } from 'fs'; +import { resolve } from 'path'; +import extractZip from 'extract-zip'; +// eslint-disable-next-line import/extensions +import restoreFile from './restore-file.mjs'; + +const path = './test/assets/metamask.zip'; +const hash = + 'syt/OJLdXM1al3TdG7s/xXRYFj0mTZ6UDrK/+KzTmvLxhfQdIO8/82MQbep2CR67Gwz8wRaM1TZzpK3dyqjNSg=='; + +await restoreFile(path, hash, async () => { + const request = await fetch( + 'https://github.com/MetaMask/metamask-extension/releases/download/v12.3.1/metamask-chrome-12.3.1.zip', + ); + const body = Buffer.from(await request.arrayBuffer()); + writeFileSync(path, body); + await extractZip(path, { dir: resolve(path).split('.')[0] }); +}); From 5047e4374afcee6f949b7c6678a15f6de5212d99 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Thu, 26 Sep 2024 18:52:12 +1000 Subject: [PATCH 05/10] feat(account): add `ensureReady` method to AccountLedgerFactory --- .../aepp/src/components/ConnectLedger.vue | 4 +- src/account/Ledger.ts | 18 +++---- src/account/LedgerFactory.ts | 49 ++++++++++++------- test/unit/ledger.ts | 10 ++-- 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/examples/browser/aepp/src/components/ConnectLedger.vue b/examples/browser/aepp/src/components/ConnectLedger.vue index a296a2a9aa..4dcdbe5867 100644 --- a/examples/browser/aepp/src/components/ConnectLedger.vue +++ b/examples/browser/aepp/src/components/ConnectLedger.vue @@ -24,9 +24,11 @@ import { mapState } from 'vuex'; import TransportWebUSB from '@ledgerhq/hw-transport-webusb'; export default { + created() { + this.accountFactory = null; + }, data: () => ({ status: '', - accountFactory: null, accounts: [], }), computed: mapState(['aeSdk']), diff --git a/src/account/Ledger.ts b/src/account/Ledger.ts index a6aaa0dddc..9e5bcff697 100644 --- a/src/account/Ledger.ts +++ b/src/account/Ledger.ts @@ -15,24 +15,18 @@ export const SIGN_PERSONAL_MESSAGE = 0x08; * Ledger wallet account class */ export default class AccountLedger extends AccountBase { - readonly transport: Transport; - - override readonly address: Encoded.AccountAddress; - - readonly index: number; - /** * @param transport - Connection to Ledger to use * @param index - Index of account * @param address - Address of account */ - constructor(transport: Transport, index: number, address: Encoded.AccountAddress) { + constructor( + readonly transport: Transport, + readonly index: number, + override readonly address: Encoded.AccountAddress, + ) { super(); - this.transport = transport; - this.index = index; - this.address = address; - const scrambleKey = 'w0w'; - transport.decorateAppAPIMethods(this, ['signTransaction', 'signMessage'], scrambleKey); + transport.decorateAppAPIMethods(this, ['signTransaction', 'signMessage'], 'w0w'); } // eslint-disable-next-line class-methods-use-this diff --git a/src/account/LedgerFactory.ts b/src/account/LedgerFactory.ts index 9463d6fb5e..3ad1744b95 100644 --- a/src/account/LedgerFactory.ts +++ b/src/account/LedgerFactory.ts @@ -5,46 +5,62 @@ import { Encoded } from '../utils/encoder'; import semverSatisfies from '../utils/semver-satisfies'; import AccountBaseFactory from './BaseFactory'; +interface AppConfiguration { + version: string; +} + /** * A factory class that generates instances of AccountLedger based on provided transport. */ export default class AccountLedgerFactory extends AccountBaseFactory { - readonly transport: Transport; - - private readonly versionCheckPromise: Promise; - /** * @param transport - Connection to Ledger to use */ - constructor(transport: Transport) { + constructor(readonly transport: Transport) { super(); - this.transport = transport; - this.versionCheckPromise = this.getAppConfiguration().then(({ version }) => { - const args = [version, '0.4.4', '0.5.0'] as const; - if (!semverSatisfies(...args)) throw new UnsupportedVersionError('app on ledger', ...args); - }); - const scrambleKey = 'w0w'; - transport.decorateAppAPIMethods(this, ['getAddress', 'getAppConfiguration'], scrambleKey); + transport.decorateAppAPIMethods(this, ['getAddress', 'getAppConfiguration'], 'w0w'); } + #ensureReadyPromise?: Promise; + /** - * @returns the version of app installed on Ledger wallet + * It throws an exception if Aeternity app on Ledger has an incompatible version, not opened or + * not installed. */ - async getAppConfiguration(): Promise<{ version: string }> { - await this.versionCheckPromise; + async ensureReady(): Promise { + const { version } = await this.#getAppConfiguration(); + const args = [version, '0.4.4', '0.5.0'] as const; + if (!semverSatisfies(...args)) + throw new UnsupportedVersionError('Aeternity app on Ledger', ...args); + this.#ensureReadyPromise = Promise.resolve(); + } + + async #ensureReady(): Promise { + this.#ensureReadyPromise ??= this.ensureReady(); + return this.#ensureReadyPromise; + } + + async #getAppConfiguration(): Promise { const response = await this.transport.send(CLA, GET_APP_CONFIGURATION, 0x00, 0x00); return { version: [response[1], response[2], response[3]].join('.'), }; } + /** + * @returns the version of Aeternity app installed on Ledger wallet + */ + async getAppConfiguration(): Promise { + return this.#getAppConfiguration(); + } + /** * Get `ak_`-prefixed address for a given account index. * @param accountIndex - Index of account * @param verify - Ask user to confirm address by showing it on the device screen */ async getAddress(accountIndex: number, verify = false): Promise { - await this.versionCheckPromise; + await this.#ensureReady(); const buffer = Buffer.alloc(4); buffer.writeUInt32BE(accountIndex, 0); const response = await this.transport.send( @@ -63,7 +79,6 @@ export default class AccountLedgerFactory extends AccountBaseFactory { * @param accountIndex - Index of account */ async initialize(accountIndex: number): Promise { - await this.versionCheckPromise; return new AccountLedger(this.transport, accountIndex, await this.getAddress(accountIndex)); } } diff --git a/test/unit/ledger.ts b/test/unit/ledger.ts index 223771dcea..9975f5453f 100644 --- a/test/unit/ledger.ts +++ b/test/unit/ledger.ts @@ -46,18 +46,18 @@ async function initTransport(s: string, i = false): Promise { afterEach(async () => { if (compareWithRealDevice && !ignoreRealDevice) { - expect(recordStore.toString()).to.be.equal(expectedRecordStore); + expect(recordStore.toString().trim()).to.be.equal(expectedRecordStore); } expectedRecordStore = ''; ignoreRealDevice = false; }); -describe('Ledger HW', () => { +describe('Ledger HW', function () { + this.timeout(compareWithRealDevice ? 60000 : 300); + describe('factory', () => { it('gets app version', async () => { await initTransport(indent` - => e006000000 - <= 000004049000 => e006000000 <= 000004049000`); const factory = new AccountLedgerFactory(transport); @@ -73,7 +73,7 @@ describe('Ledger HW', () => { ); const factory = new AccountLedgerFactory(transport); await expect(factory.getAddress(42)).to.be.rejectedWith( - 'Unsupported app on ledger version 1.4.4. Supported: >= 0.4.4 < 0.5.0', + 'Unsupported Aeternity app on Ledger version 1.4.4. Supported: >= 0.4.4 < 0.5.0', ); }); From 53f7100431cd0165a0c7af177bf2aa352a5ec6ff Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 2 Oct 2024 12:17:17 +1000 Subject: [PATCH 06/10] test(account): define a local transport in Ledger tests --- test/unit/ledger.ts | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/test/unit/ledger.ts b/test/unit/ledger.ts index 9975f5453f..89e71747e9 100644 --- a/test/unit/ledger.ts +++ b/test/unit/ledger.ts @@ -26,22 +26,20 @@ import { indent } from '../utils'; const compareWithRealDevice = false; // switch to true for manual testing // ledger should be initialized with mnemonic: // eye quarter chapter suit cruel scrub verify stuff volume control learn dust -let transport: Transport; let recordStore: RecordStore; let expectedRecordStore = ''; let ignoreRealDevice = false; -async function initTransport(s: string, i = false): Promise { +async function initTransport(s: string, i = false): Promise { expectedRecordStore = s; ignoreRealDevice = i; if (compareWithRealDevice && !ignoreRealDevice) { - transport = await TransportNodeHid.create(); + const t = await TransportNodeHid.create(); recordStore = new RecordStore(); - const TransportRecorder = createTransportRecorder(transport, recordStore); - transport = new TransportRecorder(transport); - } else { - transport = await openTransportReplayer(RecordStore.fromString(expectedRecordStore)); + const TransportRecorder = createTransportRecorder(t, recordStore); + return new TransportRecorder(t); } + return openTransportReplayer(RecordStore.fromString(expectedRecordStore)); } afterEach(async () => { @@ -57,7 +55,7 @@ describe('Ledger HW', function () { describe('factory', () => { it('gets app version', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e006000000 <= 000004049000`); const factory = new AccountLedgerFactory(transport); @@ -65,7 +63,7 @@ describe('Ledger HW', function () { }); it('ensures that app version is compatible', async () => { - await initTransport( + const transport = await initTransport( indent` => e006000000 <= 000104049000`, @@ -78,7 +76,7 @@ describe('Ledger HW', function () { }); it('gets address', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e006000000 <= 000004049000 => e0020000040000002a @@ -90,7 +88,7 @@ describe('Ledger HW', function () { }); it('gets address with verification', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e006000000 <= 000004049000 => e0020100040000002a @@ -102,7 +100,7 @@ describe('Ledger HW', function () { }); it('gets address with verification rejected', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e006000000 <= 000004049000 => e0020100040000002a @@ -114,7 +112,7 @@ describe('Ledger HW', function () { }); it('initializes an account', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e006000000 <= 000004049000 => e0020000040000002a @@ -149,7 +147,7 @@ describe('Ledger HW', function () { } it('discovers accounts', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e006000000 <= 000004049000 => e00200000400000000 @@ -168,7 +166,7 @@ describe('Ledger HW', function () { }); it('discovers accounts on unused ledger', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e006000000 <= 000004049000 => e00200000400000000 @@ -183,7 +181,7 @@ describe('Ledger HW', function () { describe('account', () => { const address = 'ak_2swhLkgBPeeADxVTAVCJnZLY5NZtCFiM93JxsEaMuC59euuFRQ'; it('fails on calling raw signing', async () => { - await initTransport('\n'); + const transport = await initTransport('\n'); const account = new AccountLedger(transport, 0, address); await expect(account.sign()).to.be.rejectedWith('RAW signing using Ledger HW'); }); @@ -197,7 +195,7 @@ describe('Ledger HW', function () { }); it('signs transaction', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e00400006a000000000000005b0661655f756174f8590c01a101f75e53f57822227a58b463095d6dab657cab804574be62de0be1f95279d09037a101f75e53f57822227a58b463095d6dab657cab804574be62de0be1f95279d09037881111d67bb1bb0000860f4c36200800000a80 <= f868f1c6ce9b9f2b3aecbec04c6a7b5c8ae30f5c0e87dbcf17fb99663cc22e41aa6edb5d1ee35678164c83d5bdc8cd8cef308b3ecf96f53f3cbd61732041ec0d9000`); const account = new AccountLedger(transport, 0, address); @@ -212,7 +210,7 @@ describe('Ledger HW', function () { }); it('signs transaction rejected', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e00400006a000000000000005b0661655f756174f8590c01a101f75e53f57822227a58b463095d6dab657cab804574be62de0be1f95279d09037a101f75e53f57822227a58b463095d6dab657cab804574be62de0be1f95279d09037881111d67bb1bb0000860f4c36200800000a80 <= 6985`); const account = new AccountLedger(transport, 0, address); @@ -226,7 +224,7 @@ describe('Ledger HW', function () { const message = 'test-message,'.repeat(3); it('signs message', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e00800002f0000000000000027746573742d6d6573736167652c746573742d6d6573736167652c746573742d6d6573736167652c <= 78397e186058f278835b8e3e866960e4418dc1e9f00b3a2423f57c16021c88720119ebb3373a136112caa1c9ff63870092064659eb2c641dd67767f15c80350c9000`); const account = new AccountLedger(transport, 0, address); @@ -236,7 +234,7 @@ describe('Ledger HW', function () { }); it('signs message rejected', async () => { - await initTransport(indent` + const transport = await initTransport(indent` => e00800002f0000000000000027746573742d6d6573736167652c746573742d6d6573736167652c746573742d6d6573736167652c <= 6985`); const account = new AccountLedger(transport, 0, address); From db9e67dbc14aec7e7043b71feb5a16289f7ad614 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 2 Oct 2024 13:21:31 +1000 Subject: [PATCH 07/10] test(account): remove global variable in Ledger tests --- test/unit/ledger.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/unit/ledger.ts b/test/unit/ledger.ts index 89e71747e9..799828fd8d 100644 --- a/test/unit/ledger.ts +++ b/test/unit/ledger.ts @@ -28,26 +28,24 @@ const compareWithRealDevice = false; // switch to true for manual testing // eye quarter chapter suit cruel scrub verify stuff volume control learn dust let recordStore: RecordStore; let expectedRecordStore = ''; -let ignoreRealDevice = false; -async function initTransport(s: string, i = false): Promise { +async function initTransport(s: string, ignoreRealDevice = false): Promise { expectedRecordStore = s; - ignoreRealDevice = i; if (compareWithRealDevice && !ignoreRealDevice) { const t = await TransportNodeHid.create(); recordStore = new RecordStore(); const TransportRecorder = createTransportRecorder(t, recordStore); return new TransportRecorder(t); } - return openTransportReplayer(RecordStore.fromString(expectedRecordStore)); + recordStore = RecordStore.fromString(expectedRecordStore); + return openTransportReplayer(recordStore); } afterEach(async () => { - if (compareWithRealDevice && !ignoreRealDevice) { + if (compareWithRealDevice) { expect(recordStore.toString().trim()).to.be.equal(expectedRecordStore); } expectedRecordStore = ''; - ignoreRealDevice = false; }); describe('Ledger HW', function () { From 09829c2da7ac178bc60308a71b07331d03081eea Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 2 Oct 2024 15:16:49 +1000 Subject: [PATCH 08/10] docs(account): error handling in Ledger --- docs/guides/ledger-wallet.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/guides/ledger-wallet.md b/docs/guides/ledger-wallet.md index 3d77d3bd5f..48a2d16fb4 100644 --- a/docs/guides/ledger-wallet.md +++ b/docs/guides/ledger-wallet.md @@ -6,10 +6,10 @@ This guide explains basic interactions on getting access to aeternity accounts o Run the code from below you need: -- a Ledger Hardware Wallet like Ledger Nano X, Ledger Nano S -- to install [Ledger Live](https://www.ledger.com/ledger-live) -- to install aeternity@0.4.4 or above app from Ledger Live to HW -- to have Ledger HW connected to computer, unlocked, with aeternity app opened +- a Ledger Hardware Wallet like Ledger Nano X, Ledger Nano S; +- to install [Ledger Live](https://www.ledger.com/ledger-live); +- to install aeternity@0.4.4 or above app from Ledger Live to HW; +- to have Ledger HW connected to computer, unlocked, with aeternity app opened. ## Usage @@ -31,7 +31,7 @@ console.log(account.address); // 'ak_2dA...' console.log(await account.signTransaction('tx_...')); // 'tx_...' (with signature added) ``` -The private key for the account would be derived on the Ledger device using the provided index and the mnemonic phrase it was initialized with. +The private key for the account would be derived on the Ledger device using the provided index and the mnemonic phrase it was initialized with. The private key won't leave the device. The complete examples of how to use it in nodejs and browser can be found [here](https://github.com/aeternity/aepp-sdk-js/tree/71da12b5df56b41f7317d1fb064e44e8ea118d6c/test/environment/ledger). @@ -69,3 +69,7 @@ const node = new Node('https://testnet.aeternity.io'); const accounts = await accountFactory.discover(node); console.log(accounts[0].address); // 'ak_2dA...' ``` + +## Error handling + +If the user rejects a transaction/message signing or address confirmation you will get an exception inherited from TransportStatusError (exposed in '@ledgerhq/hw-transport' package). With the message "Ledger device: Condition of use not satisfied (denied by the user?) (0x6985)". Also, `statusCode` equals 0x6985, and `statusText` equals `CONDITIONS_OF_USE_NOT_SATISFIED`. From 084b03b800ff06b881a7d10ee43bf5e8cd6ff65e Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 2 Oct 2024 16:39:35 +1000 Subject: [PATCH 09/10] docs(account): add a guide about AccountMetamaskFactory --- docs/guides/metamask-snap.md | 72 ++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 73 insertions(+) create mode 100644 docs/guides/metamask-snap.md diff --git a/docs/guides/metamask-snap.md b/docs/guides/metamask-snap.md new file mode 100644 index 0000000000..2f2033d2a6 --- /dev/null +++ b/docs/guides/metamask-snap.md @@ -0,0 +1,72 @@ +# Aeternity snap for MetaMask + +This guide explains basic interactions on getting access to accounts on Aeternity snap for MetaMask using JS SDK. + +## Prerequisite + +Run the code from below you need: + +- a MetaMask extension 12.2.4 or above installed in Chrome or Firefox browser; +- to setup an account in MetaMask (create a new one or restore by mnemonic phrase). + +## Usage + +Firstly, you need to create a factory of MetaMask accounts + +```js +import { AccountMetamaskFactory } from '@aeternity/aepp-sdk'; + +const accountFactory = new AccountMetamaskFactory(); +``` + +The next step is to install Aeternity snap to MetaMask. You can request installation by calling + +```js +await accountFactory.installSnap(); +``` + +If succeed it means that MetaMask is ready to provide access to accounts. Alternatively, you can call `ensureReady` instead of `installSnap`. The latter won't trigger a snap installation, it would just fall with the exception if not installed. + +Using the factory, you can create instances of specific accounts by providing an index + +```js +const account = await accountFactory.initialize(0); +console.log(account.address); // 'ak_2dA...' +console.log(await account.signTransaction('tx_...')); // 'tx_...' (with signature added) +``` + +The private key for the account would be derived in the MetaMask browser extension using the provided index and the mnemonic phrase it was initialized with. The private key won't leave the extension. + +The complete examples of how to use it in browser can be found [here](https://github.com/aeternity/aepp-sdk-js/tree/develop/examples/browser/aepp/src/components/ConnectMetamask.vue). + +## Account persistence + +Account can be persisted and restored by saving values of `index`, `address` properties + +```js +import { AccountMetamask } from '@aeternity/aepp-sdk'; + +const accountIndex = accountToPersist.index; +const accountAddress = accountToPersist.address; + +const accountFactory = new AccountMetamaskFactory(); +const restoredAccount = new AccountMetamask(accountFactory.provider, accountIndex, accountAddress); +``` + +It can be used to remember accounts between app restarts. + +## Account discovery + +In addition to the above, it is possible to get access to a sequence of accounts that already have been used on chain. It is needed, for example, to restore the previously used accounts in case the user connects MetaMask to an app that doesn't aware of them. + +```js +import { Node } from '@aeternity/aepp-sdk'; + +const node = new Node('https://testnet.aeternity.io'); +const accounts = await accountFactory.discover(node); +console.log(accounts[0].address); // 'ak_2dA...' +``` + +## Error handling + +If the user rejects a transaction/message signing or address retrieving you will get an exception as a plain object with property `code` equals 4001, and `message` equals "User rejected the request.". diff --git a/mkdocs.yml b/mkdocs.yml index 831a3034c8..53ca602a40 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ nav: - guides/connect-aepp-to-wallet.md - guides/build-wallet.md - guides/ledger-wallet.md + - guides/metamask-snap.md - transaction-options.md - Examples & Tutorials: - NodeJS: From 0831f081685783c6065da8dee1d3a2674fb11163 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 2 Oct 2024 17:23:04 +1000 Subject: [PATCH 10/10] ci: fix erlang installation --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 987c5645d1..6c265206bb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,9 +5,9 @@ on: pull_request: jobs: main: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - - run: sudo apt install erlang + - run: sudo apt update && sudo apt install --no-install-recommends erlang - uses: actions/checkout@v4 with: fetch-depth: 100