diff --git a/README.md b/README.md index 8cf6f384..b5de45db 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ When the simulator iframe opens any page, we inject the build/inject.js script a The injected script then runs in the context of the Dapp and injects an [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) compatible API at `window.ethereum`. The injected provider forwards all `request` calls to the parent extension page via `window.postMessage` where they are recorded and executed in a fork of the connected network. +We also subscribe to messages sent via the [safe-apps-sdk](https://github.com/safe-global/safe-apps-sdk). +This enables instant and smart-account optimized connections to Safe-compatible apps. + ### Simulating transaction in a fork When the provider we inject into the Dapp iframe receives a transaction request, we record it and simulate the transaction in a fork of the target network, impersonating the Safe. diff --git a/extension/.cspell.json b/extension/.cspell.json index a18fc161..1be2fc4a 100644 --- a/extension/.cspell.json +++ b/extension/.cspell.json @@ -14,6 +14,7 @@ "blockies", "borderless", "Bytecode", + "ccip", "cowswap", "Delegatecall", "delegatecalls", diff --git a/extension/.eslintrc.json b/extension/.eslintrc.json index 1552799e..37d3ad63 100644 --- a/extension/.eslintrc.json +++ b/extension/.eslintrc.json @@ -20,7 +20,15 @@ "@typescript-eslint/no-non-null-assertion": "off", "react/prop-types": "off", // we are not exporting any components so this rule is not super relevant, in tests we don't want to be forced to define prop types "jsx-a11y/no-onchange": "off", // this rule is deprecated, but somehow still part of jsx-a11y/recommended - "jsx-a11y/label-has-associated-control": "off" // this rule gives false positives when the control lives in the component of a child element + "jsx-a11y/label-has-associated-control": "off", // this rule gives false positives when the control lives in the component of a child element + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ] }, "settings": { "react": { diff --git a/extension/.pnp.cjs b/extension/.pnp.cjs index 4a55529c..1fe983b0 100755 --- a/extension/.pnp.cjs +++ b/extension/.pnp.cjs @@ -34,7 +34,9 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@gnosis.pm/zodiac", "npm:3.4.2"],\ ["@safe-global/api-kit", "npm:1.3.1"],\ ["@safe-global/protocol-kit", "npm:1.3.0"],\ + ["@safe-global/safe-apps-sdk", "npm:9.0.0"],\ ["@safe-global/safe-core-sdk-types", "npm:2.3.0"],\ + ["@safe-global/safe-gateway-typescript-sdk", "npm:3.14.0"],\ ["@shazow/whatsabi", "npm:0.2.1"],\ ["@testing-library/jest-dom", "npm:5.17.0"],\ ["@typechain/ethers-v5", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:10.2.1"],\ @@ -110,6 +112,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@adraffy/ens-normalize", [\ + ["npm:1.10.0", {\ + "packageLocation": "./.yarn/cache/@adraffy-ens-normalize-npm-1.10.0-7dfdaa4813-af0540f963.zip/node_modules/@adraffy/ens-normalize/",\ + "packageDependencies": [\ + ["@adraffy/ens-normalize", "npm:1.10.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@ampproject/remapping", [\ ["npm:2.2.1", {\ "packageLocation": "./.yarn/cache/@ampproject-remapping-npm-2.2.1-3da3d624be-03c04fd526.zip/node_modules/@ampproject/remapping/",\ @@ -2792,6 +2803,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@noble/hashes", "npm:1.3.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.2.0", {\ + "packageLocation": "./.yarn/cache/@noble-curves-npm-1.2.0-9b40ee1239-bb798d7a66.zip/node_modules/@noble/curves/",\ + "packageDependencies": [\ + ["@noble/curves", "npm:1.2.0"],\ + ["@noble/hashes", "npm:1.3.2"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@noble/hashes", [\ @@ -2808,6 +2827,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@noble/hashes", "npm:1.3.2"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.3.3", {\ + "packageLocation": "./.yarn/cache/@noble-hashes-npm-1.3.3-f7374e6cdf-8a6496d1c0.zip/node_modules/@noble/hashes/",\ + "packageDependencies": [\ + ["@noble/hashes", "npm:1.3.3"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@nodelib/fs.scandir", [\ @@ -3087,6 +3113,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@safe-global/safe-apps-sdk", [\ + ["npm:9.0.0", {\ + "packageLocation": "./.yarn/cache/@safe-global-safe-apps-sdk-npm-9.0.0-af49618fd8-91d233d39d.zip/node_modules/@safe-global/safe-apps-sdk/",\ + "packageDependencies": [\ + ["@safe-global/safe-apps-sdk", "npm:9.0.0"],\ + ["@safe-global/safe-gateway-typescript-sdk", "npm:3.14.0"],\ + ["viem", "virtual:af49618fd849d3a67883e14ce09cd985491b6c4b9ca6f2df91faaa54fadf986938b8933492a7fe320de922caf8dada17507747628aa5be9a3ec82a58ae951347#npm:1.21.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@safe-global/safe-core-sdk-types", [\ ["npm:2.3.0", {\ "packageLocation": "./.yarn/cache/@safe-global-safe-core-sdk-types-npm-2.3.0-97f472e8ad-d0d1564ad8.zip/node_modules/@safe-global/safe-core-sdk-types/",\ @@ -3111,6 +3148,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@safe-global/safe-gateway-typescript-sdk", [\ + ["npm:3.14.0", {\ + "packageLocation": "./.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.14.0-97261ee2e0-a5242ba434.zip/node_modules/@safe-global/safe-gateway-typescript-sdk/",\ + "packageDependencies": [\ + ["@safe-global/safe-gateway-typescript-sdk", "npm:3.14.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@scure/base", [\ ["npm:1.1.3", {\ "packageLocation": "./.yarn/cache/@scure-base-npm-1.1.3-4126a221a4-1606ab8a4d.zip/node_modules/@scure/base/",\ @@ -3118,6 +3164,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@scure/base", "npm:1.1.3"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.1.5", {\ + "packageLocation": "./.yarn/cache/@scure-base-npm-1.1.5-d9203f3027-9e9ee6088c.zip/node_modules/@scure/base/",\ + "packageDependencies": [\ + ["@scure/base", "npm:1.1.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@scure/bip32", [\ @@ -3130,6 +3183,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@scure/base", "npm:1.1.3"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.3.2", {\ + "packageLocation": "./.yarn/cache/@scure-bip32-npm-1.3.2-3a1cfaf4f0-c5ae84fae4.zip/node_modules/@scure/bip32/",\ + "packageDependencies": [\ + ["@scure/bip32", "npm:1.3.2"],\ + ["@noble/curves", "npm:1.2.0"],\ + ["@noble/hashes", "npm:1.3.3"],\ + ["@scure/base", "npm:1.1.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@scure/bip39", [\ @@ -4477,6 +4540,30 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "SOFT"\ }],\ + ["npm:0.9.8", {\ + "packageLocation": "./.yarn/cache/abitype-npm-0.9.8-ee830ee479-d7d887f29d.zip/node_modules/abitype/",\ + "packageDependencies": [\ + ["abitype", "npm:0.9.8"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:0.9.8", {\ + "packageLocation": "./.yarn/__virtual__/abitype-virtual-be864bae11/0/cache/abitype-npm-0.9.8-ee830ee479-d7d887f29d.zip/node_modules/abitype/",\ + "packageDependencies": [\ + ["abitype", "virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:0.9.8"],\ + ["@types/typescript", null],\ + ["@types/zod", null],\ + ["typescript", null],\ + ["zod", null]\ + ],\ + "packagePeers": [\ + "@types/typescript",\ + "@types/zod",\ + "typescript",\ + "zod"\ + ],\ + "linkType": "HARD"\ + }],\ ["virtual:cdd346606a070fb60e58475688ed793487c0cb6c84665b9c733b0c10abeff25f491476b06d982f3dc97b1695f7aaa27f4d6b39917976f4916fe6d7f1649e897c#npm:0.7.1", {\ "packageLocation": "./.yarn/__virtual__/abitype-virtual-78c9c54f2c/0/cache/abitype-npm-0.7.1-b30f086406-de0d7082d2.zip/node_modules/abitype/",\ "packageDependencies": [\ @@ -9899,6 +9986,28 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["isows", [\ + ["npm:1.0.3", {\ + "packageLocation": "./.yarn/cache/isows-npm-1.0.3-aa8c925c69-9cacd5cf59.zip/node_modules/isows/",\ + "packageDependencies": [\ + ["isows", "npm:1.0.3"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:1.0.3", {\ + "packageLocation": "./.yarn/__virtual__/isows-virtual-fe8f9cb87f/0/cache/isows-npm-1.0.3-aa8c925c69-9cacd5cf59.zip/node_modules/isows/",\ + "packageDependencies": [\ + ["isows", "virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:1.0.3"],\ + ["@types/ws", null],\ + ["ws", "virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:8.13.0"]\ + ],\ + "packagePeers": [\ + "@types/ws",\ + "ws"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["isstream", [\ ["npm:0.1.2", {\ "packageLocation": "./.yarn/cache/isstream-npm-0.1.2-8581c75385-1eb2fe63a7.zip/node_modules/isstream/",\ @@ -15891,6 +16000,36 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["viem", [\ + ["npm:1.21.4", {\ + "packageLocation": "./.yarn/cache/viem-npm-1.21.4-a58095c18e-c351fdea2d.zip/node_modules/viem/",\ + "packageDependencies": [\ + ["viem", "npm:1.21.4"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:af49618fd849d3a67883e14ce09cd985491b6c4b9ca6f2df91faaa54fadf986938b8933492a7fe320de922caf8dada17507747628aa5be9a3ec82a58ae951347#npm:1.21.4", {\ + "packageLocation": "./.yarn/__virtual__/viem-virtual-52378067c5/0/cache/viem-npm-1.21.4-a58095c18e-c351fdea2d.zip/node_modules/viem/",\ + "packageDependencies": [\ + ["viem", "virtual:af49618fd849d3a67883e14ce09cd985491b6c4b9ca6f2df91faaa54fadf986938b8933492a7fe320de922caf8dada17507747628aa5be9a3ec82a58ae951347#npm:1.21.4"],\ + ["@adraffy/ens-normalize", "npm:1.10.0"],\ + ["@noble/curves", "npm:1.2.0"],\ + ["@noble/hashes", "npm:1.3.2"],\ + ["@scure/bip32", "npm:1.3.2"],\ + ["@scure/bip39", "npm:1.2.1"],\ + ["@types/typescript", null],\ + ["abitype", "virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:0.9.8"],\ + ["isows", "virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:1.0.3"],\ + ["typescript", null],\ + ["ws", "virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:8.13.0"]\ + ],\ + "packagePeers": [\ + "@types/typescript",\ + "typescript"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["vm-browserify", [\ ["npm:1.1.2", {\ "packageLocation": "./.yarn/cache/vm-browserify-npm-1.1.2-f96404b36f-10a1c50aab.zip/node_modules/vm-browserify/",\ @@ -16699,6 +16838,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "SOFT"\ }],\ + ["npm:8.13.0", {\ + "packageLocation": "./.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip/node_modules/ws/",\ + "packageDependencies": [\ + ["ws", "npm:8.13.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ ["npm:8.14.2", {\ "packageLocation": "./.yarn/cache/ws-npm-8.14.2-b339ac47a2-3ca0dad26e.zip/node_modules/ws/",\ "packageDependencies": [\ @@ -16723,6 +16869,23 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ + ["virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:8.13.0", {\ + "packageLocation": "./.yarn/__virtual__/ws-virtual-b0708f4db8/0/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip/node_modules/ws/",\ + "packageDependencies": [\ + ["ws", "virtual:52378067c544ac71316376ca5654566708fa9726f4c2fca1f1f6185a4a8fffe9c9a8e2f2857e2d85b2d4f83547238c522e1e7c1d78d8d3c96669aca6b3f19a92#npm:8.13.0"],\ + ["@types/bufferutil", null],\ + ["@types/utf-8-validate", null],\ + ["bufferutil", null],\ + ["utf-8-validate", null]\ + ],\ + "packagePeers": [\ + "@types/bufferutil",\ + "@types/utf-8-validate",\ + "bufferutil",\ + "utf-8-validate"\ + ],\ + "linkType": "HARD"\ + }],\ ["virtual:88293ff29fa54efecc98d655f7a7551b282025b3465bc23ca5bb7a89a31c17930a7319e98225cf138bf4e6ccead5b30ae3c800738697b87af3441226d65f7ee3#npm:7.4.6", {\ "packageLocation": "./.yarn/__virtual__/ws-virtual-e1e964a4e5/0/cache/ws-npm-7.4.6-9c9a725604-3a990b32ed.zip/node_modules/ws/",\ "packageDependencies": [\ @@ -17008,7 +17171,9 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@gnosis.pm/zodiac", "npm:3.4.2"],\ ["@safe-global/api-kit", "npm:1.3.1"],\ ["@safe-global/protocol-kit", "npm:1.3.0"],\ + ["@safe-global/safe-apps-sdk", "npm:9.0.0"],\ ["@safe-global/safe-core-sdk-types", "npm:2.3.0"],\ + ["@safe-global/safe-gateway-typescript-sdk", "npm:3.14.0"],\ ["@shazow/whatsabi", "npm:0.2.1"],\ ["@testing-library/jest-dom", "npm:5.17.0"],\ ["@typechain/ethers-v5", "virtual:919984625f908c00f58e56a3a023a4bcc5a02977fb9ef0230392d1979706b2cc874abc287345e6561886da69e547c4d1330a8c5645be8f7e62b06d5144141c21#npm:10.2.1"],\ diff --git a/extension/.vscode/settings.json b/extension/.vscode/settings.json index 8447102c..b97b1ce3 100644 --- a/extension/.vscode/settings.json +++ b/extension/.vscode/settings.json @@ -6,7 +6,7 @@ "typescript.tsdk": ".yarn/sdks/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "jest.jestCommandLine": "yarn test", "search.exclude": { @@ -14,5 +14,6 @@ "**/.pnp.*": true }, "eslint.nodePath": ".yarn/sdks", - "prettier.prettierPath": ".yarn/sdks/prettier/index.js" + "prettier.prettierPath": ".yarn/sdks/prettier/index.js", + "mdx-preview.preview.useVscodeMarkdownStyles": false } diff --git a/extension/.yarn/cache/@adraffy-ens-normalize-npm-1.10.0-7dfdaa4813-af0540f963.zip b/extension/.yarn/cache/@adraffy-ens-normalize-npm-1.10.0-7dfdaa4813-af0540f963.zip new file mode 100644 index 00000000..f1155523 Binary files /dev/null and b/extension/.yarn/cache/@adraffy-ens-normalize-npm-1.10.0-7dfdaa4813-af0540f963.zip differ diff --git a/extension/.yarn/cache/@ethersproject-abstract-provider-npm-5.7.0-f94be4e0b0-74cf469624.zip b/extension/.yarn/cache/@ethersproject-abstract-provider-npm-5.7.0-f94be4e0b0-74cf469624.zip index 5f6e6bed..7767845a 100644 Binary files a/extension/.yarn/cache/@ethersproject-abstract-provider-npm-5.7.0-f94be4e0b0-74cf469624.zip and b/extension/.yarn/cache/@ethersproject-abstract-provider-npm-5.7.0-f94be4e0b0-74cf469624.zip differ diff --git a/extension/.yarn/cache/@noble-curves-npm-1.2.0-9b40ee1239-bb798d7a66.zip b/extension/.yarn/cache/@noble-curves-npm-1.2.0-9b40ee1239-bb798d7a66.zip new file mode 100644 index 00000000..f71ec53e Binary files /dev/null and b/extension/.yarn/cache/@noble-curves-npm-1.2.0-9b40ee1239-bb798d7a66.zip differ diff --git a/extension/.yarn/cache/@noble-hashes-npm-1.3.3-f7374e6cdf-8a6496d1c0.zip b/extension/.yarn/cache/@noble-hashes-npm-1.3.3-f7374e6cdf-8a6496d1c0.zip new file mode 100644 index 00000000..291bc1d3 Binary files /dev/null and b/extension/.yarn/cache/@noble-hashes-npm-1.3.3-f7374e6cdf-8a6496d1c0.zip differ diff --git a/extension/.yarn/cache/@safe-global-safe-apps-sdk-npm-9.0.0-af49618fd8-91d233d39d.zip b/extension/.yarn/cache/@safe-global-safe-apps-sdk-npm-9.0.0-af49618fd8-91d233d39d.zip new file mode 100644 index 00000000..0d463e78 Binary files /dev/null and b/extension/.yarn/cache/@safe-global-safe-apps-sdk-npm-9.0.0-af49618fd8-91d233d39d.zip differ diff --git a/extension/.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.14.0-97261ee2e0-a5242ba434.zip b/extension/.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.14.0-97261ee2e0-a5242ba434.zip new file mode 100644 index 00000000..5b2c5d04 Binary files /dev/null and b/extension/.yarn/cache/@safe-global-safe-gateway-typescript-sdk-npm-3.14.0-97261ee2e0-a5242ba434.zip differ diff --git a/extension/.yarn/cache/@scure-base-npm-1.1.5-d9203f3027-9e9ee6088c.zip b/extension/.yarn/cache/@scure-base-npm-1.1.5-d9203f3027-9e9ee6088c.zip new file mode 100644 index 00000000..78dd7fee Binary files /dev/null and b/extension/.yarn/cache/@scure-base-npm-1.1.5-d9203f3027-9e9ee6088c.zip differ diff --git a/extension/.yarn/cache/@scure-bip32-npm-1.3.2-3a1cfaf4f0-c5ae84fae4.zip b/extension/.yarn/cache/@scure-bip32-npm-1.3.2-3a1cfaf4f0-c5ae84fae4.zip new file mode 100644 index 00000000..45b7dde6 Binary files /dev/null and b/extension/.yarn/cache/@scure-bip32-npm-1.3.2-3a1cfaf4f0-c5ae84fae4.zip differ diff --git a/extension/.yarn/cache/abitype-npm-0.9.8-ee830ee479-d7d887f29d.zip b/extension/.yarn/cache/abitype-npm-0.9.8-ee830ee479-d7d887f29d.zip new file mode 100644 index 00000000..62ee603f Binary files /dev/null and b/extension/.yarn/cache/abitype-npm-0.9.8-ee830ee479-d7d887f29d.zip differ diff --git a/extension/.yarn/cache/isows-npm-1.0.3-aa8c925c69-9cacd5cf59.zip b/extension/.yarn/cache/isows-npm-1.0.3-aa8c925c69-9cacd5cf59.zip new file mode 100644 index 00000000..a94b5da9 Binary files /dev/null and b/extension/.yarn/cache/isows-npm-1.0.3-aa8c925c69-9cacd5cf59.zip differ diff --git a/extension/.yarn/cache/viem-npm-1.21.4-a58095c18e-c351fdea2d.zip b/extension/.yarn/cache/viem-npm-1.21.4-a58095c18e-c351fdea2d.zip new file mode 100644 index 00000000..74579552 Binary files /dev/null and b/extension/.yarn/cache/viem-npm-1.21.4-a58095c18e-c351fdea2d.zip differ diff --git a/extension/.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip b/extension/.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip new file mode 100644 index 00000000..74e59aab Binary files /dev/null and b/extension/.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip differ diff --git a/extension/package.json b/extension/package.json index 9db2f8f6..2f7a00a4 100644 --- a/extension/package.json +++ b/extension/package.json @@ -29,7 +29,9 @@ "@gnosis.pm/zodiac": "^3.4.2", "@safe-global/api-kit": "^1.3.1", "@safe-global/protocol-kit": "^1.3.0", + "@safe-global/safe-apps-sdk": "^9.0.0", "@safe-global/safe-core-sdk-types": "^2.3.0", + "@safe-global/safe-gateway-typescript-sdk": "^3.14.0", "@shazow/whatsabi": "^0.2.1", "@testing-library/jest-dom": "^5.16.1", "@typechain/ethers-v5": "^10.2.1", @@ -85,4 +87,4 @@ "typescript-plugin-css-modules": "^3.4.0" }, "packageManager": "yarn@3.7.0" -} +} \ No newline at end of file diff --git a/extension/src/app.tsx b/extension/src/app.tsx index 5e42c613..3b2d6f7c 100644 --- a/extension/src/app.tsx +++ b/extension/src/app.tsx @@ -24,7 +24,7 @@ import { validateAddress } from './utils' const Routes: React.FC = () => { const connectionsRouteMatch = useMatchConnectionsRoute() const pushConnectionsRoute = usePushConnectionsRoute() - const { connection, connected } = useConnection() + const { connection } = useConnection() const isConnectionsRoute = connectionsRouteMatch.isMatch const connectionChangeRequired = @@ -35,9 +35,6 @@ const Routes: React.FC = () => { const connectionToEdit = connections.length === 1 ? connections[0].id : undefined - const waitForWallet = - !isConnectionsRoute && !connectionChangeRequired && !connected - useUpdateLastUsedConnection() // open connections drawer if a valid connection is not available @@ -52,21 +49,7 @@ const Routes: React.FC = () => { connectionChangeRequired, ]) - // open connections drawer if wallet is not connected, but only after a small delay to give the wallet time to connect when initially loading the page - useEffect(() => { - let timeout: number - if (waitForWallet) { - timeout = window.setTimeout(() => { - pushConnectionsRoute() - }, 200) - } - return () => { - window.clearTimeout(timeout) - } - }, [waitForWallet, pushConnectionsRoute]) - if (!isConnectionsRoute && connectionChangeRequired) return null - if (!isConnectionsRoute && waitForWallet) return null return ( <> diff --git a/extension/src/bridge/host.ts b/extension/src/bridge/Eip1193Bridge.ts similarity index 94% rename from extension/src/bridge/host.ts rename to extension/src/bridge/Eip1193Bridge.ts index c183cb14..7244ac13 100644 --- a/extension/src/bridge/host.ts +++ b/extension/src/bridge/Eip1193Bridge.ts @@ -5,7 +5,7 @@ interface Request { params?: Array } -export default class BridgeHost { +export default class Eip1193Bridge { private provider: Eip1193Provider private connection: Connection private source: WindowProxy | undefined @@ -15,11 +15,11 @@ export default class BridgeHost { this.connection = connection } - setProvider(provider: Eip1193Provider) { + setProvider = (provider: Eip1193Provider) => { this.provider = provider } - setConnection(connection: Connection) { + setConnection = (connection: Connection) => { if (connection.avatarAddress !== this.connection.avatarAddress) { this.emitBridgeEvent('accountsChanged', [[connection.avatarAddress]]) } diff --git a/extension/src/bridge/SafeAppBridge.ts b/extension/src/bridge/SafeAppBridge.ts new file mode 100644 index 00000000..cfbc4668 --- /dev/null +++ b/extension/src/bridge/SafeAppBridge.ts @@ -0,0 +1,243 @@ +import type { + ErrorResponse, + GetTxBySafeTxHashParams, + GetBalanceParams, + MethodToResponse, + RequestId, + SDKMessageEvent, + RPCPayload, + SendTransactionsParams, + SignTypedMessageParams, + SignMessageParams, +} from '@safe-global/safe-apps-sdk' +import { PermissionRequest } from '@safe-global/safe-apps-sdk/dist/types/types/permissions' +import { + getSDKVersion, + Methods, + MessageFormatter, +} from '@safe-global/safe-apps-sdk' +import { + getBalances, + getTransactionDetails, +} from '@safe-global/safe-gateway-typescript-sdk' +import { ChainId, CHAIN_CURRENCY, NETWORK_NAME, CHAIN_PREFIX } from '../chains' +import { Connection, Eip1193Provider } from '../types' +import { reloadIframe, requestIframeHref } from '../location' + +type MessageHandler = ( + params: any, + id: SDKMessageEvent['data']['id'], + env: SDKMessageEvent['data']['env'] +) => + | void + | MethodToResponse[Methods] + | ErrorResponse + | Promise + +export default class SafeAppBridge { + private provider: Eip1193Provider + private connection: Connection + private connectedOrigin: string | undefined + + constructor(provider: Eip1193Provider, connection: Connection) { + this.provider = provider + this.connection = connection + } + + setProvider = (provider: Eip1193Provider) => { + this.provider = provider + } + + setConnection = async (connection: Connection) => { + const accountOrChainSwitched = + connection.avatarAddress !== this.connection.avatarAddress || + connection.chainId !== this.connection.chainId + + this.connection = connection + const href = await requestIframeHref() + const currentOrigin = href ? new URL(href).host : undefined + + const isConnected = + this.connectedOrigin && currentOrigin === this.connectedOrigin + + if (isConnected && accountOrChainSwitched) { + // Safe Apps don't expect and won't support switching accounts or chains. + // So we need to reload the iframe. + + this.connectedOrigin = undefined + reloadIframe() + } + } + + handleMessage = async (msg: MessageEvent): Promise => { + if ( + !msg.source || + msg.source instanceof MessagePort || + msg.source instanceof ServiceWorker + ) { + // ignore messages from ports and workers + return + } + + if (msg.data.method === Methods.getSafeInfo) { + // If we get here, it means the Safe App is connected + if (msg.origin !== this.connectedOrigin) { + console.debug('SAFE_APP_CONNECTED', msg.origin) + this.connectedOrigin = msg.origin + } + } + + const handler = this.handlers[msg.data.method as Methods] as + | MessageHandler + | undefined + if (!handler) return + + console.debug('SAFE_APP_MESSAGE', msg.data) + try { + const response = await handler(msg.data.params, msg.data.id, msg.data.env) + if (typeof response !== 'undefined') { + this.postResponse(msg.source, response, msg.data.id) + } else { + throw new Error('No response returned from handler') + } + } catch (e) { + console.error('Error handling message via SafeAppCommunicator', e) + this.postResponse(msg.source, getErrorMessage(e), msg.data.id, true) + } + } + + postResponse = ( + destination: Window, + data: unknown, + requestId: RequestId, + error = false + ): void => { + const sdkVersion = getSDKVersion() + const msg = error + ? MessageFormatter.makeErrorResponse( + requestId, + data as string, + sdkVersion + ) + : MessageFormatter.makeResponse(requestId, data, sdkVersion) + + destination.postMessage(msg, '*') + } + + handlers: { [method in Methods]: MessageHandler } = { + [Methods.getTxBySafeTxHash]: ({ safeTxHash }: GetTxBySafeTxHashParams) => { + return getTransactionDetails( + CHAIN_PREFIX[this.connection.chainId], + safeTxHash + ) + }, + + [Methods.getEnvironmentInfo]: () => ({ + origin: document.location.origin, + }), + + [Methods.getSafeInfo]: () => ({ + safeAddress: this.connection.avatarAddress, + chainId: this.connection.chainId, + owners: [], + threshold: 1, + isReadOnly: false, + network: + LEGACY_CHAIN_NAME[this.connection.chainId] || + NETWORK_NAME[this.connection.chainId].toUpperCase(), + }), + + [Methods.getSafeBalances]: ({ currency = 'usd' }: GetBalanceParams) => { + return getBalances( + CHAIN_PREFIX[this.connection.chainId], + this.connection.avatarAddress, + currency, + { + exclude_spam: true, + trusted: false, // TODO + } + ) + }, + + [Methods.rpcCall]: async (params: RPCPayload) => { + return await this.provider.request({ + method: params.call, + params: params.params, + }) + }, + + [Methods.sendTransactions]: ({ txs }: SendTransactionsParams) => { + return Promise.all( + txs.map((tx) => + this.provider.request({ method: 'eth_sendTransaction', params: [tx] }) + ) + ) + }, + + [Methods.signMessage]: ({ message }: SignMessageParams) => { + return this.provider.request({ + method: 'eth_sign', + params: [message], + }) + }, + + [Methods.signTypedMessage]: ({ typedData }: SignTypedMessageParams) => { + return this.provider.request({ + method: 'eth_signTypedData_v4', + params: [typedData], + }) + }, + + [Methods.getChainInfo]: () => { + const { chainId } = this.connection + return { + chainName: NETWORK_NAME[chainId], + chainId, + shortName: CHAIN_PREFIX[chainId], + nativeCurrency: CHAIN_CURRENCY[chainId], + blockExplorerUriTemplate: {}, // TODO + } + }, + + [Methods.wallet_getPermissions]: () => { + return this.provider.request({ method: 'wallet_getPermissions' }) + }, + + [Methods.wallet_requestPermissions]: (params: PermissionRequest[]) => { + return this.provider.request({ + method: 'wallet_requestPermissions', + params, + }) + }, + + // TODO see how to best implement the following methods + [Methods.getOffChainSignature]: () => { + return undefined + }, + + [Methods.requestAddressBook]: () => { + return undefined + }, + } +} + +const getErrorMessage = (thrown: unknown) => { + if (thrown instanceof Error) { + return thrown.message + } + + if (typeof thrown === 'string') { + return thrown + } + + try { + return JSON.stringify(thrown) + } catch { + return String(thrown) + } +} + +const LEGACY_CHAIN_NAME: { [chainId in ChainId]?: string } = { + 1: 'MAINNET', + 100: 'XDAI', +} diff --git a/extension/src/browser/Drawer/CallContract.tsx b/extension/src/browser/Drawer/CallContract.tsx index e4f7f13c..4f9c02e9 100644 --- a/extension/src/browser/Drawer/CallContract.tsx +++ b/extension/src/browser/Drawer/CallContract.tsx @@ -9,7 +9,7 @@ import { Box } from '../../components' import { useConnection } from '../../settings' import classes from './style.module.css' -import { EXPLORER_API_KEY } from '../../networks' +import { EXPLORER_API_KEY } from '../../chains' interface Props { value: CallContractTransactionInput diff --git a/extension/src/browser/Drawer/ContractAddress/index.tsx b/extension/src/browser/Drawer/ContractAddress/index.tsx index f6a211d0..163bb467 100644 --- a/extension/src/browser/Drawer/ContractAddress/index.tsx +++ b/extension/src/browser/Drawer/ContractAddress/index.tsx @@ -9,7 +9,7 @@ import { EXPLORER_API_KEY, EXPLORER_API_URL, EXPLORER_URL, -} from '../../../networks' +} from '../../../chains' import { useConnection } from '../../../settings' import classes from './style.module.css' diff --git a/extension/src/browser/Drawer/Submit.tsx b/extension/src/browser/Drawer/Submit.tsx index 721ebd06..53b623e8 100644 --- a/extension/src/browser/Drawer/Submit.tsx +++ b/extension/src/browser/Drawer/Submit.tsx @@ -1,4 +1,3 @@ -import { providers } from 'ethers' import React, { useState } from 'react' import { RiCloseLine, RiExternalLinkLine } from 'react-icons/ri' import Modal, { Styles } from 'react-modal' @@ -6,7 +5,7 @@ import { toast } from 'react-toastify' import { Button, IconButton } from '../../components' import toastClasses from '../../components/Toast/Toast.module.css' -import { ChainId, EXPLORER_URL, NETWORK_PREFIX } from '../../networks' +import { ChainId, EXPLORER_URL, CHAIN_PREFIX } from '../../chains' import { waitForMultisigExecution } from '../../providers' // import { shallExecuteDirectly } from '../../safe/sendTransaction' import { useConnection } from '../../settings' @@ -16,9 +15,10 @@ import { useSubmitTransactions } from '../ProvideProvider' import { useDispatch, useNewTransactions } from '../state' import classes from './style.module.css' +import { getReadOnlyProvider } from '../../providers/readOnlyProvider' const Submit: React.FC = () => { - const { provider, connection } = useConnection() + const { connection } = useConnection() const { chainId, pilotAddress, providerType } = connection const dispatch = useDispatch() @@ -61,16 +61,15 @@ const Submit: React.FC = () => { // wait for transaction to be mined const realBatchTransactionHash = await waitForMultisigExecution( - provider, chainId, batchTransactionHash ) console.log( `Transaction batch ${batchTransactionHash} has been executed with transaction hash ${realBatchTransactionHash}` ) - const receipt = await new providers.Web3Provider( - provider - ).waitForTransaction(realBatchTransactionHash) + const receipt = await getReadOnlyProvider(chainId).waitForTransaction( + realBatchTransactionHash + ) console.log( `Transaction ${realBatchTransactionHash} has been mined`, receipt @@ -150,7 +149,7 @@ const AwaitingSignatureModal: React.FC<{

diff --git a/extension/src/browser/Drawer/Transaction.tsx b/extension/src/browser/Drawer/Transaction.tsx index 7fa4c820..0aeb5c91 100644 --- a/extension/src/browser/Drawer/Transaction.tsx +++ b/extension/src/browser/Drawer/Transaction.tsx @@ -6,7 +6,7 @@ import { TransactionInput, TransactionType } from 'react-multisend' import { Box, Flex } from '../../components' import ToggleButton from '../../components/Drawer/ToggleButton' -import { NETWORK_CURRENCY } from '../../networks' +import { CHAIN_CURRENCY } from '../../chains' import { useConnection } from '../../settings' import { TransactionState } from '../state' @@ -268,7 +268,7 @@ const EtherValue: React.FC<{ input: TransactionInput }> = ({ input }) => { justifyContent="space-between" className={classes.value} > -

{NETWORK_CURRENCY[chainId]}:
+
{CHAIN_CURRENCY[chainId]}:
{valueBN.isZero() ? 'n/a' : formatEther(valueBN)} diff --git a/extension/src/browser/Drawer/index.tsx b/extension/src/browser/Drawer/index.tsx index 9dcee5a2..00b0c63f 100644 --- a/extension/src/browser/Drawer/index.tsx +++ b/extension/src/browser/Drawer/index.tsx @@ -13,7 +13,7 @@ import { useAllTransactions, useDispatch, useNewTransactions } from '../state' import Submit from './Submit' import { Transaction, TransactionBadge } from './Transaction' import classes from './style.module.css' -import { ChainId, MULTI_SEND_ADDRESS } from '../../networks' +import { ChainId, MULTI_SEND_ADDRESS } from '../../chains' const TransactionsDrawer: React.FC = () => { const [expanded, setExpanded] = useState(true) diff --git a/extension/src/browser/Frame.tsx b/extension/src/browser/Frame.tsx index 7d16f53e..4b269054 100644 --- a/extension/src/browser/Frame.tsx +++ b/extension/src/browser/Frame.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useRef } from 'react' +import React, { useLayoutEffect, useRef } from 'react' -import BridgeHost from '../bridge/host' +import Eip1193Bridge from '../bridge/Eip1193Bridge' +import SafeAppBridge from '../bridge/SafeAppBridge' import { useConnection } from '../settings' import { useProvider } from './ProvideProvider' @@ -12,20 +13,34 @@ type Props = { const BrowserFrame: React.FC = ({ src }) => { const provider = useProvider() const { connection } = useConnection() - const bridgeHostRef = useRef(null) + const eip1193BridgeRef = useRef(null) + const safeAppBridgeRef = useRef(null) - useEffect(() => { + // We need the message listener to be set up before the iframe content window is loaded. + // Otherwise we might miss the initial handshake message from the Safe SDK. + // Using a layout effect ensures that the listeners are set synchronously after DOM flush. + useLayoutEffect(() => { if (!provider) return - if (!bridgeHostRef.current) { - bridgeHostRef.current = new BridgeHost(provider, connection) + // establish EIP-1193 bridge + if (!eip1193BridgeRef.current) { + eip1193BridgeRef.current = new Eip1193Bridge(provider, connection) } else { - bridgeHostRef.current.setProvider(provider) - bridgeHostRef.current.setConnection(connection) + eip1193BridgeRef.current.setProvider(provider) + eip1193BridgeRef.current.setConnection(connection) + } + + // establish Safe App bridge + if (!safeAppBridgeRef.current) { + safeAppBridgeRef.current = new SafeAppBridge(provider, connection) + } else { + safeAppBridgeRef.current.setProvider(provider) + safeAppBridgeRef.current.setConnection(connection) } const handle = (ev: MessageEvent) => { - bridgeHostRef.current?.handleMessage(ev) + eip1193BridgeRef.current?.handleMessage(ev) + safeAppBridgeRef.current?.handleMessage(ev) } window.addEventListener('message', handle) diff --git a/extension/src/browser/ProvideProvider.tsx b/extension/src/browser/ProvideProvider.tsx index d7619be3..a8dc62ed 100644 --- a/extension/src/browser/ProvideProvider.tsx +++ b/extension/src/browser/ProvideProvider.tsx @@ -8,7 +8,7 @@ import React, { } from 'react' import { decodeSingle, encodeMulti, encodeSingle } from 'react-multisend' -import { ChainId, MULTI_SEND_ADDRESS } from '../networks' +import { ChainId, MULTI_SEND_ADDRESS } from '../chains' import { ForkProvider, useTenderlyProvider, diff --git a/extension/src/browser/fetchAbi.ts b/extension/src/browser/fetchAbi.ts index 393034fd..ef7b0c33 100644 --- a/extension/src/browser/fetchAbi.ts +++ b/extension/src/browser/fetchAbi.ts @@ -3,7 +3,7 @@ import { Provider } from '@ethersproject/abstract-provider' import { loaders } from '@shazow/whatsabi' import detectProxyTarget from 'ethers-proxies' -import { ChainId, EXPLORER_API_URL } from '../networks' +import { ChainId, EXPLORER_API_URL } from '../chains' const fetchAbi = async ( network: ChainId, diff --git a/extension/src/networks.ts b/extension/src/chains.ts similarity index 98% rename from extension/src/networks.ts rename to extension/src/chains.ts index 639cad89..dc73711d 100644 --- a/extension/src/networks.ts +++ b/extension/src/chains.ts @@ -62,7 +62,7 @@ export const EXPLORER_API_KEY: Record = { 80001: '', } -export const NETWORK_PREFIX: Record = { +export const CHAIN_PREFIX: Record = { 1: 'eth', 4: 'rin', 5: 'gor', @@ -77,7 +77,7 @@ export const NETWORK_PREFIX: Record = { 80001: 'maticmum', } -export const NETWORK_CURRENCY: Record = { +export const CHAIN_CURRENCY: Record = { 1: 'ETH', 4: 'ETH', 5: 'ETH', diff --git a/extension/src/location.ts b/extension/src/location.ts index 531df6a2..4ed96c4c 100644 --- a/extension/src/location.ts +++ b/extension/src/location.ts @@ -8,37 +8,55 @@ const decodeLocationHash = () => { return '' } -// The background script listens to all possible ways of location updates in our iframe and notify us via a message. -let lastHref = decodeLocationHash() -window.addEventListener('message', (event) => { - if (event.data.type === 'navigationDetected') { - // This actually means that a navigation happened anywhere in our extension tab (tab itself or any contained iframe). - // So not all events actually - const iframe = document.getElementById( - 'pilot-frame' - ) as HTMLIFrameElement | null - const iframeWindow = iframe?.contentWindow - if (!iframeWindow) return +export const requestIframeHref = async () => { + const iframe = document.getElementById( + 'pilot-frame' + ) as HTMLIFrameElement | null + const iframeWindow = iframe?.contentWindow + if (!iframeWindow) return - iframeWindow.postMessage({ zodiacPilotHrefRequest: true }, '*') + iframeWindow.postMessage({ zodiacPilotHrefRequest: true }, '*') + return new Promise((resolve) => { const handleMessage = (ev: MessageEvent) => { const { zodiacPilotHrefResponse, href } = ev.data - if (zodiacPilotHrefResponse && href !== lastHref) { - console.debug('iframe navigated to', href) - window.removeEventListener('message', handleMessage) - // preserve the connections part of the location hash to keep the connection drawer open - const [connectionsPart] = decodeLocationHash().split(';') - const prefix = connectionsPart.startsWith('connections') - ? connectionsPart + ';' - : '' - - replaceLocation(prefix + href) // don't push as this would mess with the browsing history - lastHref = href + if (zodiacPilotHrefResponse) { + window.removeEventListener('message', handleMessage) + resolve(href) } } window.addEventListener('message', handleMessage) + }) +} + +export const reloadIframe = () => { + const iframe = document.getElementById( + 'pilot-frame' + ) as HTMLIFrameElement | null + const iframeWindow = iframe?.contentWindow + if (!iframeWindow) return + iframeWindow.location.reload() +} + +// The background script listens to all possible ways of location updates in our iframe and notify us via a message. +let lastHref = decodeLocationHash() +window.addEventListener('message', async (event) => { + if (event.data.type === 'navigationDetected') { + // This actually means that a navigation happened anywhere in our extension tab (tab itself or any contained iframe). + const href = await requestIframeHref() + if (href && href !== lastHref) { + console.debug('iframe navigated to', href) + + // preserve the connections part of the location hash to keep the connection drawer open + const [connectionsPart] = decodeLocationHash().split(';') + const prefix = connectionsPart.startsWith('connections') + ? connectionsPart + ';' + : '' + + replaceLocation(prefix + href) // don't push as this would mess with the browsing history + lastHref = href + } } }) diff --git a/extension/src/providers/ForkProvider.ts b/extension/src/providers/ForkProvider.ts index f82771b1..3fbc3e9c 100644 --- a/extension/src/providers/ForkProvider.ts +++ b/extension/src/providers/ForkProvider.ts @@ -82,8 +82,13 @@ class ForkProvider extends EventEmitter { return true } - // Uniswap will try to use this for ERC-20 permits, but we prefer to do a regular approval as part of the batch + case 'eth_sign': { + // TODO support this via Safe's SignMessageLib + throw new UnsupportedMethodError('eth_sign is not supported') + } + case 'eth_signTypedData_v4': { + // TODO support this via Safe's SignMessageLib throw new UnsupportedMethodError( 'eth_signTypedData_v4 is not supported' ) diff --git a/extension/src/providers/ProvideTenderly.tsx b/extension/src/providers/ProvideTenderly.tsx index 893e9c2e..60c5dbf8 100644 --- a/extension/src/providers/ProvideTenderly.tsx +++ b/extension/src/providers/ProvideTenderly.tsx @@ -1,94 +1,56 @@ import EventEmitter from 'events' import { JsonRpcProvider } from '@ethersproject/providers' -import React, { useContext, useEffect, useState } from 'react' +import React, { useContext, useEffect, useMemo } from 'react' import { useConnection } from '../settings/connectionHooks' import { Eip1193Provider, JsonRpcRequest } from '../types' import { useBeforeUnload } from '../utils' import { initSafeProtocolKit } from '../safe/kits' import { safeInterface } from '../safe' +import { getEip1193ReadOnlyProvider } from './readOnlyProvider' +import { ChainId } from '../chains' const TenderlyContext = React.createContext(null) -export const useTenderlyProvider = (): TenderlyProvider | null => - useContext(TenderlyContext) +export const useTenderlyProvider = (): TenderlyProvider => { + const value = useContext(TenderlyContext) + if (!value) throw new Error('must be wrapped in ') + return value +} + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const ProvideTenderly: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - const { provider, chainId, connection } = useConnection() + const { + connection: { chainId, avatarAddress, moduleAddress, pilotAddress }, + } = useConnection() - const [tenderlyProvider, setTenderlyProvider] = - useState(null) + const tenderlyProvider = useMemo(() => { + return new TenderlyProvider(chainId) + }, [chainId]) + // whenever anything changes in the connection settings, we delete the current fork and start afresh useEffect(() => { - if (!chainId) return - - const tenderlyProvider = new TenderlyProvider(provider, chainId) - setTenderlyProvider(tenderlyProvider) - - const canceled = false - async function prepareSafeForSimulation() { - const { avatarAddress, moduleAddress, pilotAddress } = connection - const safe = await initSafeProtocolKit(provider, avatarAddress) - - // If we simulate as a Safe owner, we might have to override the owners & threshold of the Safe to allow single signature transactions - if (!moduleAddress) { - const [owners, threshold] = await Promise.all([ - safe.getOwners(), - safe.getThreshold(), - ]) - - const pilotIsOwner = owners.some( - (owner) => owner.toLowerCase() === pilotAddress.toLowerCase() - ) - - if (!pilotIsOwner) { - // the pilot account is not an owner, so we need to make it one and set the threshold to 1 at the same time - await tenderlyProvider.request({ - method: 'eth_sendTransaction', - params: [ - { - to: avatarAddress, - data: safeInterface.encodeFunctionData( - 'addOwnerWithThreshold', - [pilotAddress, 1] - ), - from: avatarAddress, - }, - ], - }) - } else if (threshold > 1) { - // doesn't allow to execute with single signature, so we need to override the threshold - await tenderlyProvider.request({ - method: 'eth_sendTransaction', - params: [ - { - to: avatarAddress, - data: safeInterface.encodeFunctionData('changeThreshold', [1]), - from: avatarAddress, - }, - ], - }) - } - } - - if (!canceled) return - } - - prepareSafeForSimulation() + prepareSafeForSimulation( + { chainId, avatarAddress, moduleAddress, pilotAddress }, + tenderlyProvider + ) return () => { tenderlyProvider.deleteFork() } - }, [provider, chainId, connection]) + }, [tenderlyProvider, chainId, avatarAddress, moduleAddress, pilotAddress]) // delete fork when closing browser tab (the effect teardown won't be executed in that case) useBeforeUnload(() => { if (tenderlyProvider) tenderlyProvider.deleteFork() }) + if (!tenderlyProvider) return null + return ( {children} @@ -98,6 +60,64 @@ const ProvideTenderly: React.FC<{ children: React.ReactNode }> = ({ export default ProvideTenderly +async function prepareSafeForSimulation( + { + chainId, + avatarAddress, + moduleAddress, + pilotAddress, + }: { + chainId: ChainId + avatarAddress: string + moduleAddress: string + pilotAddress: string + }, + tenderlyProvider: TenderlyProvider +) { + const safe = await initSafeProtocolKit(chainId, avatarAddress) + + // If we simulate as a Safe owner, we might have to override the owners & threshold of the Safe to allow single signature transactions + if (!moduleAddress) { + const [owners, threshold] = await Promise.all([ + safe.getOwners(), + safe.getThreshold(), + ]) + + const pilotIsOwner = owners.some( + (owner) => owner.toLowerCase() === pilotAddress.toLowerCase() + ) + + if (!pilotIsOwner) { + // the pilot account is not an owner, so we need to make it one and set the threshold to 1 at the same time + await tenderlyProvider.request({ + method: 'eth_sendTransaction', + params: [ + { + to: avatarAddress, + data: safeInterface.encodeFunctionData('addOwnerWithThreshold', [ + pilotAddress, + 1, + ]), + from: avatarAddress, + }, + ], + }) + } else if (threshold > 1) { + // doesn't allow to execute with single signature, so we need to override the threshold + await tenderlyProvider.request({ + method: 'eth_sendTransaction', + params: [ + { + to: avatarAddress, + data: safeInterface.encodeFunctionData('changeThreshold', [1]), + from: avatarAddress, + }, + ], + }) + } + } +} + export interface TenderlyTransactionInfo { id: string project_id: string @@ -157,9 +177,9 @@ export class TenderlyProvider extends EventEmitter { private tenderlyForkApi: string - constructor(provider: Eip1193Provider, chainId: number) { + constructor(chainId: ChainId) { super() - this.provider = provider + this.provider = getEip1193ReadOnlyProvider(chainId, ZERO_ADDRESS) this.chainId = chainId this.tenderlyForkApi = 'https://fork-api.pilot.gnosisguild.org' } diff --git a/extension/src/providers/WrappingProvider.ts b/extension/src/providers/WrappingProvider.ts index bd3580bf..697afce3 100644 --- a/extension/src/providers/WrappingProvider.ts +++ b/extension/src/providers/WrappingProvider.ts @@ -95,10 +95,7 @@ class WrappingProvider extends EventEmitter { if (!this.connection.moduleAddress) { // use safeTxGas estimation for direct execution - const safeApiKit = initSafeApiKit( - this.provider, - this.connection.chainId - ) + const safeApiKit = initSafeApiKit(this.connection.chainId) const result = await safeApiKit.estimateSafeTransaction( this.connection.avatarAddress, request diff --git a/extension/src/providers/readOnlyProvider.ts b/extension/src/providers/readOnlyProvider.ts new file mode 100644 index 00000000..055e5d29 --- /dev/null +++ b/extension/src/providers/readOnlyProvider.ts @@ -0,0 +1,178 @@ +import EventEmitter from 'events' +import { hexValue } from 'ethers/lib/utils' +import { + StaticJsonRpcProvider, + TransactionRequest, +} from '@ethersproject/providers' +import { ChainId, RPC } from '../chains' + +const readOnlyProviderCache = new Map() +const eip1193ProviderCache = new Map() + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +export const getReadOnlyProvider = ( + chainId: ChainId +): StaticJsonRpcProvider => { + if (readOnlyProviderCache.has(chainId)) { + return readOnlyProviderCache.get(chainId)! + } + + const provider = new StaticJsonRpcProvider(RPC[chainId], chainId) + readOnlyProviderCache.set(chainId, provider) + return provider +} + +/** + * Returns a read-only EIP-1193 provider powered by our RPC nodes. This provider is useful for reading data from the blockchain, but cannot be used to sign transactions. + * Memoizes the provider to avoid triggering effect cascades. + * @throws if used for wallet RPC calls + **/ +export const getEip1193ReadOnlyProvider = ( + chainId: ChainId, + address: string +): Eip1193JsonRpcProvider => { + const cacheKey = `${chainId}:${address.toLowerCase()}` + if (eip1193ProviderCache.has(cacheKey)) { + return eip1193ProviderCache.get(cacheKey)! + } + + const provider = new Eip1193JsonRpcProvider(chainId) + eip1193ProviderCache.set(cacheKey, provider) + return provider +} + +const hexlifyTransaction = (transaction: TransactionRequest) => + StaticJsonRpcProvider.hexlifyTransaction(transaction, { + from: true, + customData: true, + ccipReadEnabled: true, + }) + +/** + * Based on the ethers v5 Eip1193Bridge (https://github.com/ethers-io/ethers.js/blob/v5.7/packages/experimental/src.ts/eip1193-bridge.ts) + * Copyright (c) 2019 Richard Moore, released under MIT license. + */ +export class Eip1193JsonRpcProvider extends EventEmitter { + readonly provider: StaticJsonRpcProvider + readonly chainId: ChainId + readonly address: string + + constructor(chainId: ChainId, address: string = ZERO_ADDRESS) { + super() + this.chainId = chainId + this.address = address + this.provider = getReadOnlyProvider(chainId) + } + + request(request: { method: string; params?: Array }): Promise { + return this.send(request.method, request.params || []) + } + + async send(method: string, params: any[] = []): Promise { + let coerce = (value: any) => value + + switch (method) { + case 'eth_gasPrice': { + const result = await this.provider.getGasPrice() + return result.toHexString() + } + case 'eth_accounts': { + return this.address === ZERO_ADDRESS ? [] : [this.address] + } + case 'eth_blockNumber': { + return await this.provider.getBlockNumber() + } + case 'eth_chainId': { + return hexValue(this.chainId) + } + case 'eth_getBalance': { + const result = await this.provider.getBalance(params[0], params[1]) + return result.toHexString() + } + case 'eth_getStorageAt': { + return this.provider.getStorageAt(params[0], params[1], params[2]) + } + case 'eth_getTransactionCount': { + const result = await this.provider.getTransactionCount( + params[0], + params[1] + ) + return hexValue(result) + } + case 'eth_getBlockTransactionCountByHash': + case 'eth_getBlockTransactionCountByNumber': { + const result = await this.provider.getBlock(params[0]) + return hexValue(result.transactions.length) + } + case 'eth_getCode': { + const result = await this.provider.getCode(params[0], params[1]) + return result + } + case 'eth_sendRawTransaction': { + return await this.provider.sendTransaction(params[0]) + } + case 'eth_call': { + const req = hexlifyTransaction(params[0]) + return await this.provider.call(req, params[1]) + } + case 'estimateGas': { + if (params[1] && params[1] !== 'latest') { + throw new Error('estimateGas does not support blockTag') + } + + const req = hexlifyTransaction(params[0]) + const result = await this.provider.estimateGas(req) + return result.toHexString() + } + + // @TODO: Transform? No uncles? + case 'eth_getBlockByHash': + case 'eth_getBlockByNumber': { + if (params[1]) { + return await this.provider.getBlockWithTransactions(params[0]) + } else { + return await this.provider.getBlock(params[0]) + } + } + case 'eth_getTransactionByHash': { + return await this.provider.getTransaction(params[0]) + } + case 'eth_getTransactionReceipt': { + return await this.provider.getTransactionReceipt(params[0]) + } + + case 'eth_sendTransaction': + case 'eth_sign': { + throw new Error(`${method} requires signing`) + } + + case 'eth_getUncleCountByBlockHash': + case 'eth_getUncleCountByBlockNumber': { + coerce = hexValue + break + } + + case 'eth_getTransactionByBlockHashAndIndex': + case 'eth_getTransactionByBlockNumberAndIndex': + case 'eth_getUncleByBlockHashAndIndex': + case 'eth_getUncleByBlockNumberAndIndex': + case 'eth_newFilter': + case 'eth_newBlockFilter': + case 'eth_newPendingTransactionFilter': + case 'eth_uninstallFilter': + case 'eth_getFilterChanges': + case 'eth_getFilterLogs': + case 'eth_getLogs': + break + } + + // If our provider supports send, maybe it can do a better job? + if ((this.provider).send) { + const result = this.provider.send(method, params) + return coerce(result) + } + + return new Error(`unsupported method: ${method}`) + } +} diff --git a/extension/src/providers/useMetaMask.tsx b/extension/src/providers/useMetaMask.tsx index eba7a890..cb4bffd5 100644 --- a/extension/src/providers/useMetaMask.tsx +++ b/extension/src/providers/useMetaMask.tsx @@ -12,13 +12,13 @@ import { toast } from 'react-toastify' import { ChainId, EXPLORER_URL, - NETWORK_CURRENCY, + CHAIN_CURRENCY, NETWORK_NAME, RPC, -} from '../networks' +} from '../chains' import { Eip1193Provider } from '../types' -interface MetaMaskContextT { +export interface MetaMaskContextT { provider: Eip1193Provider | undefined connect: () => Promise<{ chainId: number; accounts: string[] }> switchChain: (chainId: ChainId) => Promise @@ -207,8 +207,8 @@ const switchChain = async (chainId: ChainId) => { chainId: `0x${chainId.toString(16)}`, chainName: NETWORK_NAME[chainId], nativeCurrency: { - name: NETWORK_CURRENCY[chainId], - symbol: NETWORK_CURRENCY[chainId], + name: CHAIN_CURRENCY[chainId], + symbol: CHAIN_CURRENCY[chainId], decimals: 18, }, rpcUrls: [RPC[chainId]], diff --git a/extension/src/providers/useWalletConnect.ts b/extension/src/providers/useWalletConnect.ts index d31d0694..082b8b8d 100644 --- a/extension/src/providers/useWalletConnect.ts +++ b/extension/src/providers/useWalletConnect.ts @@ -8,7 +8,7 @@ import { SignClient } from '@walletconnect/sign-client' import { KeyValueStorage } from '@walletconnect/keyvaluestorage' import { EthereumProviderOptions } from '@walletconnect/ethereum-provider/dist/types/EthereumProvider' -import { RPC } from '../networks' +import { RPC } from '../chains' import { waitForMultisigExecution } from '../safe' import { JsonRpcError } from '../types' @@ -68,11 +68,7 @@ class WalletConnectEthereumMultiProvider extends WalletConnectEthereumProvider { if (method === 'eth_sendTransaction') { const safeTxHash = (await requestWithCorrectErrors(request)) as string - const txHash = await waitForMultisigExecution( - this.signer, - this.chainId, - safeTxHash - ) + const txHash = await waitForMultisigExecution(this.chainId, safeTxHash) return txHash } @@ -127,7 +123,7 @@ class WalletConnectJsonRpcError extends Error implements JsonRpcError { } } -interface WalletConnectResult { +export interface WalletConnectResult { provider: WalletConnectEthereumMultiProvider connected: boolean connect(): Promise<{ chainId: number; accounts: string[] }> diff --git a/extension/src/safe/kits.ts b/extension/src/safe/kits.ts index f0b602bb..003b93ff 100644 --- a/extension/src/safe/kits.ts +++ b/extension/src/safe/kits.ts @@ -2,8 +2,8 @@ import Safe, { EthersAdapter } from '@safe-global/protocol-kit' import SafeApiKit from '@safe-global/api-kit' import * as ethers from 'ethers' -import { ChainId } from '../networks' -import { Eip1193Provider } from '../types' +import { ChainId } from '../chains' +import { getReadOnlyProvider } from '../providers/readOnlyProvider' export const TX_SERVICE_URL: Record = { [1]: 'https://safe-transaction-mainnet.safe.global', @@ -20,9 +20,7 @@ export const TX_SERVICE_URL: Record = { [80001]: undefined, // not available } -export const initSafeApiKit = (provider: Eip1193Provider, chainId: ChainId) => { - const web3Provider = new ethers.providers.Web3Provider(provider) - +export const initSafeApiKit = (chainId: ChainId) => { const txServiceUrl = TX_SERVICE_URL[chainId as ChainId] if (!txServiceUrl) { throw new Error(`service not available for chain #${chainId}`) @@ -30,21 +28,19 @@ export const initSafeApiKit = (provider: Eip1193Provider, chainId: ChainId) => { const ethAdapter = new EthersAdapter({ ethers, - signerOrProvider: web3Provider.getSigner(), + signerOrProvider: getReadOnlyProvider(chainId), }) return new SafeApiKit({ txServiceUrl, ethAdapter }) } export const initSafeProtocolKit = async ( - provider: Eip1193Provider, + chainId: ChainId, safeAddress: string ) => { - const web3Provider = new ethers.providers.Web3Provider(provider) - const ethAdapter = new EthersAdapter({ ethers, - signerOrProvider: web3Provider.getSigner(), + signerOrProvider: getReadOnlyProvider(chainId), }) return await Safe.create({ ethAdapter, safeAddress }) diff --git a/extension/src/safe/sendTransaction.ts b/extension/src/safe/sendTransaction.ts index d0110374..119efdef 100644 --- a/extension/src/safe/sendTransaction.ts +++ b/extension/src/safe/sendTransaction.ts @@ -2,20 +2,18 @@ import Safe, { EthersAdapter } from '@safe-global/protocol-kit' import * as ethers from 'ethers' import { getAddress } from 'ethers/lib/utils' import { MetaTransaction } from 'react-multisend' +import { getReadOnlyProvider } from '../providers/readOnlyProvider' import { Connection, Eip1193Provider, TransactionData } from '../types' import { initSafeApiKit } from './kits' import { waitForMultisigExecution } from './waitForMultisigExecution' -export const shallExecuteDirectly = async ( - provider: Eip1193Provider, - connection: Connection -) => { - const web3Provider = new ethers.providers.Web3Provider(provider) +export const shallExecuteDirectly = async (connection: Connection) => { + const provider = getReadOnlyProvider(connection.chainId) const ethAdapter = new EthersAdapter({ ethers, - signerOrProvider: web3Provider.getSigner(), + signerOrProvider: provider, }) const safeSdk = await Safe.create({ ethAdapter, @@ -24,7 +22,7 @@ export const shallExecuteDirectly = async ( const threshold = await safeSdk.getThreshold() const pilotIsSmartAccount = - (await web3Provider.getCode(connection.pilotAddress)) !== '0x' + (await provider.getCode(connection.pilotAddress)) !== '0x' return !connection.moduleAddress && threshold === 1 && pilotIsSmartAccount } @@ -41,7 +39,7 @@ export const sendTransaction = async ( } const web3Provider = new ethers.providers.Web3Provider(provider) - const safeApiKit = initSafeApiKit(provider, connection.chainId) + const safeApiKit = initSafeApiKit(connection.chainId) const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: web3Provider.getSigner(), @@ -66,7 +64,7 @@ export const sendTransaction = async ( }) const safeTxHash = await safeSdk.getTransactionHash(safeTransaction) - if (await shallExecuteDirectly(provider, connection)) { + if (await shallExecuteDirectly(connection)) { // we execute the transaction directly. this way the pilot safe can collect signatures for the exec transaction (giving more context to co-signers) rather than for signing a meta transaction await safeSdk.executeTransaction(safeTransaction) } else { @@ -83,11 +81,7 @@ export const sendTransaction = async ( }) } - return await waitForMultisigExecution( - provider, - connection.chainId, - safeTxHash - ) + return await waitForMultisigExecution(connection.chainId, safeTxHash) } const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' diff --git a/extension/src/safe/signing.ts b/extension/src/safe/signing.ts new file mode 100644 index 00000000..ac2281b2 --- /dev/null +++ b/extension/src/safe/signing.ts @@ -0,0 +1,50 @@ +import { EIP712TypedData } from '@safe-global/safe-gateway-typescript-sdk' +import { Contract } from 'ethers' +import { hashMessage, _TypedDataEncoder, toUtf8String } from 'ethers/lib/utils' +import { MetaTransaction } from 'react-multisend' + +const SIGN_MESSAGE_LIB_ADDRESS = '0xd53cd0aB83D845Ac265BE939c57F53AD838012c9' +const SIGN_MESSAGE_LIB_ABI = [ + 'function signMessage(bytes calldata _data)', + 'function getMessageHash(bytes memory message) public view returns (bytes32)', +] + +const signMessageLib = new Contract( + SIGN_MESSAGE_LIB_ADDRESS, + SIGN_MESSAGE_LIB_ABI +) + +export const signMessage = (message: string): MetaTransaction => ({ + to: SIGN_MESSAGE_LIB_ADDRESS, + data: signMessageLib.interface.encodeFunctionData('signMessage', [ + hashMessage(decode(message)), + ]), + value: '0', + operation: 1, +}) + +export const signTypedData = (data: EIP712TypedData) => { + // We need to remove EIP712Domain from the types object since ethers does not like it + const { EIP712Domain: _, ...types } = data.types + + return { + to: SIGN_MESSAGE_LIB_ADDRESS, + data: signMessageLib.interface.encodeFunctionData('signMessage', [ + _TypedDataEncoder.hash(data.domain as any, types, data.message), + ]), + value: '0', + operation: 1, + } +} + +const decode = (message: string): string => { + if (!message.startsWith('0x')) { + return message + } + + try { + return toUtf8String(message) + } catch (e) { + return message + } +} diff --git a/extension/src/safe/useSafeDelegates.ts b/extension/src/safe/useSafeDelegates.ts index 3515d528..d65e5c38 100644 --- a/extension/src/safe/useSafeDelegates.ts +++ b/extension/src/safe/useSafeDelegates.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' -import { ChainId } from '../networks' +import { ChainId } from '../chains' import { useConnection } from '../settings' import { validateAddress } from '../utils' @@ -10,7 +10,7 @@ export const useSafeDelegates = ( safeAddress: string, connectionId?: string ) => { - const { provider, connected, chainId } = useConnection(connectionId) + const { provider, chainId } = useConnection(connectionId) const [loading, setLoading] = useState(false) const [delegates, setDelegates] = useState([]) @@ -18,9 +18,9 @@ export const useSafeDelegates = ( const checksumSafeAddress = validateAddress(safeAddress) useEffect(() => { - if (!connected || !chainId || !checksumSafeAddress) return + if (!chainId || !checksumSafeAddress) return - const safeApiKit = initSafeApiKit(provider, chainId as ChainId) + const safeApiKit = initSafeApiKit(chainId as ChainId) setLoading(true) let canceled = false @@ -44,7 +44,7 @@ export const useSafeDelegates = ( setDelegates([]) canceled = true } - }, [provider, checksumSafeAddress, connected, chainId]) + }, [provider, checksumSafeAddress, chainId]) return { loading, delegates } } diff --git a/extension/src/safe/useSafesWithOwner.ts b/extension/src/safe/useSafesWithOwner.ts index af32dd21..d9dc49d6 100644 --- a/extension/src/safe/useSafesWithOwner.ts +++ b/extension/src/safe/useSafesWithOwner.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' -import { ChainId } from '../networks' +import { ChainId } from '../chains' import { useConnection } from '../settings' import { validateAddress } from '../utils' @@ -10,7 +10,7 @@ export const useSafesWithOwner = ( ownerAddress: string, connectionId?: string ) => { - const { provider, connected, chainId } = useConnection(connectionId) + const { chainId } = useConnection(connectionId) const [loading, setLoading] = useState(false) const [safes, setSafes] = useState([]) @@ -18,9 +18,9 @@ export const useSafesWithOwner = ( const checksumOwnerAddress = validateAddress(ownerAddress) useEffect(() => { - if (!connected || !chainId || !checksumOwnerAddress) return + if (!chainId || !checksumOwnerAddress) return - const safeService = initSafeApiKit(provider, chainId as ChainId) + const safeService = initSafeApiKit(chainId as ChainId) setLoading(true) let canceled = false @@ -44,7 +44,7 @@ export const useSafesWithOwner = ( setSafes([]) canceled = true } - }, [provider, checksumOwnerAddress, connected, chainId]) + }, [checksumOwnerAddress, chainId]) return { loading, safes } } diff --git a/extension/src/safe/waitForMultisigExecution.ts b/extension/src/safe/waitForMultisigExecution.ts index aa6d63ca..707fed3f 100644 --- a/extension/src/safe/waitForMultisigExecution.ts +++ b/extension/src/safe/waitForMultisigExecution.ts @@ -1,14 +1,12 @@ -import { ChainId } from '../networks' -import { Eip1193Provider } from '../types' +import { ChainId } from '../chains' import { initSafeApiKit } from './kits' export function waitForMultisigExecution( - provider: Eip1193Provider, chainId: number, safeTxHash: string ): Promise { - const safeService = initSafeApiKit(provider, chainId as ChainId) + const safeService = initSafeApiKit(chainId as ChainId) return new Promise((resolve, reject) => { function tryAgain() { diff --git a/extension/src/settings/Connection/ConnectButton/index.tsx b/extension/src/settings/Connection/ConnectButton/index.tsx index ac1f5eb5..6fac408a 100644 --- a/extension/src/settings/Connection/ConnectButton/index.tsx +++ b/extension/src/settings/Connection/ConnectButton/index.tsx @@ -4,7 +4,7 @@ import { RiAlertLine } from 'react-icons/ri' import { Button, Flex, Tag } from '../../../components' import { shortenAddress } from '../../../components/Address' -import { ChainId } from '../../../networks' +import { ChainId } from '../../../chains' import { useMetaMask, useWalletConnect } from '../../../providers' import PUBLIC_PATH from '../../../publicPath' import { ProviderType } from '../../../types' diff --git a/extension/src/settings/Connection/useZodiacModules.ts b/extension/src/settings/Connection/useZodiacModules.ts index 227de3a0..0a2bfffa 100644 --- a/extension/src/settings/Connection/useZodiacModules.ts +++ b/extension/src/settings/Connection/useZodiacModules.ts @@ -13,7 +13,7 @@ import { useEffect, useState } from 'react' import { validateAddress } from '../../utils' import { useConnection } from '../connectionHooks' -import { ChainId, RPC } from '../../networks' +import { ChainId, RPC } from '../../chains' const SUPPORTED_MODULES = [KnownContracts.DELAY, KnownContracts.ROLES] export type SupportedModuleType = KnownContracts.DELAY | KnownContracts.ROLES diff --git a/extension/src/settings/connectionHooks.tsx b/extension/src/settings/connectionHooks.tsx index 0474ca47..8155de8a 100644 --- a/extension/src/settings/connectionHooks.tsx +++ b/extension/src/settings/connectionHooks.tsx @@ -1,5 +1,3 @@ -import { EventEmitter } from 'events' - import { KnownContracts } from '@gnosis.pm/zodiac' import { nanoid } from 'nanoid' import React, { ReactNode, useCallback, useEffect } from 'react' @@ -8,6 +6,9 @@ import { createContext, useContext, useMemo } from 'react' import { useMetaMask, useWalletConnect } from '../providers' import { Connection, Eip1193Provider, ProviderType } from '../types' import { useStickyState, validateAddress } from '../utils' +import { MetaMaskContextT } from '../providers/useMetaMask' +import { WalletConnectResult } from '../providers/useWalletConnect' +import { getEip1193ReadOnlyProvider } from '../providers/readOnlyProvider' const DEFAULT_VALUE: Connection[] = [ { @@ -123,27 +124,15 @@ export const useConnection = (id?: string) => { const metamask = useMetaMask() const walletConnect = useWalletConnect(connection.id, connection.chainId || 1) + const defaultProvider = getEip1193ReadOnlyProvider( + connection.chainId, + connection.pilotAddress + ) const provider: Eip1193Provider = (connection.providerType === ProviderType.MetaMask ? metamask.provider - : walletConnect?.provider) || new DummyProvider() // defaulting to a dummy here makes typing when using this hook a bit easier. (we won't request anything when not connected anyways) - - const isConnectedTo = ( - connectionContext: typeof metamask | typeof walletConnect, - chainId: number, - account: string - ) => { - const accountLower = account.toLowerCase() - return ( - connectionContext && - connectionContext.chainId === chainId && - connectionContext.accounts.some( - (acc) => acc.toLowerCase() === accountLower - ) && - ('connected' in connectionContext ? connectionContext.connected : true) - ) - } + : walletConnect?.provider) || defaultProvider const connected = isConnectedTo( connection.providerType === ProviderType.MetaMask @@ -203,10 +192,21 @@ export const useConnection = (id?: string) => { } } -class DummyProvider extends EventEmitter { - async request(): Promise { - return - } +const isConnectedTo = ( + providerContext: MetaMaskContextT | WalletConnectResult | null, + chainId: number, + account: string +) => { + if (!providerContext) return false + const accountLower = account.toLowerCase() + return ( + providerContext && + providerContext.chainId === chainId && + providerContext.accounts?.some( + (acc) => acc.toLowerCase() === accountLower + ) && + ('connected' in providerContext ? providerContext.connected : true) + ) } type ConnectionStateMigration = (connection: Connection) => Connection diff --git a/extension/src/transactionTranslations/index.ts b/extension/src/transactionTranslations/index.ts index 6f957089..8d6ed711 100644 --- a/extension/src/transactionTranslations/index.ts +++ b/extension/src/transactionTranslations/index.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { MetaTransaction } from 'react-multisend' -import { ChainId } from '../networks' +import { ChainId } from '../chains' import { useConnection } from '../settings' import cowswapSetPreSignature from './cowswapSetPreSignature' diff --git a/extension/src/transactionTranslations/types.ts b/extension/src/transactionTranslations/types.ts index 38534ff5..b07d9ac6 100644 --- a/extension/src/transactionTranslations/types.ts +++ b/extension/src/transactionTranslations/types.ts @@ -1,6 +1,6 @@ import { MetaTransaction } from 'react-multisend' -import { ChainId } from '../networks' +import { ChainId } from '../chains' import { SupportedModuleType } from '../settings/Connection/useZodiacModules' export interface TransactionTranslation { diff --git a/extension/src/types/index.ts b/extension/src/types/index.ts index 38d29915..b21b5cd3 100644 --- a/extension/src/types/index.ts +++ b/extension/src/types/index.ts @@ -1,4 +1,4 @@ -import { ChainId } from '../networks' +import { ChainId } from '../chains' import { SupportedModuleType } from '../settings/Connection/useZodiacModules' export enum ProviderType { diff --git a/extension/yarn.lock b/extension/yarn.lock index 251330ec..11d4fd8e 100644 --- a/extension/yarn.lock +++ b/extension/yarn.lock @@ -19,6 +19,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:1.10.0": + version: 1.10.0 + resolution: "@adraffy/ens-normalize@npm:1.10.0" + checksum: af0540f963a2632da2bbc37e36ea6593dcfc607b937857133791781e246d47f870d5e3d21fa70d5cfe94e772c284588c81ea3f5b7f4ea8fbb824369444e4dbcb + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.2.1 resolution: "@ampproject/remapping@npm:2.2.1" @@ -2030,6 +2037,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.2.0, @noble/curves@npm:~1.2.0": + version: 1.2.0 + resolution: "@noble/curves@npm:1.2.0" + dependencies: + "@noble/hashes": 1.3.2 + checksum: bb798d7a66d8e43789e93bc3c2ddff91a1e19fdb79a99b86cd98f1e5eff0ee2024a2672902c2576ef3577b6f282f3b5c778bebd55761ddbb30e36bf275e83dd0 + languageName: node + linkType: hard + "@noble/hashes@npm:1.3.1": version: 1.3.1 resolution: "@noble/hashes@npm:1.3.1" @@ -2037,13 +2053,20 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": +"@noble/hashes@npm:1.3.2, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": version: 1.3.2 resolution: "@noble/hashes@npm:1.3.2" checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474 languageName: node linkType: hard +"@noble/hashes@npm:~1.3.2": + version: 1.3.3 + resolution: "@noble/hashes@npm:1.3.3" + checksum: 8a6496d1c0c64797339bc694ad06cdfaa0f9e56cd0c3f68ae3666cfb153a791a55deb0af9c653c7ed2db64d537aa3e3054629740d2f2338bb1dcb7ab60cd205b + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -2290,6 +2313,16 @@ __metadata: languageName: node linkType: hard +"@safe-global/safe-apps-sdk@npm:^9.0.0": + version: 9.0.0 + resolution: "@safe-global/safe-apps-sdk@npm:9.0.0" + dependencies: + "@safe-global/safe-gateway-typescript-sdk": ^3.5.3 + viem: ^1.6.0 + checksum: 91d233d39d60574550a0455dc55e8cc58eb8b2324bf522e87ab0ed8b08eee702f60f71310790d0a892c5ad9cfc054ef5c7a05ef52f84ba2ecec7a1dcab689f7c + languageName: node + linkType: hard + "@safe-global/safe-core-sdk-types@npm:^2.3.0": version: 2.3.0 resolution: "@safe-global/safe-core-sdk-types@npm:2.3.0" @@ -2312,6 +2345,13 @@ __metadata: languageName: node linkType: hard +"@safe-global/safe-gateway-typescript-sdk@npm:^3.14.0, @safe-global/safe-gateway-typescript-sdk@npm:^3.5.3": + version: 3.14.0 + resolution: "@safe-global/safe-gateway-typescript-sdk@npm:3.14.0" + checksum: a5242ba434aa25a68a1e06f5c413fd0c5c82df9d300909e955aa32dd99146cbc2b060fe7bc43db6b242d473652b5a94ce6a6d6ebaf06eb96106129c6982c06d7 + languageName: node + linkType: hard + "@scure/base@npm:~1.1.0": version: 1.1.3 resolution: "@scure/base@npm:1.1.3" @@ -2319,6 +2359,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:~1.1.2": + version: 1.1.5 + resolution: "@scure/base@npm:1.1.5" + checksum: 9e9ee6088cb3aa0fb91f5a48497d26682c7829df3019b1251d088d166d7a8c0f941c68aaa8e7b96bbad20c71eb210397cb1099062cde3e29d4bad6b975c18519 + languageName: node + linkType: hard + "@scure/bip32@npm:1.3.1": version: 1.3.1 resolution: "@scure/bip32@npm:1.3.1" @@ -2330,6 +2377,17 @@ __metadata: languageName: node linkType: hard +"@scure/bip32@npm:1.3.2": + version: 1.3.2 + resolution: "@scure/bip32@npm:1.3.2" + dependencies: + "@noble/curves": ~1.2.0 + "@noble/hashes": ~1.3.2 + "@scure/base": ~1.1.2 + checksum: c5ae84fae43490853693b481531132b89e056d45c945fc8b92b9d032577f753dfd79c5a7bbcbf0a7f035951006ff0311b6cf7a389e26c9ec6335e42b20c53157 + languageName: node + linkType: hard + "@scure/bip39@npm:1.2.1": version: 1.2.1 resolution: "@scure/bip39@npm:1.2.1" @@ -3439,6 +3497,21 @@ __metadata: languageName: node linkType: hard +"abitype@npm:0.9.8": + version: 0.9.8 + resolution: "abitype@npm:0.9.8" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3 >=3.19.1 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: d7d887f29d6821e3f7a400de9620511b80ead3f85c5c87308aaec97965d3493e6687ed816e88722b4f512249bd66dee9e69231b49af0e1db8f69400a62c87cf6 + languageName: node + linkType: hard + "abortcontroller-polyfill@npm:^1.7.5": version: 1.7.5 resolution: "abortcontroller-polyfill@npm:1.7.5" @@ -8115,6 +8188,15 @@ __metadata: languageName: node linkType: hard +"isows@npm:1.0.3": + version: 1.0.3 + resolution: "isows@npm:1.0.3" + peerDependencies: + ws: "*" + checksum: 9cacd5cf59f67deb51e825580cd445ab1725ecb05a67c704050383fb772856f3cd5e7da8ad08f5a3bd2823680d77d099459d0c6a7037972a74d6429af61af440 + languageName: node + linkType: hard + "isstream@npm:~0.1.2": version: 0.1.2 resolution: "isstream@npm:0.1.2" @@ -13328,6 +13410,27 @@ __metadata: languageName: node linkType: hard +"viem@npm:^1.6.0": + version: 1.21.4 + resolution: "viem@npm:1.21.4" + dependencies: + "@adraffy/ens-normalize": 1.10.0 + "@noble/curves": 1.2.0 + "@noble/hashes": 1.3.2 + "@scure/bip32": 1.3.2 + "@scure/bip39": 1.2.1 + abitype: 0.9.8 + isows: 1.0.3 + ws: 8.13.0 + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: c351fdea2d53d2d781ac73c964348b3b9fc5dd46f9eb53903e867705fc9e30a893cb9f2c8d7a00acdcdeca27d14eeebf976eed9f948c28c47018dc9211369117 + languageName: node + linkType: hard + "vm-browserify@npm:^1.0.1": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" @@ -14094,6 +14197,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.13.0": + version: 8.13.0 + resolution: "ws@npm:8.13.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c + languageName: node + linkType: hard + "ws@npm:^3.0.0": version: 3.3.3 resolution: "ws@npm:3.3.3" @@ -14325,7 +14443,9 @@ __metadata: "@gnosis.pm/zodiac": ^3.4.2 "@safe-global/api-kit": ^1.3.1 "@safe-global/protocol-kit": ^1.3.0 + "@safe-global/safe-apps-sdk": ^9.0.0 "@safe-global/safe-core-sdk-types": ^2.3.0 + "@safe-global/safe-gateway-typescript-sdk": ^3.14.0 "@shazow/whatsabi": ^0.2.1 "@testing-library/jest-dom": ^5.16.1 "@typechain/ethers-v5": ^10.2.1