├── .gitignore ├── LICENSE ├── README.md ├── examples └── cra-dapp │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.tsx │ ├── contracts │ │ ├── CounterContractAbi.d.ts │ │ ├── CounterContractAbi.hex.ts │ │ ├── factories │ │ │ └── CounterContractAbi__factory.ts │ │ └── index.ts │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ └── reportWebVitals.ts │ └── tsconfig.json ├── package.json ├── packages ├── signature-verification │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Forc.lock │ ├── Forc.toml │ ├── README.md │ ├── fuel-toolchain.toml │ ├── verification-predicate │ │ ├── Cargo.toml │ │ ├── Forc.toml │ │ ├── out │ │ │ └── release │ │ │ │ ├── verification-predicate-abi.json │ │ │ │ ├── verification-predicate-bin-root │ │ │ │ └── verification-predicate.bin │ │ ├── src │ │ │ └── main.sw │ │ └── tests │ │ │ ├── harness.rs │ │ │ └── utils │ │ │ └── mod.rs │ └── verification-script │ │ ├── Cargo.toml │ │ ├── Forc.toml │ │ ├── src │ │ └── main.sw │ │ └── tests │ │ └── harness.rs └── wallet-connector-evm │ ├── .prettierrc │ ├── README.md │ ├── generatePredicateResources.ts │ ├── package.json │ ├── src │ ├── EvmWalletConnector.ts │ ├── eip-1193.ts │ ├── index.ts │ ├── metamask-icon.ts │ └── predicateResources.ts │ ├── test │ ├── chainConfig.json │ ├── mockProvider.ts │ ├── testConnector.ts │ └── walletConnector.test.ts │ ├── tsconfig.json │ └── vite.config.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── turbo.json └── vercel.json /.gitignore: -------------------------------------------------------------------------------- 1 | # predicate 2 | target 3 | packages/signature-verification/verification-script/out 4 | */*.lock 5 | 6 | ## Misc 7 | .DS_Store 8 | vite.config.js.timestamp-*.mjs 9 | 10 | # sdk 11 | node_modules 12 | cache 13 | dist 14 | 15 | # tmp 16 | .github 17 | 18 | # counter contract 19 | counter 20 | .turbo -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > This project has been merged into [Fuel Connectors](https://github.com/FuelLabs/fuel-connectors) 3 | 4 | # Fuel Wallet Connector for MetaMask 5 | 6 | This Connector is part of the effort to enable users to use their current **MetaMask Wallet**, 7 | to sign transactions on Fuel Network. 8 | 9 | > **Warning** 10 | > This project is under active development. 11 | 12 | ## 📗 Table of contents 13 | 14 | - [📗 About EVM Connector](#📗-description) 15 | - [🧑‍💻 Getting Started](#🧑‍💻-getting-started) 16 | - [🧰 Examples](./examples/) 17 | - 🗂️ Project 18 | - [Predicate](./packages/signature-verification/) 19 | - [EVM Wallet Connector](./packages/wallet-connector-evm/) 20 | - [📜 License](#📜-license) 21 | 22 | ## 📗 Description 23 | 24 | The Connector follows the new standard for Fuel compatible [Wallet Connectors](https://github.com/FuelLabs/fuels-wallet/wiki/Fuel-Wallet-Connectors), creating a more integrated ecosystem. 25 | 26 | To enable the use of a MetaMask wallet on Fuel we use [Predicates](https://docs.fuel.network/docs/intro/glossary/#predicate) on Fuel Network, that allow transactions to be validated using a script. 27 | 28 | Below we share a model that explains how our EVM Connector works. 29 | 30 | ```mermaid 31 | sequenceDiagram 32 | participant A as Dapp 33 | participant B as EVM Wallet Connector 34 | participant C as MetaMask (EVM Wallet) 35 | 36 | note over A,C: List Accounts 37 | A->>B: fuel.accounts() 38 | B->>C: ethProvider.request({ "method": "eth_accounts" }) 39 | C-->>B: ["0xa202E75a467726Ad49F76e8914c42433c1Ad821F"] 40 | B->>B: Create a predicate for each ETH account address 41 | B-->>A: ['fuel1s6cswzjfunkarjh9rlr7fdug4r04le2zec9agtudj3gkjwarlwnsw8859m'] 42 | 43 | note over A,C: Send Transaction 44 | A->>B: fuel.sendTransaction("
", { }) 45 | B->>B: Hash transaction Id 46 | B->>C: ethProvider.request({ "method": "personal_sign" }) 47 | C-->>B: "0xa202....222" 48 | B->>B: Send transaction using predicate validation to Fuel Nework 49 | B-->>A: "0x111....222" 50 | ``` 51 | 52 | ## 🧑‍💻 Getting Started 53 | 54 | ### Install 55 | 56 | ```sh 57 | npm install @fuels/wallet-connector-evm @fuel-wallet/sdk@0.15.2 58 | ``` 59 | 60 | ### Using 61 | 62 | ```ts 63 | import { Fuel, defaultConnectors } from "@fuel-wallet/sdk"; 64 | import { EVMWalletConnector } from "@fuels/wallet-connector-evm"; 65 | 66 | const fuel = new Fuel({ 67 | connectors: [ 68 | // Also show other connectors like Fuel Wallet 69 | ...defaultConnectors(), 70 | new EVMWalletConnector(), 71 | ], 72 | }); 73 | 74 | await fuel.selectConnector("EVM wallet connector"); 75 | const connection = await fuel.connect(); 76 | console.log(connection); 77 | ``` 78 | 79 | ## 🚧 Development 80 | 81 | ### Building the project 82 | 83 | ```sh 84 | pnpm build:all 85 | ``` 86 | 87 | ### Tests 88 | 89 | #### Predicate 90 | 91 | ```sh 92 | cd packages/signature-verification 93 | forc build --release 94 | cargo test 95 | ``` 96 | 97 | #### EVM Wallet Connector 98 | 99 | ```sh 100 | cd packages/wallet-connector-evm 101 | pnpm test 102 | ``` 103 | 104 | ## 📜 License 105 | 106 | This repo is licensed under the `Apache-2.0` license. See [`LICENSE`](./LICENSE) for more information. 107 | -------------------------------------------------------------------------------- /examples/cra-dapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/cra-dapp/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /examples/cra-dapp/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /examples/cra-dapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-dapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fuel-wallet/react": "0.15.2", 7 | "@fuel-wallet/sdk": "0.15.2", 8 | "@fuels/wallet-connector-evm": "workspace:*", 9 | "@types/node": "^16.18.68", 10 | "@types/react": "^18.2.45", 11 | "@types/react-dom": "^18.2.18", 12 | "crypto-browserify": "^3.12.0", 13 | "fuels": "0.74.0", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-scripts": "5.0.1", 17 | "typescript": "^4.9.5", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "build": "react-scripts build", 22 | "start": "react-scripts start", 23 | "eject": "react-scripts eject", 24 | "fmt": "prettier --config .prettierrc 'src/*.ts' 'src/*.tsx' --write" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } -------------------------------------------------------------------------------- /examples/cra-dapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/EVM-Wallet-Connector/1e1e62c71a200f6e49b6535c00cc58f342454261/examples/cra-dapp/public/favicon.ico -------------------------------------------------------------------------------- /examples/cra-dapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/cra-dapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/cra-dapp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/cra-dapp/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body * { 7 | box-sizing: border-box; 8 | } 9 | 10 | .App { 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | padding: 20px; 15 | padding-top: 50px; 16 | width: 100vw; 17 | min-height: 100vh; 18 | text-align: center; 19 | } 20 | 21 | .App[data-theme='dark'] { 22 | background-color: #282c34; 23 | color: white; 24 | } 25 | 26 | button { 27 | all: unset; 28 | border: 1px solid black; 29 | padding: 10px 20px; 30 | border-radius: 4px; 31 | box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.2); 32 | transition: box-shadow 0.2s ease-in-out; 33 | cursor: pointer; 34 | } 35 | 36 | button:active { 37 | box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2); 38 | } 39 | 40 | button:disabled { 41 | opacity: 0.3; 42 | cursor: default; 43 | } 44 | 45 | .Actions { 46 | display: flex; 47 | flex-direction: row; 48 | align-items: center; 49 | gap: 10px; 50 | } 51 | 52 | .Accounts > div { 53 | font-style: italic; 54 | border-radius: 4px; 55 | padding: 10px 10px; 56 | } 57 | 58 | .Error { 59 | color: #d60000; 60 | width: 100%; 61 | text-align: center; 62 | } 63 | 64 | .accountActions { 65 | display: flex; 66 | flex-direction: row; 67 | justify-content: flex-end; 68 | gap: 10px; 69 | margin-top: 10px; 70 | margin-bottom: 10px; 71 | max-width: 100vw; 72 | } 73 | 74 | .Info { 75 | padding: 30px 100px; 76 | } 77 | 78 | .Counter { 79 | padding-top: 20px; 80 | } 81 | 82 | .AccountColumns > span { 83 | margin-left: 8px; 84 | } 85 | 86 | .AccountItem::after { 87 | content: ''; 88 | display: block; 89 | width: 70%; 90 | height: 10px; 91 | margin: 0 auto; 92 | border-bottom: 1px solid rgba(0, 0, 0, 0.4); 93 | } 94 | 95 | @media only screen and (max-width: 600px) { 96 | .accountActions { 97 | flex-direction: column; 98 | padding-left: 20px; 99 | padding-right: 20px; 100 | } 101 | 102 | .Info { 103 | padding: 20px 30px; 104 | } 105 | 106 | .Accounts, 107 | .BottomInfo { 108 | width: 100vw; 109 | padding-left: 20px; 110 | padding-right: 20px; 111 | overflow-x: hidden; 112 | word-break: break-word; 113 | } 114 | 115 | .AccountColumns { 116 | display: flex; 117 | flex-direction: column; 118 | } 119 | 120 | .AccountColumns > span { 121 | margin-left: 0px; 122 | margin-top: 8px; 123 | } 124 | } -------------------------------------------------------------------------------- /examples/cra-dapp/src/App.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { useEffect, useState } from 'react'; 3 | import { 4 | useAccounts, 5 | useDisconnect, 6 | useConnectUI, 7 | useWallet, 8 | useBalance, 9 | useIsConnected, 10 | useFuel 11 | } from '@fuel-wallet/react'; 12 | import './App.css'; 13 | import { CounterContractAbi__factory } from './contracts'; 14 | import { bn, Address, BaseAssetId } from 'fuels'; 15 | 16 | const COUNTER_CONTRACT_ID = 17 | '0x0a46aafb83b387155222893b52ed12e5a4b9d6cd06770786f2b5e4307a63b65c'; 18 | const DEFAULT_ADDRESS = Address.fromRandom().toString(); 19 | const DEFAULT_AMOUNT = bn.parseUnits('0.001'); 20 | 21 | function AccountItem({ address }: { address: string }) { 22 | const [isLoading, setLoading] = useState(false); 23 | const [isLoadingCall, setLoadingCall] = useState(false); 24 | const { balance, refetch } = useBalance({ 25 | address 26 | }); 27 | const { wallet } = useWallet(address); 28 | const hasBalance = balance && balance.gte(DEFAULT_AMOUNT); 29 | 30 | useEffect(() => { 31 | const interval = setInterval(() => refetch(), 5000); 32 | return () => clearInterval(interval); 33 | }, [refetch]); 34 | 35 | async function handleTransfer() { 36 | setLoading(true); 37 | try { 38 | const receiverAddress = prompt('Receiver address', DEFAULT_ADDRESS); 39 | const receiver = Address.fromString(receiverAddress || DEFAULT_ADDRESS); 40 | const resp = await wallet?.transfer( 41 | receiver, 42 | DEFAULT_AMOUNT, 43 | BaseAssetId, 44 | { 45 | gasPrice: 1, 46 | gasLimit: 10_000 47 | } 48 | ); 49 | const result = await resp?.waitForResult(); 50 | console.log(result?.status); 51 | } catch (err: any) { 52 | alert(err.message); 53 | } finally { 54 | setLoading(false); 55 | } 56 | } 57 | 58 | async function increment() { 59 | if (wallet) { 60 | setLoadingCall(true); 61 | const contract = CounterContractAbi__factory.connect( 62 | COUNTER_CONTRACT_ID, 63 | wallet 64 | ); 65 | try { 66 | await contract.functions 67 | .increment() 68 | .txParams({ gasPrice: 1, gasLimit: 100_000 }) 69 | .call(); 70 | } catch (err) { 71 | console.log('error sending transaction...', err); 72 | } finally { 73 | setLoadingCall(false); 74 | } 75 | } 76 | } 77 | 78 | return ( 79 |
80 |
81 | 82 | Account: {address}{' '} 83 | 84 | 85 | Balance: {balance?.format() || '0'} ETH 86 | 87 |
88 |
89 | {!hasBalance && ( 90 | 94 | 95 | 96 | )} 97 | 105 | 113 |
114 |
115 | ); 116 | } 117 | 118 | function LogEvents() { 119 | const { fuel } = useFuel(); 120 | useEffect(() => { 121 | const log = (prefix: string) => (data: any) => { 122 | console.log(prefix, data); 123 | }; 124 | const logAccounts = log('accounts'); 125 | const logConnection = log('connection'); 126 | const logCurrentAccount = log('currentAccount'); 127 | 128 | fuel.on(fuel.events.accounts, logAccounts); 129 | fuel.on(fuel.events.connection, logConnection); 130 | fuel.on(fuel.events.currentAccount, logCurrentAccount); 131 | return () => { 132 | fuel.off(fuel.events.accounts, logAccounts); 133 | fuel.off(fuel.events.connection, logConnection); 134 | fuel.off(fuel.events.currentAccount, logCurrentAccount); 135 | }; 136 | }, [fuel]); 137 | 138 | return null; 139 | } 140 | 141 | function ContractCounter() { 142 | const { wallet } = useWallet(); 143 | const { balance } = useBalance({ 144 | address: wallet?.address.toString() 145 | }); 146 | const [counter, setCounter] = useState(0); 147 | const shouldShowCounter = wallet && balance?.gt(0); 148 | 149 | useEffect(() => { 150 | if (!shouldShowCounter) return; 151 | getCount(); 152 | const interval = setInterval(() => getCount(), 5000); 153 | return () => clearInterval(interval); 154 | }, [shouldShowCounter]); 155 | 156 | const getCount = async () => { 157 | const counterContract = CounterContractAbi__factory.connect( 158 | COUNTER_CONTRACT_ID, 159 | wallet! 160 | ); 161 | try { 162 | const { value } = await counterContract.functions 163 | .count() 164 | .txParams({ 165 | gasPrice: 1, 166 | gasLimit: 100_000 167 | }) 168 | .simulate(); 169 | setCounter(value.toNumber()); 170 | } catch (error) { 171 | console.error(error); 172 | } 173 | }; 174 | 175 | if (!shouldShowCounter) return null; 176 | 177 | return ( 178 |
179 |

Counter: {counter}

180 |
181 | ); 182 | } 183 | 184 | function App() { 185 | const { connect, error, isError, theme, setTheme, isConnecting } = 186 | useConnectUI(); 187 | const { disconnect } = useDisconnect(); 188 | const { isConnected, refetch } = useIsConnected(); 189 | const { accounts } = useAccounts(); 190 | const lightTheme = theme === 'light'; 191 | 192 | useEffect(() => { 193 | const interval = setInterval(() => refetch(), 1000); 194 | return () => clearInterval(interval); 195 | }, [refetch]); 196 | 197 | return ( 198 |
199 | 200 |
201 | 209 | {isConnected && ( 210 | 211 | )} 212 | 215 |
216 |
217 | {isConnected && ( 218 | <> 219 |

220 | The connected accounts below are the predicate accounts on Fuel 221 | for each of the connected EVM wallet accounts. 222 |

223 |

224 | You can use an EVM wallet account to send transactions from its 225 | corresponding predicate account. 226 |

227 |

228 | Additional accounts can be connected via the EVM wallet extension. 229 |

230 | 231 | )} 232 |
233 | {isError &&

{error?.message}

} 234 | {isConnected && ( 235 |
236 |

Connected accounts

237 | {accounts?.map((account) => ( 238 | 239 | ))} 240 |
241 | )} 242 | 243 |
244 | {isConnected && ( 245 | <> 246 |

247 | The counter contract is deployed to the address below:{' '} 248 | {COUNTER_CONTRACT_ID}. 249 |

250 | 251 | )} 252 |
253 |
254 | ); 255 | } 256 | 257 | export default App; 258 | -------------------------------------------------------------------------------- /examples/cra-dapp/src/contracts/CounterContractAbi.d.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | 6 | /* 7 | Fuels version: 0.73.0 8 | Forc version: 0.49.2 9 | Fuel-Core version: 0.22.0 10 | */ 11 | 12 | import type { 13 | BigNumberish, 14 | BN, 15 | BytesLike, 16 | Contract, 17 | DecodedValue, 18 | FunctionFragment, 19 | Interface, 20 | InvokeFunction, 21 | } from 'fuels'; 22 | 23 | interface CounterContractAbiInterface extends Interface { 24 | functions: { 25 | count: FunctionFragment; 26 | increment: FunctionFragment; 27 | }; 28 | 29 | encodeFunctionData(functionFragment: 'count', values: []): Uint8Array; 30 | encodeFunctionData(functionFragment: 'increment', values: []): Uint8Array; 31 | 32 | decodeFunctionData(functionFragment: 'count', data: BytesLike): DecodedValue; 33 | decodeFunctionData(functionFragment: 'increment', data: BytesLike): DecodedValue; 34 | } 35 | 36 | export class CounterContractAbi extends Contract { 37 | interface: CounterContractAbiInterface; 38 | functions: { 39 | count: InvokeFunction<[], BN>; 40 | increment: InvokeFunction<[], void>; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /examples/cra-dapp/src/contracts/CounterContractAbi.hex.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | 6 | /* 7 | Fuels version: 0.73.0 8 | Forc version: 0.49.2 9 | Fuel-Core version: 0.22.0 10 | */ 11 | 12 | export default '0x7400000347000000000000000000059c5dfcc00110fff3005d4060495d47f00913490440764800055d47f00a134904407648007172f0007b36f000001aec5000910002185d43f00b104103005d47f00b104513007248002028ed04805fec0004504bb028724c0020284914c05d4fb0045d47f0041b4534405d4bf004104514805d4bf005104514805d4bf0061f4514805d4bf00719493480504fb09872500020284d05005043b1f872500020284135005043b1f8504fb1f85053b0e81ae910001ae5400020f8330058fbe00250fbe004740001131a47d0005053b1081ae810001ae5400020f8330058fbe00250fbe0047400010b1a53d0005057b13872580020285515805047b1b872580020284545805053b178a35154615047b1587250002028453500504fb1785053b198a35114e072440020284144405043b0b872440020284144405047b048724c0020284504c05fec100d5fed200e5043b0485047b1d872480020284504805d43b00d5d47b00e5d4bf0081b4904805d4ff0041b493480264800001a487000504fb1d8394904d0764000065043b0885fec0011504bb12872440010284904407400000a5043b0785fec100f5d4ff0041b453440104524405d4510005fed1010504bb12872440010284904405043b0d872440010284124405d43b0251341004076400001360000005d43b01c244000001aec5000910003f05d53f00b105143005d43f00b104103007244002028ed44405fec00045047b02872480020284504805d4bb0045d43f0041b4124005d47f004104104405d47f005104104405d47f0061f4104405d47f00719452440504bb110724c0020284944c0504fb39072500020284d2500504bb390504fb3905053b1a01ae900001ae5400020f8330058fbe00250fbe004740000a41a43d0005053b1c01ae810001ae5400020f8330058fbe00250fbe0047400009c1a53d0005057b23072580020285505805043b33072580020284145805053b2b0a35154215043b2507250002028413500504fb2b05053b2d0a35104e072400020284944005043b1307248002028414480504bb048724c0020284904c05fec100d5fed100e5043b0485047b37072480020284504805d43b00d5d47b00e5d4bf0081b4904805d4ff0041b493480264800001a487000504fb370394904d0764000065043b1005fec0020504bb22072440010284904407400000a5043b0f05fec101e5d4ff0041b453440104524405d4510005fed101f504bb22072440010284904405043b19072440010284124405d43b0441341004076400001360000005d43f00b104103005d47f00b104513005d4bb033105d2040504bb0a8724c0020284904c05fec001950492028724c0020284914c05d47b0195d4bf0041b4914805d4ff004104924c05d4ff005104924c05d4ff0061f4924c05d4ff007194514c0504fb15072500020284d05005043b3b072500020284135005043b3b0504fb3b05053b1e01ae920001ae5400020f8330058fbe00250fbe004740000361a4bd0005053b2001ae810001ae5400020f8330058fbe00250fbe0047400002e1a53d0005057b2707258002028552580504bb35072580020284945805053b2f0a35154a1504bb2907250002028493500504fb2f05053b310a35124e072480020284144805043b1707248002028414480504bb078724c0020284904c05fec10135fed10145043b0785047b3d072480020284504805d43b0135d47b0145d4bf0081b4904805d4ff0041b493480264800001a487000504fb3d0394934d05d4ff0041b453440104524405f4570005047b3d03b450490240000001af05000910000285ff100005ff110015ff120025ff130035ff3b0041aec5000910000201a43a0001a4790001a4be0005fec00005fec00015fec00025fed00031a43b000724c0020284504c01af51000920000201af9200059f050285d43c0005d47c0015d4bc0025d4fc0035defc004920000284af8000047000000f383b0ce51358be57daa3b725fe44acdb2d880604e367199080b4379c41bb6ed0000000000000008000000000000001f000000000000000500000000000000040000000000000020000000003c5bb3f2000000005842f1be000000000000059c' -------------------------------------------------------------------------------- /examples/cra-dapp/src/contracts/factories/CounterContractAbi__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | 6 | /* 7 | Fuels version: 0.73.0 8 | Forc version: 0.49.2 9 | Fuel-Core version: 0.22.0 10 | */ 11 | 12 | import { Interface, Contract, ContractFactory } from "fuels"; 13 | import type { Provider, Account, AbstractAddress, BytesLike, DeployContractOptions, StorageSlot } from "fuels"; 14 | import type { CounterContractAbi, CounterContractAbiInterface } from "../CounterContractAbi"; 15 | 16 | const _abi = { 17 | "types": [ 18 | { 19 | "typeId": 0, 20 | "type": "()", 21 | "components": [], 22 | "typeParameters": null 23 | }, 24 | { 25 | "typeId": 1, 26 | "type": "u64", 27 | "components": null, 28 | "typeParameters": null 29 | } 30 | ], 31 | "functions": [ 32 | { 33 | "inputs": [], 34 | "name": "count", 35 | "output": { 36 | "name": "", 37 | "type": 1, 38 | "typeArguments": null 39 | }, 40 | "attributes": [ 41 | { 42 | "name": "storage", 43 | "arguments": [ 44 | "read" 45 | ] 46 | } 47 | ] 48 | }, 49 | { 50 | "inputs": [], 51 | "name": "increment", 52 | "output": { 53 | "name": "", 54 | "type": 0, 55 | "typeArguments": null 56 | }, 57 | "attributes": [ 58 | { 59 | "name": "storage", 60 | "arguments": [ 61 | "read", 62 | "write" 63 | ] 64 | } 65 | ] 66 | } 67 | ], 68 | "loggedTypes": [], 69 | "messagesTypes": [], 70 | "configurables": [] 71 | }; 72 | 73 | const _storageSlots: StorageSlot[] = [ 74 | { 75 | "key": "f383b0ce51358be57daa3b725fe44acdb2d880604e367199080b4379c41bb6ed", 76 | "value": "0000000000000000000000000000000000000000000000000000000000000000" 77 | } 78 | ]; 79 | 80 | export class CounterContractAbi__factory { 81 | static readonly abi = _abi; 82 | 83 | static readonly storageSlots = _storageSlots; 84 | 85 | static createInterface(): CounterContractAbiInterface { 86 | return new Interface(_abi) as unknown as CounterContractAbiInterface 87 | } 88 | 89 | static connect( 90 | id: string | AbstractAddress, 91 | accountOrProvider: Account | Provider 92 | ): CounterContractAbi { 93 | return new Contract(id, _abi, accountOrProvider) as unknown as CounterContractAbi 94 | } 95 | 96 | static async deployContract( 97 | bytecode: BytesLike, 98 | wallet: Account, 99 | options: DeployContractOptions = {} 100 | ): Promise { 101 | const factory = new ContractFactory(bytecode, _abi, wallet); 102 | 103 | const { storageSlots } = CounterContractAbi__factory; 104 | 105 | const contract = await factory.deployContract({ 106 | storageSlots, 107 | ...options, 108 | }); 109 | 110 | return contract as unknown as CounterContractAbi; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /examples/cra-dapp/src/contracts/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | 6 | /* 7 | Fuels version: 0.73.0 8 | Forc version: 0.49.2 9 | Fuel-Core version: 0.22.0 10 | */ 11 | 12 | export type { CounterContractAbi } from './CounterContractAbi'; 13 | 14 | export { CounterContractAbi__factory } from './factories/CounterContractAbi__factory'; 15 | -------------------------------------------------------------------------------- /examples/cra-dapp/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | -------------------------------------------------------------------------------- /examples/cra-dapp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { FuelProvider } from '@fuel-wallet/react'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import { EVMWalletConnector } from '@fuels/wallet-connector-evm'; 5 | 6 | import './index.css'; 7 | import App from './App'; 8 | import reportWebVitals from './reportWebVitals'; 9 | 10 | const root = ReactDOM.createRoot( 11 | document.getElementById('root') as HTMLElement 12 | ); 13 | root.render( 14 | 15 | 23 | 24 | 25 | 26 | ); 27 | 28 | // If you want to start measuring performance in your app, pass a function 29 | // to log results (for example: reportWebVitals(console.log)) 30 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 31 | reportWebVitals(); 32 | -------------------------------------------------------------------------------- /examples/cra-dapp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/cra-dapp/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import type { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /examples/cra-dapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "types": ["node"], 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evm-wallet-connector-project", 3 | "private": true, 4 | "license": "Apache-2.0", 5 | "scripts": { 6 | "build:forc": "pnpm forc build --release --path ./packages/signature-verification", 7 | "build:all": "run-s build:forc build:connector", 8 | "build:connector": "pnpm run build --filter=wallet-connector-evm", 9 | "build": "turbo run build" 10 | }, 11 | "devDependencies": { 12 | "turbo": "^1.11.2", 13 | "npm-run-all": "^4.1.5" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/signature-verification/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["verification-predicate", "verification-script"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | authors = ["Fuel Labs "] 7 | version = "0.1.0" 8 | edition = "2021" 9 | homepage = "https://fuel.network/" 10 | license = "Apache-2.0" 11 | repository = "https://github.com/FuelLabs/EVM-Wallet-Connector" 12 | 13 | [workspace.dependencies] 14 | ethers-core = "2.0" 15 | ethers-signers = "2.0" 16 | fuels = { version = "0.55", features = ["fuel-core-lib"] } 17 | tokio = { version = "1.12", features = ["rt", "macros"] } 18 | -------------------------------------------------------------------------------- /packages/signature-verification/Forc.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "core" 3 | source = "path+from-root-C3992B43B72ADB8C" 4 | 5 | [[package]] 6 | name = "std" 7 | source = "git+https://github.com/fuellabs/sway?tag=v0.49.1#2ac7030570f22510b0ac2a7b5ddf7baa20bdc0e1" 8 | dependencies = ["core"] 9 | 10 | [[package]] 11 | name = "verification-predicate" 12 | source = "member" 13 | dependencies = ["std"] 14 | 15 | [[package]] 16 | name = "verification-script" 17 | source = "member" 18 | dependencies = ["std"] 19 | -------------------------------------------------------------------------------- /packages/signature-verification/Forc.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["verification-predicate", "verification-script"] 3 | -------------------------------------------------------------------------------- /packages/signature-verification/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The repository consists of 2 projects 4 | 5 | - verification-predicate 6 | - verification-predicate 7 | 8 | The predicate is used to verify the signer in a transaction is that of a specific EVM wallet. 9 | 10 | The Script exists for the purpose of debugging the predicate. 11 | Scripts can emit logs while predicates cannot therefore values may be inspected in the script. 12 | 13 | The Sway code for the 2 projects ought to be identical except for the declaration of the project type on the first line. 14 | The Rust tests (which utilize our Rust SDK and ethers-rs) ought to be close to identical except for the differences in how we set up scripts and predicates. 15 | 16 | ## Building the projects 17 | 18 | Prerequsite: have `forc` installed. 19 | 20 | In the root of the repository run 21 | 22 | ```bash 23 | forc build --release 24 | ``` 25 | 26 | This will build both projects and is required before tests can be run. 27 | 28 | ## Running the tests 29 | 30 | Prerequsite: have `cargo` installed. 31 | 32 | In the root of the repository run 33 | 34 | ```bash 35 | cargo test 36 | ``` 37 | 38 | This will run the tests for both projects. 39 | 40 | ## Continuous Integration (CI) 41 | 42 | To satisfy CI checks run the following commands from the root of the repository. 43 | 44 | Format Sway files 45 | 46 | ```bash 47 | forc fmt 48 | ``` 49 | 50 | Format Rust files 51 | 52 | ```bash 53 | cargo fmt 54 | ``` 55 | 56 | Check for Rust code suggestions 57 | 58 | ```bash 59 | cargo clippy --all-features --all-targets -- -D warnings 60 | ``` 61 | 62 | Any warnings presented by clippy must be resolved. 63 | -------------------------------------------------------------------------------- /packages/signature-verification/fuel-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "beta-5" 3 | 4 | [components] 5 | forc = "0.50.0" 6 | fuel-core = "0.22.0" 7 | -------------------------------------------------------------------------------- /packages/signature-verification/verification-predicate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "verification-predicate" 3 | description = "A cargo-generate template for Rust + Sway integration testing." 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | authors = { workspace = true } 7 | license = { workspace = true } 8 | repository = { workspace = true } 9 | 10 | [dev-dependencies] 11 | ethers-core = { workspace = true } 12 | ethers-signers = { workspace = true } 13 | fuels = { workspace = true } 14 | tokio = { workspace = true } 15 | 16 | [[test]] 17 | harness = true 18 | name = "integration_tests" 19 | path = "tests/harness.rs" 20 | -------------------------------------------------------------------------------- /packages/signature-verification/verification-predicate/Forc.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["Fuel Labs "] 3 | entry = "main.sw" 4 | license = "Apache-2.0" 5 | name = "verification-predicate" 6 | -------------------------------------------------------------------------------- /packages/signature-verification/verification-predicate/out/release/verification-predicate-abi.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | { 4 | "typeId": 0, 5 | "type": "b256", 6 | "components": null, 7 | "typeParameters": null 8 | }, 9 | { 10 | "typeId": 1, 11 | "type": "bool", 12 | "components": null, 13 | "typeParameters": null 14 | }, 15 | { 16 | "typeId": 2, 17 | "type": "struct EvmAddress", 18 | "components": [ 19 | { 20 | "name": "value", 21 | "type": 0, 22 | "typeArguments": null 23 | } 24 | ], 25 | "typeParameters": null 26 | }, 27 | { 28 | "typeId": 3, 29 | "type": "u64", 30 | "components": null, 31 | "typeParameters": null 32 | } 33 | ], 34 | "functions": [ 35 | { 36 | "inputs": [ 37 | { 38 | "name": "witness_index", 39 | "type": 3, 40 | "typeArguments": null 41 | } 42 | ], 43 | "name": "main", 44 | "output": { 45 | "name": "", 46 | "type": 1, 47 | "typeArguments": null 48 | }, 49 | "attributes": null 50 | } 51 | ], 52 | "loggedTypes": [], 53 | "messagesTypes": [], 54 | "configurables": [ 55 | { 56 | "name": "SIGNER", 57 | "configurableType": { 58 | "name": "", 59 | "type": 2, 60 | "typeArguments": [] 61 | }, 62 | "offset": 1952 63 | } 64 | ] 65 | } -------------------------------------------------------------------------------- /packages/signature-verification/verification-predicate/out/release/verification-predicate-bin-root: -------------------------------------------------------------------------------- 1 | 0xad8bfa6ad9c15d9d57af61964c2372ed1fa1ace00aeefbc8ed5cf07192510d68 -------------------------------------------------------------------------------- /packages/signature-verification/verification-predicate/out/release/verification-predicate.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/EVM-Wallet-Connector/1e1e62c71a200f6e49b6535c00cc58f342454261/packages/signature-verification/verification-predicate/out/release/verification-predicate.bin -------------------------------------------------------------------------------- /packages/signature-verification/verification-predicate/src/main.sw: -------------------------------------------------------------------------------- 1 | predicate; 2 | 3 | use std::{ 4 | b512::B512, 5 | bytes::Bytes, 6 | constants::ZERO_B256, 7 | tx::{ 8 | tx_id, 9 | tx_witness_data, 10 | }, 11 | vm::evm::{ 12 | ecr::ec_recover_evm_address, 13 | evm_address::EvmAddress, 14 | }, 15 | }; 16 | 17 | /// Personal sign prefix for Ethereum inclusive of the 32 bytes for the length of the Tx ID. 18 | /// 19 | /// # Additional Information 20 | /// 21 | /// Take "\x19Ethereum Signed Message:\n32" and converted to hex. 22 | /// The 00000000 at the end is the padding added by Sway to fill the word. 23 | const ETHEREUM_PREFIX = 0x19457468657265756d205369676e6564204d6573736167653a0a333200000000; 24 | 25 | struct SignedData { 26 | /// The id of the transaction to be signed. 27 | transaction_id: b256, 28 | /// EIP-191 personal sign prefix. 29 | ethereum_prefix: b256, 30 | /// Additional data used for reserving memory for hashing (hack). 31 | #[allow(dead_code)] 32 | empty: b256, 33 | } 34 | 35 | configurable { 36 | /// The Ethereum address that signed the transaction. 37 | SIGNER: EvmAddress = EvmAddress { 38 | value: ZERO_B256, 39 | }, 40 | } 41 | 42 | fn main(witness_index: u64) -> bool { 43 | // Retrieve the Ethereum signature from the witness data in the Tx at the specified index. 44 | let signature: B512 = tx_witness_data(witness_index); 45 | 46 | // Hash the Fuel Tx (as the signed message) and attempt to recover the signer from the signature. 47 | let result = ec_recover_evm_address(signature, personal_sign_hash(tx_id())); 48 | 49 | // If the signers match then the predicate has validated the Tx. 50 | if result.is_ok() { 51 | if SIGNER == result.unwrap() { 52 | return true; 53 | } 54 | } 55 | 56 | // Otherwise, an invalid signature has been passed and we invalidate the Tx. 57 | false 58 | } 59 | 60 | /// Return the Keccak-256 hash of the transaction ID in the format of EIP-191. 61 | /// 62 | /// # Arguments 63 | /// 64 | /// * `transaction_id`: [b256] - Fuel Tx ID. 65 | fn personal_sign_hash(transaction_id: b256) -> b256 { 66 | // Hack, allocate memory to reduce manual `asm` code. 67 | let data = SignedData { 68 | transaction_id, 69 | ethereum_prefix: ETHEREUM_PREFIX, 70 | empty: ZERO_B256, 71 | }; 72 | 73 | // Pointer to the data we have signed external to Sway. 74 | let data_ptr = asm(ptr: data.transaction_id) { 75 | ptr 76 | }; 77 | 78 | // The Ethereum prefix is 28 bytes (plus padding we exclude). 79 | // The Tx ID is 32 bytes at the end of the prefix. 80 | let len_to_hash = 28 + 32; 81 | 82 | // Create a buffer in memory to overwrite with the result being the hash. 83 | let mut buffer = b256::min(); 84 | 85 | // Copy the Tx ID to the end of the prefix and hash the exact len of the prefix and id (without 86 | // the padding at the end because that would alter the hash). 87 | asm( 88 | hash: buffer, 89 | tx_id: data_ptr, 90 | end_of_prefix: data_ptr + len_to_hash, 91 | prefix: data.ethereum_prefix, 92 | id_len: 32, 93 | hash_len: len_to_hash, 94 | ) { 95 | mcp end_of_prefix tx_id id_len; 96 | k256 hash prefix hash_len; 97 | } 98 | 99 | // The buffer contains the hash. 100 | buffer 101 | } 102 | -------------------------------------------------------------------------------- /packages/signature-verification/verification-predicate/tests/harness.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use utils::{compact, create_predicate, create_transaction}; 4 | 5 | use fuels::{ 6 | accounts::{Account, ViewOnlyAccount}, 7 | prelude::{launch_provider_and_get_wallet, AssetId, TxPolicies}, 8 | tx::Witness, 9 | types::transaction::Transaction, 10 | }; 11 | 12 | use ethers_core::rand::thread_rng; 13 | use ethers_signers::{LocalWallet, Signer as EthSigner}; 14 | 15 | #[tokio::test] 16 | async fn valid_signature_transfers_funds() { 17 | // Create a Fuel wallet which will fund the predicate for test purposes 18 | let fuel_wallet = launch_provider_and_get_wallet().await.unwrap(); 19 | 20 | // Network related 21 | let fuel_provider = fuel_wallet.provider().unwrap(); 22 | 23 | // Create an Ethereum wallet used to sign the Fuel Transaction ID 24 | let ethereum_wallet = LocalWallet::new(&mut thread_rng()); 25 | 26 | // Create the predicate for signature verification 27 | let predicate = create_predicate(ethereum_wallet.address().0, fuel_provider).await; 28 | 29 | // Define the quantity and asset that the predicate account will contain 30 | let starting_balance = 100; 31 | let asset_id = AssetId::default(); 32 | 33 | // Define the amount that will be transferred from the predicate to the recipient for a test 34 | let transfer_amount = 10; 35 | 36 | // Fund the predicate to check the change of balance upon signature recovery 37 | fuel_wallet 38 | .transfer( 39 | &predicate.address().clone(), 40 | starting_balance, 41 | asset_id, 42 | TxPolicies::default(), 43 | ) 44 | .await 45 | .unwrap(); 46 | 47 | // Create a transaction to send to the Fuel network 48 | let mut script_transaction = create_transaction( 49 | &predicate, 50 | asset_id, 51 | starting_balance, 52 | transfer_amount, 53 | fuel_wallet.address(), 54 | fuel_provider, 55 | ) 56 | .await; 57 | 58 | // Now that we have the Tx the Ethereum wallet must sign the ID of the Fuel Tx 59 | let tx_id = script_transaction.id(fuel_provider.chain_id()); 60 | 61 | // Original signature `{ r, s, v }` which is equivalent to [u8; 65] 62 | let signature = ethereum_wallet.sign_message(*tx_id).await.unwrap(); 63 | 64 | // Convert into compact format `[u8; 64]` for Sway 65 | let compact_signature = compact(&signature); 66 | 67 | // Add the signed data as a witness onto the Tx 68 | script_transaction 69 | .append_witness(Witness::from(compact_signature.to_vec())) 70 | .unwrap(); 71 | 72 | // Check predicate balance before sending the Tx 73 | let balance_before = predicate.get_asset_balance(&asset_id).await.unwrap(); 74 | 75 | // Execute the Tx 76 | let _tx_id = fuel_provider 77 | .send_transaction(script_transaction) 78 | .await 79 | .unwrap(); 80 | 81 | // Check predicate balance after sending the Tx 82 | let balance_after = predicate.get_asset_balance(&asset_id).await.unwrap(); 83 | 84 | assert_eq!(balance_before, starting_balance); 85 | assert_eq!(balance_after, starting_balance - transfer_amount); 86 | } 87 | 88 | #[tokio::test] 89 | async fn invalid_signature_reverts_predicate() { 90 | // Create a Fuel wallet which will fund the predicate for test purposes 91 | let fuel_wallet = launch_provider_and_get_wallet().await.unwrap(); 92 | 93 | // Network related 94 | let fuel_provider = fuel_wallet.provider().unwrap(); 95 | 96 | // Create an Ethereum wallet used to sign the Fuel Transaction ID 97 | let ethereum_wallet = LocalWallet::new(&mut thread_rng()); 98 | 99 | // Create the predicate for signature verification 100 | let predicate = create_predicate(ethereum_wallet.address().0, fuel_provider).await; 101 | 102 | // Define the quantity and asset that the predicate account will contain 103 | let starting_balance = 100; 104 | let asset_id = AssetId::default(); 105 | 106 | // Define the amount that will be transferred from the predicate to the recipient for a test 107 | let transfer_amount = 10; 108 | 109 | // Fund the predicate to check the change of balance upon signature recovery 110 | fuel_wallet 111 | .transfer( 112 | &predicate.address().clone(), 113 | starting_balance, 114 | asset_id, 115 | TxPolicies::default(), 116 | ) 117 | .await 118 | .unwrap(); 119 | 120 | // Create a transaction to send to the Fuel network 121 | let mut script_transaction = create_transaction( 122 | &predicate, 123 | asset_id, 124 | starting_balance, 125 | transfer_amount, 126 | fuel_wallet.address(), 127 | fuel_provider, 128 | ) 129 | .await; 130 | 131 | // Now that we have the Tx the Ethereum wallet must sign the ID of the Fuel Tx 132 | let tx_id = script_transaction.id(fuel_provider.chain_id()); 133 | 134 | // Original signature `{ r, s, v }` which is equivalent to [u8; 65] 135 | let signature = ethereum_wallet.sign_message(*tx_id).await.unwrap(); 136 | 137 | // Convert into compact format `[u8; 64]` for Sway 138 | let mut compact_signature = compact(&signature); 139 | 140 | // Invalidate the signature to force a different address to be recovered 141 | // Flipping 1 byte is sufficient to fail recovery 142 | // Keep it within the bounds of a u8 143 | if compact_signature[0] < 255 { 144 | compact_signature[0] += 1; 145 | } else { 146 | compact_signature[0] -= 1; 147 | } 148 | 149 | // Add the signed data as a witness onto the Tx 150 | script_transaction 151 | .append_witness(Witness::from(compact_signature.to_vec())) 152 | .unwrap(); 153 | 154 | // Execute the Tx, causing a revert because the predicate fails to recovery correct address 155 | let tx_result = fuel_provider.send_transaction(script_transaction).await; 156 | 157 | assert!(tx_result.is_err()); 158 | } 159 | -------------------------------------------------------------------------------- /packages/signature-verification/verification-predicate/tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use fuels::{ 2 | accounts::{predicate::Predicate, Account}, 3 | prelude::{abigen, AssetId, Bech32Address, Provider, ScriptTransaction, TxPolicies}, 4 | types::{ 5 | transaction_builders::{BuildableTransaction, ScriptTransactionBuilder}, 6 | Bits256, EvmAddress, 7 | }, 8 | }; 9 | 10 | use ethers_core::types::{Signature, U256}; 11 | 12 | const PREDICATE_BINARY_PATH: &str = "./out/release/verification-predicate.bin"; 13 | 14 | abigen!(Predicate( 15 | name = "MyPredicate", 16 | abi = "verification-predicate/out/release/verification-predicate-abi.json" 17 | )); 18 | 19 | fn pad_ethereum_address(eth_wallet_address: &[u8]) -> [u8; 32] { 20 | let mut address: [u8; 32] = [0; 32]; 21 | address[12..].copy_from_slice(eth_wallet_address); 22 | address 23 | } 24 | 25 | pub(crate) async fn create_predicate( 26 | ethereum_address: [u8; 20], 27 | fuel_provider: &Provider, 28 | ) -> Predicate { 29 | let padded_ethereum_address = pad_ethereum_address(ðereum_address); 30 | let evm_address = EvmAddress::from(Bits256(padded_ethereum_address)); 31 | 32 | // Create the predicate by setting the signer and pass in the witness argument 33 | let witness_index = 0; 34 | let configurables = MyPredicateConfigurables::new().with_SIGNER(evm_address); 35 | let predicate_data = MyPredicateEncoder::encode_data(witness_index); 36 | 37 | Predicate::load_from(PREDICATE_BINARY_PATH) 38 | .unwrap() 39 | .with_provider(fuel_provider.clone()) 40 | .with_data(predicate_data) 41 | .with_configurables(configurables) 42 | } 43 | 44 | pub(crate) async fn create_transaction( 45 | predicate: &Predicate, 46 | asset_id: AssetId, 47 | starting_balance: u64, 48 | transfer_amount: u64, 49 | recipient: &Bech32Address, 50 | provider: &Provider, 51 | ) -> ScriptTransaction { 52 | // Fetch predicate input in order to have a UTXO with funds for transfer 53 | let inputs = predicate 54 | .get_asset_inputs_for_amount(asset_id, starting_balance) 55 | .await 56 | .unwrap(); 57 | 58 | // Specify amount to transfer to recipient, send the rest back to the predicate 59 | let outputs = predicate.get_asset_outputs_for_amount(recipient, asset_id, transfer_amount); 60 | 61 | // Create the Tx 62 | let transaction_builder = ScriptTransactionBuilder::prepare_transfer( 63 | inputs, 64 | outputs, 65 | TxPolicies::default().with_witness_limit(72), 66 | ); 67 | 68 | transaction_builder.build(provider).await.unwrap() 69 | } 70 | 71 | // This can probably be cleaned up 72 | pub(crate) fn compact(signature: &Signature) -> [u8; 64] { 73 | let shifted_parity = U256::from(signature.v - 27) << 255; 74 | 75 | let r = signature.r; 76 | let y_parity_and_s = shifted_parity | signature.s; 77 | 78 | let mut sig = [0u8; 64]; 79 | let mut r_bytes = [0u8; 32]; 80 | let mut s_bytes = [0u8; 32]; 81 | r.to_big_endian(&mut r_bytes); 82 | y_parity_and_s.to_big_endian(&mut s_bytes); 83 | sig[..32].copy_from_slice(&r_bytes); 84 | sig[32..64].copy_from_slice(&s_bytes); 85 | 86 | sig 87 | } 88 | -------------------------------------------------------------------------------- /packages/signature-verification/verification-script/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "verification-script" 3 | description = "A cargo-generate template for Rust + Sway integration testing." 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | authors = { workspace = true } 7 | license = { workspace = true } 8 | repository = { workspace = true } 9 | 10 | [dev-dependencies] 11 | ethers-core = { workspace = true } 12 | ethers-signers = { workspace = true } 13 | fuels = { workspace = true } 14 | tokio = { workspace = true } 15 | 16 | [[test]] 17 | harness = true 18 | name = "integration_tests" 19 | path = "tests/harness.rs" 20 | -------------------------------------------------------------------------------- /packages/signature-verification/verification-script/Forc.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["Fuel Labs "] 3 | entry = "main.sw" 4 | license = "Apache-2.0" 5 | name = "verification-script" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /packages/signature-verification/verification-script/src/main.sw: -------------------------------------------------------------------------------- 1 | script; 2 | 3 | use std::{ 4 | b512::B512, 5 | constants::ZERO_B256, 6 | tx::{ 7 | tx_id, 8 | tx_witness_data, 9 | }, 10 | vm::evm::{ 11 | ecr::ec_recover_evm_address, 12 | evm_address::EvmAddress, 13 | }, 14 | }; 15 | 16 | /// Personal sign prefix for Ethereum inclusive of the 32 bytes for the length of the Tx ID. 17 | /// 18 | /// # Additional Information 19 | /// 20 | /// Take "\x19Ethereum Signed Message:\n32" and converted to hex. 21 | /// The 00000000 at the end is the padding added by Sway to fill the word. 22 | const ETHEREUM_PREFIX = 0x19457468657265756d205369676e6564204d6573736167653a0a333200000000; 23 | 24 | struct SignedData { 25 | /// The id of the transaction to be signed. 26 | transaction_id: b256, 27 | /// EIP-191 personal sign prefix. 28 | ethereum_prefix: b256, 29 | /// Additional data used for reserving memory for hashing (hack). 30 | #[allow(dead_code)] 31 | empty: b256, 32 | } 33 | 34 | configurable { 35 | /// The Ethereum address that signed the transaction. 36 | SIGNER: EvmAddress = EvmAddress { 37 | value: ZERO_B256, 38 | }, 39 | } 40 | 41 | fn main(witness_index: u64) -> bool { 42 | // Retrieve the Ethereum signature from the witness data in the Tx at the specified index. 43 | let signature: B512 = tx_witness_data(witness_index); 44 | 45 | // Hash the Fuel Tx (as the signed message) and attempt to recover the signer from the signature. 46 | let result = ec_recover_evm_address(signature, personal_sign_hash(tx_id())); 47 | 48 | // If the signers match then the predicate has validated the Tx. 49 | if result.is_ok() { 50 | if SIGNER == result.unwrap() { 51 | return true; 52 | } 53 | } 54 | 55 | // Otherwise, an invalid signature has been passed and we invalidate the Tx. 56 | false 57 | } 58 | 59 | /// Return the Keccak-256 hash of the transaction ID in the format of EIP-191. 60 | /// 61 | /// # Arguments 62 | /// 63 | /// * `transaction_id`: [b256] - Fuel Tx ID. 64 | fn personal_sign_hash(transaction_id: b256) -> b256 { 65 | // Hack, allocate memory to reduce manual `asm` code. 66 | let data = SignedData { 67 | transaction_id, 68 | ethereum_prefix: ETHEREUM_PREFIX, 69 | empty: ZERO_B256, 70 | }; 71 | 72 | // Pointer to the data we have signed external to Sway. 73 | let data_ptr = asm(ptr: data.transaction_id) { 74 | ptr 75 | }; 76 | 77 | // The Ethereum prefix is 28 bytes (plus padding we exclude). 78 | // The Tx ID is 32 bytes at the end of the prefix. 79 | let len_to_hash = 28 + 32; 80 | 81 | // Create a buffer in memory to overwrite with the result being the hash. 82 | let mut buffer = b256::min(); 83 | 84 | // Copy the Tx ID to the end of the prefix and hash the exact len of the prefix and id (without 85 | // the padding at the end because that would alter the hash). 86 | asm( 87 | hash: buffer, 88 | tx_id: data_ptr, 89 | end_of_prefix: data_ptr + len_to_hash, 90 | prefix: data.ethereum_prefix, 91 | id_len: 32, 92 | hash_len: len_to_hash, 93 | ) { 94 | mcp end_of_prefix tx_id id_len; 95 | k256 hash prefix hash_len; 96 | } 97 | 98 | // The buffer contains the hash. 99 | buffer 100 | } 101 | -------------------------------------------------------------------------------- /packages/signature-verification/verification-script/tests/harness.rs: -------------------------------------------------------------------------------- 1 | use fuels::{ 2 | prelude::{abigen, launch_provider_and_get_wallet, TxPolicies}, 3 | tx::Witness, 4 | types::{transaction::Transaction, Bits256, EvmAddress}, 5 | }; 6 | 7 | use ethers_core::{ 8 | rand::thread_rng, 9 | types::{Signature, U256}, 10 | }; 11 | use ethers_signers::{LocalWallet, Signer as EthSigner}; 12 | 13 | const SCRIPT_BINARY_PATH: &str = "./out/release/verification-script.bin"; 14 | 15 | abigen!(Script( 16 | name = "MyScript", 17 | abi = "verification-script/out/release/verification-script-abi.json" 18 | )); 19 | 20 | fn convert_eth_address(eth_wallet_address: &[u8]) -> [u8; 32] { 21 | let mut address: [u8; 32] = [0; 32]; 22 | address[12..].copy_from_slice(eth_wallet_address); 23 | address 24 | } 25 | 26 | #[tokio::test] 27 | async fn valid_signature_returns_true_for_validating() { 28 | // Create a Fuel wallet which will fund the predicate for test purposes 29 | let fuel_wallet = launch_provider_and_get_wallet().await.unwrap(); 30 | 31 | // Create eth wallet and convert to EVMAddress 32 | let eth_wallet = LocalWallet::new(&mut thread_rng()); 33 | let padded_eth_address = convert_eth_address(ð_wallet.address().0); 34 | let evm_address = EvmAddress::from(Bits256(padded_eth_address)); 35 | 36 | // Create the predicate by setting the signer and pass in the witness argument 37 | let witness_index = 1; 38 | let configurables = MyScriptConfigurables::new().with_SIGNER(evm_address); 39 | 40 | let script_call_handler = MyScript::new(fuel_wallet.clone(), SCRIPT_BINARY_PATH) 41 | .with_configurables(configurables) 42 | .main(witness_index) 43 | .with_tx_policies( 44 | TxPolicies::default() 45 | .with_witness_limit(144) 46 | .with_script_gas_limit(1_000_000), 47 | ); 48 | 49 | let mut tx = script_call_handler.build_tx().await.unwrap(); 50 | 51 | // Now that we have the Tx the ethereum wallet must sign the ID 52 | let consensus_parameters = fuel_wallet.provider().unwrap().consensus_parameters(); 53 | let tx_id = tx.id(consensus_parameters.chain_id); 54 | 55 | let signature = eth_wallet.sign_message(*tx_id).await.unwrap(); 56 | 57 | // Convert into compact format `[u8; 64]` for Sway 58 | let compact_signature = compact(&signature); 59 | 60 | // Add the signed data as a witness onto the Tx 61 | tx.append_witness(Witness::from(compact_signature.to_vec())) 62 | .unwrap(); 63 | 64 | // Execute the Tx 65 | let tx_id = fuel_wallet 66 | .provider() 67 | .unwrap() 68 | .send_transaction(tx) 69 | .await 70 | .unwrap(); 71 | 72 | let tx_status = fuel_wallet 73 | .provider() 74 | .unwrap() 75 | .tx_status(&tx_id) 76 | .await 77 | .unwrap(); 78 | 79 | let response = script_call_handler.get_response_from(tx_status).unwrap(); 80 | 81 | assert!(response.value); 82 | } 83 | 84 | #[tokio::test] 85 | async fn invalid_signature_returns_false_for_failed_validation() { 86 | // Create a Fuel wallet which will fund the predicate for test purposes 87 | let fuel_wallet = launch_provider_and_get_wallet().await.unwrap(); 88 | 89 | // Create eth wallet and convert to EVMAddress 90 | let eth_wallet = LocalWallet::new(&mut thread_rng()); 91 | let padded_eth_address = convert_eth_address(ð_wallet.address().0); 92 | let evm_address = EvmAddress::from(Bits256(padded_eth_address)); 93 | 94 | // Create the predicate by setting the signer and pass in the witness argument 95 | let witness_index = 1; 96 | let configurables = MyScriptConfigurables::new().with_SIGNER(evm_address); 97 | 98 | let script_call_handler = MyScript::new(fuel_wallet.clone(), SCRIPT_BINARY_PATH) 99 | .with_configurables(configurables) 100 | .main(witness_index) 101 | .with_tx_policies( 102 | TxPolicies::default() 103 | .with_witness_limit(144) 104 | .with_script_gas_limit(1_000_000), 105 | ); 106 | 107 | let mut tx = script_call_handler.build_tx().await.unwrap(); 108 | 109 | // Now that we have the Tx the ethereum wallet must sign the ID 110 | let consensus_parameters = fuel_wallet.provider().unwrap().consensus_parameters(); 111 | let tx_id = tx.id(consensus_parameters.chain_id); 112 | 113 | let signature = eth_wallet.sign_message(*tx_id).await.unwrap(); 114 | 115 | // Convert into compact format `[u8; 64]` for Sway 116 | let mut compact_signature = compact(&signature); 117 | 118 | // Invalidate the signature to force a different address to be recovered 119 | // Flipping 1 byte is sufficient to fail recovery 120 | // Keep it within the bounds of a u8 121 | if compact_signature[0] < 255 { 122 | compact_signature[0] += 1; 123 | } else { 124 | compact_signature[0] -= 1; 125 | } 126 | 127 | // Add the signed data as a witness onto the Tx 128 | tx.append_witness(Witness::from(compact_signature.to_vec())) 129 | .unwrap(); 130 | 131 | // Execute the Tx 132 | let tx_id = fuel_wallet 133 | .provider() 134 | .unwrap() 135 | .send_transaction(tx) 136 | .await 137 | .unwrap(); 138 | 139 | let tx_status = fuel_wallet 140 | .provider() 141 | .unwrap() 142 | .tx_status(&tx_id) 143 | .await 144 | .unwrap(); 145 | 146 | let response = script_call_handler.get_response_from(tx_status).unwrap(); 147 | 148 | assert!(!response.value); 149 | } 150 | 151 | // This can probably be cleaned up 152 | fn compact(signature: &Signature) -> [u8; 64] { 153 | let shifted_parity = U256::from(signature.v - 27) << 255; 154 | 155 | let r = signature.r; 156 | let y_parity_and_s = shifted_parity | signature.s; 157 | 158 | let mut sig = [0u8; 64]; 159 | let mut r_bytes = [0u8; 32]; 160 | let mut s_bytes = [0u8; 32]; 161 | r.to_big_endian(&mut r_bytes); 162 | y_parity_and_s.to_big_endian(&mut s_bytes); 163 | sig[..32].copy_from_slice(&r_bytes); 164 | sig[32..64].copy_from_slice(&s_bytes); 165 | 166 | sig 167 | } 168 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/README.md: -------------------------------------------------------------------------------- 1 | # Fuel Wallet Connector for MetaMask 2 | 3 | This Connector is part of the effort to enable users to use their current **MetaMask Wallet**, 4 | to sign transactions on Fuel Network. 5 | 6 | > **Warning** 7 | > This project is under active development. 8 | 9 | ## 🧑‍💻 Getting Started 10 | 11 | ### Install 12 | 13 | ```sh 14 | npm install @fuels/wallet-connector-evm @fuel-wallet/sdk@0.15.2 15 | ``` 16 | 17 | ### Using 18 | 19 | ```ts 20 | import { Fuel, defaultConnectors } from '@fuel-wallet/sdk'; 21 | import { EVMWalletConnector } from '@fuels/wallet-connector-evm'; 22 | 23 | const fuel = new Fuel({ 24 | connectors: [ 25 | // Also show other connectors like Fuel Wallet 26 | ...defaultConnectors(), 27 | new EVMWalletConnector() 28 | ] 29 | }); 30 | 31 | await fuel.selectConnector('EVM wallet connector'); 32 | const connection = await fuel.connect(); 33 | console.log(connection); 34 | ``` 35 | 36 | ## 📜 License 37 | 38 | This repo is licensed under the `Apache-2.0` license. See [`LICENSE`](../../LICENSE) for more information. 39 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/generatePredicateResources.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | const predicates = ['verification-predicate']; 9 | 10 | let code = 'export const predicates = {\n'; 11 | 12 | predicates.forEach((predicate) => { 13 | const outputDirectory = `${__dirname}/../signature-verification/${predicate}/out/release`; 14 | const abiPath = `${outputDirectory}/${predicate}-abi.json`; 15 | const bytecodePath = `${outputDirectory}/${predicate}.bin`; 16 | 17 | const abi = fs.readFileSync(abiPath, 'utf8'); 18 | const bytecode = fs.readFileSync(bytecodePath); 19 | 20 | code += ` '${predicate}': {\n`; 21 | code += ` abi: ${abi},\n`; 22 | code += ` bytecode: base64ToUint8Array('${bytecode.toString('base64')}'),\n`; 23 | code += ` },\n`; 24 | }); 25 | 26 | code += ` 27 | }; 28 | 29 | function base64ToUint8Array(base64: string) { 30 | var binaryString = atob(base64); 31 | var bytes = new Uint8Array(binaryString.length); 32 | for (var i = 0; i < binaryString.length; i++) { 33 | bytes[i] = binaryString.charCodeAt(i); 34 | } 35 | return bytes; 36 | } 37 | ` 38 | 39 | fs.writeFileSync(`${__dirname}/src/predicateResources.ts`, code); 40 | console.log('Generated'); 41 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fuels/wallet-connector-evm", 3 | "version": "0.0.2", 4 | "type": "module", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "./dist/wallet-connector-evm.umd.cjs", 9 | "module": "./dist/wallet-connector-evm.js", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/wallet-connector-evm.js", 13 | "require": "./dist/wallet-connector-evm.umd.cjs" 14 | } 15 | }, 16 | "types": "./dist/index.d.ts", 17 | "scripts": { 18 | "build:resources": "tsx generatePredicateResources.ts", 19 | "build": "run-s build:resources && tsc && vite build", 20 | "fmt": "prettier --config .prettierrc 'src/*.ts' 'test/*.ts' --write", 21 | "test": "run-s build:resources && mocha --require ts-node/register test/*.test.ts" 22 | }, 23 | "dependencies": { 24 | "@ethereumjs/util": "^9.0.1", 25 | "@ethersproject/bytes": "^5.7.0", 26 | "@fuel-wallet/sdk": "0.15.2", 27 | "@fuel-wallet/types": "0.15.2", 28 | "fuels": "0.74.0", 29 | "@fuel-ts/account": "0.74.0", 30 | "json-rpc-2.0": "^1.7.0", 31 | "memoizee": "^0.4.15" 32 | }, 33 | "devDependencies": { 34 | "@fuels/ts-config": "^0.1.4", 35 | "@types/chai": "^4.3.11", 36 | "@types/chai-as-promised": "^7.1.8", 37 | "@types/memoizee": "^0.4.11", 38 | "@types/mocha": "^10.0.6", 39 | "@types/node": "^20.10.5", 40 | "chai": "^4.3.10", 41 | "chai-as-promised": "^7.1.1", 42 | "mocha": "^10.2.0", 43 | "prettier": "^3.1.1", 44 | "ts-loader": "^9.5.1", 45 | "ts-node": "^10.9.2", 46 | "tsx": "^4.7.0", 47 | "typescript": "~5.2.2", 48 | "vite": "^5.0.10", 49 | "vite-plugin-dts": "^3.6.4", 50 | "webpack": "^5.89.0", 51 | "webpack-cli": "^5.1.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/src/EvmWalletConnector.ts: -------------------------------------------------------------------------------- 1 | // External libraries 2 | import { 3 | BytesLike, 4 | hexlify, 5 | arrayify, 6 | splitSignature 7 | } from '@ethersproject/bytes'; 8 | import { hexToBytes } from '@ethereumjs/util'; 9 | import memoize from 'memoizee'; 10 | 11 | import { Asset, AbiMap } from '@fuel-wallet/types'; 12 | import { 13 | Provider, 14 | transactionRequestify, 15 | type TransactionRequestLike, 16 | JsonAbi, 17 | Predicate, 18 | Address, 19 | InputValue, 20 | getPredicateRoot 21 | } from 'fuels'; 22 | import { 23 | FuelConnector, 24 | Network, 25 | Version, 26 | ConnectorMetadata 27 | } from '@fuel-wallet/sdk'; 28 | 29 | import { EIP1193Provider } from './eip-1193'; 30 | import { predicates } from './predicateResources'; 31 | import { METAMASK_ICON } from './metamask-icon'; 32 | 33 | type EVMWalletConnectorConfig = { 34 | fuelProvider?: Provider | string; 35 | ethProvider?: EIP1193Provider; 36 | }; 37 | 38 | const HAS_WINDOW = typeof window !== 'undefined'; 39 | const WINDOW: any = HAS_WINDOW ? window : {}; 40 | 41 | export class EVMWalletConnector extends FuelConnector { 42 | ethProvider: EIP1193Provider | null = null; 43 | fuelProvider: Provider | null = null; 44 | private predicate: { abi: any; bytecode: Uint8Array }; 45 | private setupLock: boolean = false; 46 | private _currentAccount: string | null = null; 47 | private config: Required; 48 | private _ethereumEvents: number = 0; 49 | 50 | // metadata placeholder 51 | metadata: ConnectorMetadata = { 52 | image: METAMASK_ICON, 53 | install: { 54 | action: 'Install', 55 | description: 'Install a ethereum Wallet to connect to Fuel', 56 | link: 'https://ethereum.org/en/wallets/find-wallet/' 57 | } 58 | }; 59 | 60 | constructor(config: EVMWalletConnectorConfig = {}) { 61 | super(); 62 | this.name = 'EVM wallet connector'; 63 | this.predicate = predicates['verification-predicate']; 64 | this.installed = true; 65 | this.config = Object.assign(config, { 66 | fuelProvider: 'https://beta-5.fuel.network/graphql', 67 | ethProvider: config.ethProvider || (window as any).ethereum 68 | }); 69 | this.setupEthereumEvents(); 70 | } 71 | 72 | setupEthereumEvents() { 73 | this._ethereumEvents = Number(setInterval(() => { 74 | if (WINDOW.ethereum) { 75 | clearInterval(this._ethereumEvents); 76 | window.dispatchEvent( 77 | new CustomEvent('FuelConnector', { detail: this }), 78 | ); 79 | } 80 | }, 500)); 81 | } 82 | 83 | async getLazyEthereum() { 84 | if (this.config.ethProvider) return this.config.ethProvider; 85 | return WINDOW.ethereum; 86 | } 87 | 88 | /** 89 | * ============================================================ 90 | * Application communication methods 91 | * ============================================================ 92 | */ 93 | 94 | async getProviders() { 95 | if (!this.fuelProvider || !this.ethProvider) { 96 | this.ethProvider = await this.getLazyEthereum(); 97 | 98 | if (!this.ethProvider) { 99 | throw new Error('Ethereum provider not found'); 100 | } 101 | 102 | if (typeof this.config.fuelProvider === 'string') { 103 | this.fuelProvider = await Provider.create(this.config.fuelProvider); 104 | } else { 105 | this.fuelProvider = this.config.fuelProvider; 106 | } 107 | 108 | if (!this.fuelProvider) { 109 | throw new Error('Fuel provider not found'); 110 | } 111 | } 112 | 113 | return { fuelProvider: this.fuelProvider, ethProvider: this.ethProvider }; 114 | } 115 | 116 | async setup() { 117 | if (this.setupLock) return; 118 | this.setupLock = true; 119 | await this.setupCurrentAccount(); 120 | await this.setupEventBridge(); 121 | } 122 | 123 | async setupEventBridge() { 124 | const { ethProvider } = await this.getProviders(); 125 | ethProvider.on('accountsChanged', async (accounts) => { 126 | this.emit('accounts', await this.accounts()); 127 | if (this._currentAccount !== accounts[0]) { 128 | await this.setupCurrentAccount(); 129 | } 130 | }); 131 | ethProvider.on('connect', async (arg) => { 132 | this.emit('connection', await this.isConnected()); 133 | }); 134 | ethProvider.on('disconnect', async (arg) => { 135 | this.emit('connection', await this.isConnected()); 136 | }); 137 | } 138 | 139 | async setupCurrentAccount() { 140 | const [currentAccount = null] = await this.accounts(); 141 | this._currentAccount = currentAccount; 142 | this.emit('currentAccount', currentAccount); 143 | } 144 | 145 | /** 146 | * ============================================================ 147 | * Connector methods 148 | * ============================================================ 149 | */ 150 | 151 | async ping(): Promise { 152 | await this.getProviders(); 153 | await this.setup(); 154 | return true; 155 | } 156 | 157 | async version(): Promise { 158 | return { app: '0.0.0', network: '0.0.0' }; 159 | } 160 | 161 | async isConnected(): Promise { 162 | const accounts = await this.accounts(); 163 | return accounts.length > 0; 164 | } 165 | 166 | async accounts(): Promise> { 167 | const accounts = await this.getPredicateAccounts(); 168 | return accounts.map((account) => account.predicateAccount); 169 | } 170 | 171 | async connect(): Promise { 172 | if (!(await this.isConnected())) { 173 | const { ethProvider } = await this.getProviders(); 174 | await ethProvider.request({ 175 | method: 'wallet_requestPermissions', 176 | params: [ 177 | { 178 | eth_accounts: {} 179 | } 180 | ] 181 | }); 182 | const wallet_chain_id = await ethProvider.request({ 183 | method: 'eth_chainId', 184 | params: [] 185 | }); 186 | if (wallet_chain_id != '0x1') { 187 | await ethProvider.request({ 188 | method: 'wallet_switchEthereumChain', 189 | params: [ 190 | { 191 | chainId: '0x1' 192 | } 193 | ] 194 | }); 195 | } 196 | } 197 | this.connected = true; 198 | return true; 199 | } 200 | 201 | async disconnect(): Promise { 202 | if (await this.isConnected()) { 203 | const { ethProvider } = await this.getProviders(); 204 | await ethProvider.request({ 205 | method: 'wallet_revokePermissions', 206 | params: [ 207 | { 208 | eth_accounts: {} 209 | } 210 | ] 211 | }); 212 | } 213 | this.connected = false; 214 | return true; 215 | } 216 | 217 | async signMessage(address: string, message: string): Promise { 218 | throw new Error('A predicate account cannot sign messages'); 219 | } 220 | 221 | async sendTransaction( 222 | address: string, 223 | transaction: TransactionRequestLike 224 | ): Promise { 225 | if (!(await this.isConnected())) { 226 | throw Error('No connected accounts'); 227 | } 228 | const { ethProvider, fuelProvider } = await this.getProviders(); 229 | const chainId = fuelProvider.getChainId(); 230 | const account = await this.getPredicateFromAddress(address); 231 | if (!account) { 232 | throw Error(`No account found for ${address}`); 233 | } 234 | const transactionRequest = transactionRequestify(transaction); 235 | 236 | // Create a predicate and set the witness index to call in predicate` 237 | const predicate = createPredicate( 238 | account.ethAccount, 239 | fuelProvider, 240 | this.predicate.bytecode, 241 | this.predicate.abi 242 | ); 243 | predicate.connect(fuelProvider); 244 | predicate.setData(transactionRequest.witnesses.length); 245 | 246 | // Attach missing inputs (including estimated predicate gas usage) / outputs to the request 247 | await predicate.provider.estimateTxDependencies(transactionRequest); 248 | 249 | // To each input of the request, attach the predicate and its data 250 | const requestWithPredicateAttached = 251 | predicate.populateTransactionPredicateData(transactionRequest); 252 | 253 | const txID = requestWithPredicateAttached.getTransactionId(chainId); 254 | const signature = await ethProvider.request({ 255 | method: 'personal_sign', 256 | params: [txID, account.ethAccount] 257 | }); 258 | 259 | // Transform the signature into compact form for Sway to understand 260 | const compactSignature = splitSignature(hexToBytes(signature)).compact; 261 | 262 | // We have a witness, attach it to the transaction for inspection / recovery via the predicate 263 | // TODO: is below comment still relevant? 264 | // TODO: not that there is a strange witness before we add out compact signature 265 | // it is [ 0x ] and we may need to update versions later if / when this is fixed 266 | transactionRequest.witnesses.push(compactSignature); 267 | 268 | const transactionWithPredicateEstimated = 269 | await fuelProvider.estimatePredicates(requestWithPredicateAttached); 270 | 271 | const response = await fuelProvider.operations.submit({ 272 | encodedTransaction: hexlify( 273 | transactionWithPredicateEstimated.toTransactionBytes() 274 | ) 275 | }); 276 | 277 | return response.submit.id; 278 | } 279 | 280 | async currentAccount(): Promise { 281 | if (!(await this.isConnected())) { 282 | throw Error('No connected accounts'); 283 | } 284 | 285 | const { ethProvider, fuelProvider } = await this.getProviders(); 286 | const ethAccounts: string[] = await ethProvider.request({ 287 | method: 'eth_accounts' 288 | }); 289 | 290 | if (ethAccounts.length === 0) { 291 | throw Error('No accounts found'); 292 | } 293 | 294 | // Eth Wallet (MetaMask at least) return the current select account as the first 295 | // item in the accounts list. 296 | const fuelAccount = getPredicateAddress( 297 | ethAccounts[0]!, 298 | this.predicate.bytecode, 299 | this.predicate.abi 300 | ); 301 | 302 | return fuelAccount; 303 | } 304 | 305 | async addAssets(assets: Asset[]): Promise { 306 | console.warn('A predicate account cannot add assets'); 307 | return false; 308 | } 309 | 310 | async addAsset(asset: Asset): Promise { 311 | console.warn('A predicate account cannot add an asset'); 312 | return false; 313 | } 314 | 315 | async assets(): Promise> { 316 | // TODO: can get assets at a predicates address? emit warning/throw error if not? 317 | return []; 318 | } 319 | 320 | async addNetwork(networkUrl: string): Promise { 321 | console.warn('Cannot add a network'); 322 | return false; 323 | } 324 | 325 | async selectNetwork(_network: Network): Promise { 326 | // TODO: actually allow selecting networks once mainnet is released? 327 | console.warn('Cannot select a network'); 328 | return false; 329 | } 330 | 331 | async networks(): Promise { 332 | return [await this.currentNetwork()]; 333 | } 334 | 335 | async currentNetwork(): Promise { 336 | const { fuelProvider } = await this.getProviders(); 337 | const chainId = fuelProvider.getChainId(); 338 | return { url: fuelProvider.url, chainId: chainId }; 339 | } 340 | 341 | async addAbi(abiMap: AbiMap): Promise { 342 | console.warn('Cannot add an ABI to a predicate account'); 343 | return false; 344 | } 345 | 346 | async getAbi(contractId: string): Promise { 347 | throw Error('Cannot get contractId ABI for a predicate'); 348 | } 349 | 350 | async hasAbi(contractId: string): Promise { 351 | console.warn('A predicate account cannot have an ABI'); 352 | return false; 353 | } 354 | 355 | private async getPredicateFromAddress(address: string) { 356 | const accounts = await this.getPredicateAccounts(); 357 | return accounts.find((account) => account.predicateAccount === address); 358 | } 359 | 360 | private async getPredicateAccounts(): Promise< 361 | Array<{ 362 | ethAccount: string; 363 | predicateAccount: string; 364 | }> 365 | > { 366 | const { ethProvider, fuelProvider } = await this.getProviders(); 367 | const ethAccounts: Array = await ethProvider.request({ 368 | method: 'eth_accounts' 369 | }); 370 | const chainId = fuelProvider.getChainId(); 371 | const accounts = ethAccounts.map((account) => ({ 372 | ethAccount: account, 373 | predicateAccount: getPredicateAddress( 374 | account, 375 | this.predicate.bytecode, 376 | this.predicate.abi 377 | ) 378 | })); 379 | return accounts; 380 | } 381 | } 382 | 383 | export const getPredicateAddress = memoize( 384 | ( 385 | ethAddress: string, 386 | predicateBytecode: BytesLike, 387 | predicateAbi: JsonAbi 388 | ): string => { 389 | const configurable = { 390 | SIGNER: Address.fromB256( 391 | ethAddress.replace('0x', '0x000000000000000000000000') 392 | ).toEvmAddress() 393 | }; 394 | 395 | // @ts-ignore 396 | const { predicateBytes } = Predicate.processPredicateData( 397 | predicateBytecode, 398 | predicateAbi, 399 | configurable 400 | ); 401 | const address = Address.fromB256(getPredicateRoot(predicateBytes)); 402 | return address.toString(); 403 | } 404 | ); 405 | 406 | export const createPredicate = memoize(function createPredicate( 407 | ethAddress: string, 408 | provider: Provider, 409 | predicateBytecode: BytesLike, 410 | predicateAbi: JsonAbi 411 | ): Predicate { 412 | const configurable = { 413 | SIGNER: Address.fromB256( 414 | ethAddress.replace('0x', '0x000000000000000000000000') 415 | ).toEvmAddress() 416 | }; 417 | 418 | const predicate = new Predicate( 419 | arrayify(predicateBytecode), 420 | provider, 421 | predicateAbi, 422 | configurable 423 | ); 424 | 425 | return predicate; 426 | }); 427 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/src/eip-1193.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | export interface EIP1193Provider extends EventEmitter { 4 | request(args: { method: string; params?: any[] }): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EvmWalletConnector'; 2 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/src/metamask-icon.ts: -------------------------------------------------------------------------------- 1 | export const METAMASK_ICON = 2 | 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIGlkPSJMYXllcl8xIiB4PSIwIiB5PSIwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMTguNiAzMTguNiI+CiAgPHN0eWxlPgogICAgLnN0MSwuc3Q2e2ZpbGw6I2U0NzYxYjtzdHJva2U6I2U0NzYxYjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmR9LnN0NntmaWxsOiNmNjg1MWI7c3Ryb2tlOiNmNjg1MWJ9CiAgPC9zdHlsZT4KICA8cGF0aCBmaWxsPSIjZTI3NjFiIiBzdHJva2U9IiNlMjc2MWIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0ibTI3NC4xIDM1LjUtOTkuNSA3My45TDE5MyA2NS44eiIvPgogIDxwYXRoIGQ9Im00NC40IDM1LjUgOTguNyA3NC42LTE3LjUtNDQuM3ptMTkzLjkgMTcxLjMtMjYuNSA0MC42IDU2LjcgMTUuNiAxNi4zLTU1LjN6bS0yMDQuNC45TDUwLjEgMjYzbDU2LjctMTUuNi0yNi41LTQwLjZ6IiBjbGFzcz0ic3QxIi8+CiAgPHBhdGggZD0ibTEwMy42IDEzOC4yLTE1LjggMjMuOSA1Ni4zIDIuNS0yLTYwLjV6bTExMS4zIDAtMzktMzQuOC0xLjMgNjEuMiA1Ni4yLTIuNXpNMTA2LjggMjQ3LjRsMzMuOC0xNi41LTI5LjItMjIuOHptNzEuMS0xNi41IDMzLjkgMTYuNS00LjctMzkuM3oiIGNsYXNzPSJzdDEiLz4KICA8cGF0aCBmaWxsPSIjZDdjMWIzIiBzdHJva2U9IiNkN2MxYjMiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0ibTIxMS44IDI0Ny40LTMzLjktMTYuNSAyLjcgMjIuMS0uMyA5LjN6bS0xMDUgMCAzMS41IDE0LjktLjItOS4zIDIuNS0yMi4xeiIvPgogIDxwYXRoIGZpbGw9IiMyMzM0NDciIHN0cm9rZT0iIzIzMzQ0NyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJtMTM4LjggMTkzLjUtMjguMi04LjMgMTkuOS05LjF6bTQwLjkgMCA4LjMtMTcuNCAyMCA5LjF6Ii8+CiAgPHBhdGggZmlsbD0iI2NkNjExNiIgc3Ryb2tlPSIjY2Q2MTE2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Im0xMDYuOCAyNDcuNCA0LjgtNDAuNi0zMS4zLjl6TTIwNyAyMDYuOGw0LjggNDAuNiAyNi41LTM5Ljd6bTIzLjgtNDQuNy01Ni4yIDIuNSA1LjIgMjguOSA4LjMtMTcuNCAyMCA5LjF6bS0xMjAuMiAyMy4xIDIwLTkuMSA4LjIgMTcuNCA1LjMtMjguOS01Ni4zLTIuNXoiLz4KICA8cGF0aCBmaWxsPSIjZTQ3NTFmIiBzdHJva2U9IiNlNDc1MWYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0ibTg3LjggMTYyLjEgMjMuNiA0Ni0uOC0yMi45em0xMjAuMyAyMy4xLTEgMjIuOSAyMy43LTQ2em0tNjQtMjAuNi01LjMgMjguOSA2LjYgMzQuMSAxLjUtNDQuOXptMzAuNSAwLTIuNyAxOCAxLjIgNDUgNi43LTM0LjF6Ii8+CiAgPHBhdGggZD0ibTE3OS44IDE5My41LTYuNyAzNC4xIDQuOCAzLjMgMjkuMi0yMi44IDEtMjIuOXptLTY5LjItOC4zLjggMjIuOSAyOS4yIDIyLjggNC44LTMuMy02LjYtMzQuMXoiIGNsYXNzPSJzdDYiLz4KICA8cGF0aCBmaWxsPSIjYzBhZDllIiBzdHJva2U9IiNjMGFkOWUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0ibTE4MC4zIDI2Mi4zLjMtOS4zLTIuNS0yLjJoLTM3LjdsLTIuMyAyLjIuMiA5LjMtMzEuNS0xNC45IDExIDkgMjIuMyAxNS41aDM4LjNsMjIuNC0xNS41IDExLTl6Ii8+CiAgPHBhdGggZmlsbD0iIzE2MTYxNiIgc3Ryb2tlPSIjMTYxNjE2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Im0xNzcuOSAyMzAuOS00LjgtMy4zaC0yNy43bC00LjggMy4zLTIuNSAyMi4xIDIuMy0yLjJoMzcuN2wyLjUgMi4yeiIvPgogIDxwYXRoIGZpbGw9IiM3NjNkMTYiIHN0cm9rZT0iIzc2M2QxNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJtMjc4LjMgMTE0LjIgOC41LTQwLjgtMTIuNy0zNy45LTk2LjIgNzEuNCAzNyAzMS4zIDUyLjMgMTUuMyAxMS42LTEzLjUtNS0zLjYgOC03LjMtNi4yLTQuOCA4LTYuMXpNMzEuOCA3My40bDguNSA0MC44LTUuNCA0IDggNi4xLTYuMSA0LjggOCA3LjMtNSAzLjYgMTEuNSAxMy41IDUyLjMtMTUuMyAzNy0zMS4zLTk2LjItNzEuNHoiLz4KICA8cGF0aCBkPSJtMjY3LjIgMTUzLjUtNTIuMy0xNS4zIDE1LjkgMjMuOS0yMy43IDQ2IDMxLjItLjRoNDYuNXptLTE2My42LTE1LjMtNTIuMyAxNS4zLTE3LjQgNTQuMmg0Ni40bDMxLjEuNC0yMy42LTQ2em03MSAyNi40IDMuMy01Ny43IDE1LjItNDEuMWgtNjcuNWwxNSA0MS4xIDMuNSA1Ny43IDEuMiAxOC4yLjEgNDQuOGgyNy43bC4yLTQ0Ljh6IiBjbGFzcz0ic3Q2Ii8+Cjwvc3ZnPg=='; 3 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/src/predicateResources.ts: -------------------------------------------------------------------------------- 1 | export const predicates = { 2 | 'verification-predicate': { 3 | abi: { 4 | types: [ 5 | { 6 | typeId: 0, 7 | type: 'b256', 8 | components: null, 9 | typeParameters: null 10 | }, 11 | { 12 | typeId: 1, 13 | type: 'bool', 14 | components: null, 15 | typeParameters: null 16 | }, 17 | { 18 | typeId: 2, 19 | type: 'struct EvmAddress', 20 | components: [ 21 | { 22 | name: 'value', 23 | type: 0, 24 | typeArguments: null 25 | } 26 | ], 27 | typeParameters: null 28 | }, 29 | { 30 | typeId: 3, 31 | type: 'u64', 32 | components: null, 33 | typeParameters: null 34 | } 35 | ], 36 | functions: [ 37 | { 38 | inputs: [ 39 | { 40 | name: 'witness_index', 41 | type: 3, 42 | typeArguments: null 43 | } 44 | ], 45 | name: 'main', 46 | output: { 47 | name: '', 48 | type: 1, 49 | typeArguments: null 50 | }, 51 | attributes: null 52 | } 53 | ], 54 | loggedTypes: [], 55 | messagesTypes: [], 56 | configurables: [ 57 | { 58 | name: 'SIGNER', 59 | configurableType: { 60 | name: '', 61 | type: 2, 62 | typeArguments: [] 63 | }, 64 | offset: 1952 65 | } 66 | ] 67 | }, 68 | bytecode: base64ToUint8Array( 69 | 'dAAAA0cAAAAAAAAAAAAHSF38wAEQ//MAGuxQAJEABrhxRAADYUkSAHZIAAJhQRIMdAAAB3JMAAITSSTAWkkgAXZIAAJhQRJKdAAAASQAAABdQQAAXU/wDxBNMwBdU/AQEFFDAF1f8BAQXXMAYUEEAVBHs1ga6QAAGuUQACD4MwBY++ACUPvgBHQAAMIaQ9AAUEe2eHJIAEAoRQSAUEO2eBpEAABySAAgKO0UgFBHsCBySAAgKEU0gFBHsEBySAAgKEV0gFBHtMhySABgKEe0gFBHtMhQS7SockwAIChJRMBQS7SoXU/wCBBNFMBQU7TIUFFAIF1X8AldW/AIKE0VQEFJRYBQR7SoUEuwyHJMACAbTATAEE0kwHJQACAoTXUAckwAIBtMFMAQTSTAclAAIChNdQBQT7HQclAAQChNJQBQS7YQclAAQChJNQBQS7YQUE+wiHJQAEAoTQUAUEOygHJQACAoQRUAPkk0ABpAgAATQQBAdkAAClBDsjhf7ABHUEe2EFBJAAhyTABAKEkUwFBLs/hyRABIKEkEQHQAAAZQQ7GIX+wQMV/sADlQS7P4ckQASChJBEBQQ7WockQASChBJEBQQ7KgckQASChBJEBdQ7B/E0EAQHZAADxQQ7WoUEey6HJIAEgoRQSAXUOwtRNBAAB2QAABNgAAAFBDsuhQQQAIUEe1KHJIAEAoRQSAUEO1KHJEACAbRARAEEUEQFBDtShySAAgG0gUgBBJBIBQQ7FIckwAIChBFMBQRQAgckwAIChFJMBQR7O4ckgAQChFBIBQQ7OYGukQABrlAAAg+DMAWPvgAlD74AR0AABiGkPQAFBHtfBySAAgKEUEgFBDshBf7ABCUEe18FBLtYhyTAAgKEkUwFBHtYhwRAAMUEe1iFBLsQhyTAAgKEkUwFBFAAhyTAAgKEUkwFBLtEByRAAoKEkEQHQAAApQQ7KgUEEAQFBHsGBf7BAMUEkQIHJMAAgoSQTAUEu0QHJAACgoSRQAUEO2UHJEACgoQSRAXUOwiBNBAABcR/BQdkAAARpEAAB2RAABdAAAG1BDtlBQR7MwckgAKChFBIBdQ7DKE0EAAHZAAAE2AAAAUEOzMFBBAAhQR7VockgAIChFBIBQQ7EoXUfwERBFEwBySAAgKEEUgFBHtWhQS7RockwAIChJBMBQQ7SIckwAIChBFMChQSQgdkAAASQAAABcQ/BQJEAAABrwUACRAAAoX/EAAF/xEAFf8SACX/EwA1/zsAQa7FAAkQAAABpDoAAaR5AAGkvgAHJMAEAoRQTAGvUQAJIAAAAa+SAAWfBQKF1DwABdR8ABXUvAAl1PwANd78AEkgAAKEr4AAAa8FAAkQAAOF/xAABf8RABX/EgAl/xMANf8UAEX/FQBV/zsAYa7FAAkQAAeBpDoAAaR5AAGkvgAF1P8BAQTTMAGlAAACZQAAAaUHAAX+1ACF/sAAlf7AAKUFOwQHJUAEAo7QVAGuuwABrlQAAg+DMAWPvgAlD74AR0AAAWUEOwWHJQACAoQTUAUEOwWF1PsAhdU7AKQUE1AHJMACAoRQTAGvUQAJIAAHga+SAAWfBQOF1DwABdR8ABXUvAAl1PwANdU8AEXVfABV3vwAaSAAA4SvgAABrwUACRAABYX/EAAF/xEAFf8SACX/EwA1/xQARf8VAFX/FgBl/xcAdf8YAIX/GQCV/zsAoa7FAAkQAAQBpDoAAaZ5AAGmPgAF1H8AkmRAAAGkRwAHJIACAo7QSAGkuwAF1NIABdUSABXVUgAl1JIANfRTAAX0VAAV9FUAJfRSADXUvwCRNJIAB2SAAsXUmQAhNJIAB2SAAiXUmQAl1P8AkQSSTAXU2QAl1RkABdVZABFVklQHZYAAF0AAAHJkgAABpYcAAVXVAAdlwAAXQAAAEoWUVAGlFgAF9lQAAaWAAAXVPwCRZRZQB2UAAFX2UgAV9lIAIaRAAAJkQAAHQAAA9dUZAAEFFEwBBRRYAQVRWAXFVQAF5RUAAQWWBAdQAADl9lEABdR/AJX2UQAV1H8AlfZRACGkQAACZEAABQQQAgXUfwCSZEAAAaRHAAUEuwIHJMACAoSQTAXUEgAF1NIAFdUSACXUkgA19FAABfRTABX0VAAl9FIANdQ/AJE0EAAHZAACxdQZACE0EAAHZAACJdQZACXUvwCRBBBIBdSZACXU2QAF1RkAEVVQUAdlQAAXQAAAcmQAAAGlRwABVZQAB2WAABdAAAAShVNQAaTVAAX2UwABpUAABdT/AJFk1UwHZMAAVfZQABX2UAAhpAAAAmQAAAdAAAD11NkAAQTTSAEE01QBBRFUBcUUAAXk1AABBVUEB1AAAOX2UQAF1D8AlfZQABXUPwCV9lAAIaQAAAJkAAABr0AACSAABAGvmAAFnwUFhdQ8AAXUfAAV1LwAJdT8ADXVPABF1XwAVdW8AGXV/AB11jwAhdZ8AJXe/ACpIAAFhK+AAARwAAABlFdGhlcmV1bSBTaWduZWQgTWVzc2FnZToKMzIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAgAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdIAAAAAAAAB2gAAAAAAAAHoA==' 70 | ) 71 | } 72 | }; 73 | 74 | function base64ToUint8Array(base64: string) { 75 | var binaryString = atob(base64); 76 | var bytes = new Uint8Array(binaryString.length); 77 | for (var i = 0; i < binaryString.length; i++) { 78 | bytes[i] = binaryString.charCodeAt(i); 79 | } 80 | return bytes; 81 | } 82 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/test/chainConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "chain_name": "local_testnet", 3 | "block_gas_limit": 5000000000, 4 | "initial_state": { 5 | "coins": [ 6 | { 7 | "owner": "0x6e8ec14985922d2b5daf0a9ae68ca63d849e72f09fbc8ab2ddbb88b779a083a0", 8 | "amount": "0xFFFFFFFFFFFFFFFF", 9 | "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" 10 | }, 11 | { 12 | "owner": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", 13 | "amount": "0xFFFFFFFFFFFFFFFF", 14 | "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" 15 | }, 16 | { 17 | "owner": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", 18 | "amount": "0xFFFFFFFFFFFFFFFF", 19 | "asset_id": "0x0101010101010101010101010101010101010101010101010101010101010101" 20 | }, 21 | { 22 | "owner": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", 23 | "amount": "0xFFFFFFFFFFFFFFFF", 24 | "asset_id": "0x0202020202020202020202020202020202020202020202020202020202020202" 25 | }, 26 | { 27 | "owner": "0x09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db", 28 | "amount": "0xFFFFFFFFFFFFFFFF", 29 | "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" 30 | }, 31 | { 32 | "owner": "0x09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db", 33 | "amount": "0xFFFFFFFFFFFFFFFF", 34 | "asset_id": "0x0101010101010101010101010101010101010101010101010101010101010101" 35 | }, 36 | { 37 | "owner": "0x09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db", 38 | "amount": "0xFFFFFFFFFFFFFFFF", 39 | "asset_id": "0x0202020202020202020202020202020202020202020202020202020202020202" 40 | }, 41 | { 42 | "owner": "0x5d99ee966b42cd8fc7bdd1364b389153a9e78b42b7d4a691470674e817888d4e", 43 | "amount": "0xFFFFFFFFFFFFFFFF", 44 | "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" 45 | }, 46 | { 47 | "owner": "0x5d99ee966b42cd8fc7bdd1364b389153a9e78b42b7d4a691470674e817888d4e", 48 | "amount": "0xFFFFFFFFFFFFFFFF", 49 | "asset_id": "0x0101010101010101010101010101010101010101010101010101010101010101" 50 | }, 51 | { 52 | "owner": "0x5d99ee966b42cd8fc7bdd1364b389153a9e78b42b7d4a691470674e817888d4e", 53 | "amount": "0xFFFFFFFFFFFFFFFF", 54 | "asset_id": "0x0202020202020202020202020202020202020202020202020202020202020202" 55 | } 56 | ] 57 | }, 58 | "transaction_parameters": { 59 | "contract_max_size": 16777216, 60 | "max_inputs": 255, 61 | "max_outputs": 255, 62 | "max_witnesses": 255, 63 | "max_gas_per_tx": 500000000, 64 | "max_script_length": 1048576, 65 | "max_script_data_length": 1048576, 66 | "max_static_contracts": 255, 67 | "max_storage_slots": 255, 68 | "max_predicate_length": 1048576, 69 | "max_predicate_data_length": 1048576, 70 | "max_gas_per_predicate": 100000000, 71 | "gas_price_factor": 1000000000, 72 | "gas_per_byte": 4, 73 | "max_message_data_length": 1048576 74 | }, 75 | "consensus": { 76 | "PoA": { 77 | "signing_key": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d" 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /packages/wallet-connector-evm/test/mockProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bytesToHex, 3 | ecsign, 4 | hashPersonalMessage, 5 | hexToBytes, 6 | privateToAddress, 7 | randomBytes, 8 | toRpcSig 9 | } from '@ethereumjs/util'; 10 | import EventEmitter from 'events'; 11 | 12 | type ProviderSetup = { 13 | address: string; 14 | privateKey: string; 15 | networkVersion: number; 16 | debug?: boolean; 17 | manualConfirmEnable?: boolean; 18 | }; 19 | 20 | interface IMockProvider { 21 | request(args: { 22 | method: 'eth_accounts'; 23 | params: string[]; 24 | }): Promise; 25 | request(args: { 26 | method: 'eth_requestAccounts'; 27 | params: string[]; 28 | }): Promise; 29 | 30 | request(args: { method: 'net_version' }): Promise; 31 | request(args: { method: 'eth_chainId'; params: string[] }): Promise; 32 | 33 | request(args: { method: 'personal_sign'; params: string[] }): Promise; 34 | request(args: { method: 'eth_decrypt'; params: string[] }): Promise; 35 | 36 | request(args: { method: string; params?: any[] }): Promise; 37 | } 38 | 39 | // eslint-disable-next-line import/prefer-default-export 40 | export class MockProvider extends EventEmitter implements IMockProvider { 41 | private accounts: { address: string; privateKey: Uint8Array }[] = []; 42 | private connected = false; 43 | 44 | public debug = false; 45 | 46 | public isMetaMask = true; 47 | public manualConfirmEnable = false; 48 | 49 | private acceptEnable?: (value: unknown) => void; 50 | 51 | private rejectEnable?: (value: unknown) => void; 52 | 53 | constructor(numAccounts = 3) { 54 | super(); 55 | for (let i = 0; i < numAccounts; i += 1) { 56 | // const privateKey = randomBytes(32); 57 | const privateKey = hexToBytes( 58 | '0x96dfa8c25bdae93fa0b6460079f8bb18aaec70c8451b5e32251cbc22f0dbf308' 59 | ); 60 | const address = bytesToHex(privateToAddress(privateKey)); 61 | this.accounts.push({ address, privateKey: privateKey }); 62 | } 63 | } 64 | 65 | // eslint-disable-next-line no-console 66 | private log = (...args: (any | null)[]) => 67 | this.debug && console.log('🦄', ...args); 68 | 69 | get selectedAddress(): string { 70 | return this.accounts[0]!.address; 71 | } 72 | 73 | get networkVersion(): number { 74 | return 1; 75 | } 76 | 77 | get chainId(): string { 78 | return `0x${(1).toString(16)}`; 79 | } 80 | 81 | answerEnable(acceptance: boolean) { 82 | if (acceptance) this.acceptEnable!('Accepted'); 83 | else this.rejectEnable!('User rejected'); 84 | } 85 | 86 | getAccounts(): string[] { 87 | return this.accounts.map(({ address }) => address); 88 | } 89 | 90 | async request({ method, params }: any): Promise { 91 | this.log(`request[${method}]`); 92 | 93 | switch (method) { 94 | case 'eth_requestAccounts': 95 | if (this.manualConfirmEnable) { 96 | return new Promise((resolve, reject) => { 97 | this.acceptEnable = resolve; 98 | this.rejectEnable = reject; 99 | }).then(() => this.accounts.map(({ address }) => address)); 100 | } 101 | this.connected = true; 102 | return this.accounts.map(({ address }) => address); 103 | 104 | case 'eth_accounts': 105 | return this.connected ? this.getAccounts() : []; 106 | 107 | case 'net_version': 108 | return this.networkVersion; 109 | 110 | case 'eth_chainId': 111 | return this.chainId; 112 | 113 | case 'personal_sign': { 114 | const [message, address] = params; 115 | const account = this.accounts.find((a) => a.address === address); 116 | if (!account) throw new Error('Account not found'); 117 | 118 | const hash = hashPersonalMessage(hexToBytes(message)); 119 | const signed = ecsign(hash, account.privateKey); 120 | const signedStr = toRpcSig(signed.v, signed.r, signed.s); 121 | 122 | return signedStr; 123 | } 124 | 125 | case 'eth_sendTransaction': { 126 | throw new Error('This service can not send transactions.'); 127 | } 128 | 129 | default: 130 | this.log(`requesting missing method ${method}`); 131 | // eslint-disable-next-line prefer-promise-reject-errors 132 | throw new Error( 133 | `The method ${method} is not implemented by the mock provider.` 134 | ); 135 | } 136 | } 137 | 138 | sendAsync(props: { method: string }, cb: any) { 139 | switch (props.method) { 140 | case 'eth_accounts': 141 | cb(null, { result: [this.getAccounts()] }); 142 | break; 143 | 144 | case 'net_version': 145 | cb(null, { result: this.networkVersion }); 146 | break; 147 | 148 | default: 149 | this.log(`Method '${props.method}' is not supported yet.`); 150 | } 151 | } 152 | 153 | on(props: string, listener: (...args: any[]) => void) { 154 | super.on(props, listener); 155 | this.log('registering event:', props); 156 | return this; 157 | } 158 | 159 | removeAllListeners() { 160 | super.removeAllListeners(); 161 | this.log('removeAllListeners', null); 162 | return this; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/test/testConnector.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from 'fuels'; 2 | 3 | import { EVMWalletConnector } from '../src/index'; 4 | import { EIP1193Provider } from '../src/eip-1193'; 5 | 6 | export class testEVMWalletConnector extends EVMWalletConnector { 7 | constructor(ethProvider: EIP1193Provider, fuelProvider: Provider) { 8 | super(); 9 | this.ethProvider = ethProvider; 10 | this.fuelProvider = fuelProvider; 11 | } 12 | 13 | async getProviders() { 14 | return { fuelProvider: this.fuelProvider!, ethProvider: this.ethProvider! }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/test/walletConnector.test.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | import path from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | 10 | import type { Asset } from '@fuel-wallet/types'; 11 | import { 12 | bn, 13 | Wallet, 14 | BaseAssetId, 15 | Provider, 16 | ScriptTransactionRequest, 17 | WalletUnlocked, 18 | ProviderOptions 19 | } from 'fuels'; 20 | import { launchNodeAndGetWallets } from '@fuel-ts/account/test-utils'; 21 | import { MockProvider } from './mockProvider'; 22 | import { 23 | EVMWalletConnector, 24 | createPredicate, 25 | getPredicateAddress 26 | } from '../src/index'; 27 | import { predicates } from '../src/predicateResources'; 28 | 29 | chai.use(chaiAsPromised); 30 | 31 | const predicate = 'verification-predicate'; 32 | 33 | describe('EVM Wallet Connector', () => { 34 | // Providers used to interact with wallets 35 | let ethProvider: MockProvider; 36 | let fuelProvider: Provider; 37 | 38 | // Our connector bridging MetaMask and predicate accounts 39 | let connector: EVMWalletConnector; 40 | 41 | // Accounts from hardhat used to determine predicate accounts 42 | let ethAccount1: string; 43 | let ethAccount2: string; 44 | 45 | // Predicate accounts associated with the ethereum accounts 46 | let predicateAccount1: string; 47 | let predicateAccount2: string; 48 | 49 | let stopProvider: any; 50 | 51 | const bytecode = predicates[predicate].bytecode; 52 | const abi = predicates[predicate].abi; 53 | 54 | before(async () => { 55 | process.env.GENESIS_SECRET = 56 | '0x6e48a022f9d4ae187bca4e2645abd62198ae294ee484766edbdaadf78160dc68'; 57 | const { stop, provider } = await launchNodeAndGetWallets({ 58 | launchNodeOptions: { 59 | args: ['--chain', `${__dirname}/chainConfig.json`] 60 | } 61 | }); 62 | fuelProvider = provider; 63 | stopProvider = stop; 64 | }); 65 | 66 | after(() => { 67 | stopProvider && stopProvider(); 68 | }); 69 | 70 | beforeEach(async () => { 71 | // Setting the providers once should not cause issues 72 | // Create the Ethereum provider 73 | ethProvider = new MockProvider(); 74 | 75 | const accounts = ethProvider.getAccounts(); 76 | const chainId = await fuelProvider.getChainId(); 77 | 78 | const predicateAccounts = await Promise.all( 79 | accounts.map(async (account) => 80 | getPredicateAddress(account, bytecode, abi) 81 | ) 82 | ); 83 | 84 | ethAccount1 = accounts[0]!; 85 | ethAccount2 = accounts[1]!; 86 | 87 | predicateAccount1 = predicateAccounts[0]!; 88 | predicateAccount2 = predicateAccounts[1]!; 89 | 90 | // Class contains state, reset the state for each test 91 | connector = new EVMWalletConnector({ 92 | ethProvider, 93 | fuelProvider 94 | }); 95 | }); 96 | 97 | afterEach(() => { 98 | ethProvider.removeAllListeners(); 99 | }); 100 | 101 | describe('connect()', () => { 102 | it('connects to ethers signer', async () => { 103 | let connected = await connector.connect(); 104 | 105 | expect(connected).to.be.true; 106 | }); 107 | }); 108 | 109 | describe('isConnected()', () => { 110 | it('false when not connected', async () => { 111 | let connected = await connector.isConnected(); 112 | 113 | expect(connected).to.be.false; 114 | }); 115 | 116 | it('true when connected', async () => { 117 | await connector.connect(); 118 | let connected = await connector.isConnected(); 119 | 120 | expect(connected).to.be.true; 121 | }); 122 | }); 123 | 124 | describe('disconnect()', () => { 125 | it('disconnects from ethers signer', async () => { 126 | await connector.connect(); 127 | 128 | let connected = await connector.disconnect(); 129 | 130 | expect(connected).to.be.true; 131 | }); 132 | }); 133 | 134 | describe('accounts()', () => { 135 | it('returns the predicate accounts associated with the wallet', async () => { 136 | await connector.connect(); 137 | 138 | let predicateAccounts = await connector.accounts(); 139 | let acc1 = predicateAccounts[0]; 140 | let acc2 = predicateAccounts[1]; 141 | 142 | expect(acc1).to.be.equal(predicateAccount1); 143 | expect(acc2).to.be.equal(predicateAccount2); 144 | }); 145 | }); 146 | 147 | describe('currentAccount()', () => { 148 | it('returns the predicate account associated with the current signer account', async () => { 149 | await connector.connect(); 150 | 151 | let account = await connector.currentAccount(); 152 | 153 | expect(account).to.be.equal(predicateAccount1); 154 | }); 155 | 156 | it('throws error when not connected', async () => { 157 | await expect(connector.currentAccount()).to.be.rejectedWith( 158 | 'No connected accounts' 159 | ); 160 | }); 161 | }); 162 | 163 | describe('signMessage()', () => { 164 | it('throws error', async () => { 165 | await expect( 166 | connector.signMessage('address', 'message') 167 | ).to.be.rejectedWith('Not implemented'); 168 | }); 169 | }); 170 | 171 | describe('sendTransaction()', () => { 172 | const ALT_ASSET_ID = 173 | '0x0101010101010101010101010101010101010101010101010101010101010101'; 174 | 175 | it('transfer when signer is not passed in', async () => { 176 | let predicate = await createPredicate( 177 | ethAccount1, 178 | fuelProvider, 179 | bytecode, 180 | abi 181 | ); 182 | 183 | const fundingWallet = new WalletUnlocked('0x01', fuelProvider); 184 | 185 | // Transfer base asset coins to predicate 186 | await fundingWallet 187 | .transfer(predicate.address, 1_000_000, BaseAssetId, { 188 | gasLimit: 10000, 189 | gasPrice: 1 190 | }) 191 | .then((resp) => resp.wait()); 192 | // Transfer alt asset coins to predicate 193 | await fundingWallet 194 | .transfer(predicate.address, 1_000_000, ALT_ASSET_ID, { 195 | gasLimit: 10000, 196 | gasPrice: 1 197 | }) 198 | .then((resp) => resp.wait()); 199 | 200 | // Check predicate balances 201 | const predicateETHBalanceInitial = await predicate.getBalance(); 202 | const predicateAltBalanceInitial = 203 | await predicate.getBalance(ALT_ASSET_ID); 204 | 205 | // Check predicate has the balance required 206 | expect(predicateETHBalanceInitial.gte(1000000)); 207 | expect(predicateAltBalanceInitial.gte(1000000)); 208 | 209 | // Amount to transfer 210 | const amountToTransfer = 10; 211 | 212 | // Create a recipient Wallet 213 | const recipientWallet = Wallet.generate({ provider: fuelProvider }); 214 | const recipientBalanceInitial = 215 | await recipientWallet.getBalance(ALT_ASSET_ID); 216 | 217 | // Create transfer from predicate to recipient 218 | const transactionRequest = new ScriptTransactionRequest({ 219 | gasLimit: 10000, 220 | gasPrice: 1 221 | }); 222 | transactionRequest.addCoinOutput( 223 | recipientWallet.address, 224 | amountToTransfer, 225 | ALT_ASSET_ID 226 | ); 227 | 228 | // fund transaction 229 | const resources = await predicate.getResourcesToSpend([ 230 | { 231 | assetId: BaseAssetId, 232 | amount: bn(1_000_000) 233 | }, 234 | { 235 | assetId: ALT_ASSET_ID, 236 | amount: bn(1_000_000) 237 | } 238 | ]); 239 | transactionRequest.addResources(resources); 240 | 241 | // Connect ETH account 242 | await connector.connect(); 243 | 244 | // TODO: The user accounts mapping must be populated in order to check if the account is valid 245 | // Temporary hack here? 246 | await connector.accounts(); 247 | 248 | // Send transaction using EvmWalletConnector 249 | // TODO: better way of handling un-used address string? 250 | await connector.sendTransaction('', transactionRequest); 251 | 252 | // Check balances are correct 253 | const predicateAltBalanceFinal = await predicate.getBalance(ALT_ASSET_ID); 254 | const recipientBalanceFinal = 255 | await recipientWallet.getBalance(ALT_ASSET_ID); 256 | 257 | expect(predicateAltBalanceFinal.toString()).eq( 258 | predicateAltBalanceInitial.sub(amountToTransfer).toString() 259 | ); 260 | expect(recipientBalanceFinal.toString()).eq( 261 | recipientBalanceInitial.add(amountToTransfer).toString() 262 | ); 263 | }); 264 | 265 | it('transfer when the current signer is passed in', async () => { 266 | let predicate = await createPredicate( 267 | ethAccount1, 268 | fuelProvider, 269 | bytecode, 270 | abi 271 | ); 272 | 273 | const fundingWallet = new WalletUnlocked('0x01', fuelProvider); 274 | 275 | // Transfer base asset coins to predicate 276 | await fundingWallet 277 | .transfer(predicate.address, 1_000_000, BaseAssetId, { 278 | gasLimit: 10000, 279 | gasPrice: 1 280 | }) 281 | .then((resp) => resp.wait()); 282 | // Transfer alt asset coins to predicate 283 | await fundingWallet 284 | .transfer(predicate.address, 1_000_000, ALT_ASSET_ID, { 285 | gasLimit: 10000, 286 | gasPrice: 1 287 | }) 288 | .then((resp) => resp.wait()); 289 | 290 | // Check predicate balances 291 | const predicateETHBalanceInitial = await predicate.getBalance(); 292 | const predicateAltBalanceInitial = 293 | await predicate.getBalance(ALT_ASSET_ID); 294 | 295 | // Check predicate has the balance required 296 | expect(predicateETHBalanceInitial.gte(1000000)); 297 | expect(predicateAltBalanceInitial.gte(1000000)); 298 | 299 | // Amount to transfer 300 | const amountToTransfer = 10; 301 | 302 | // Create a recipient Wallet 303 | const recipientWallet = Wallet.generate({ provider: fuelProvider }); 304 | const recipientBalanceInitial = 305 | await recipientWallet.getBalance(ALT_ASSET_ID); 306 | 307 | // Create transfer from predicate to recipient 308 | const transactionRequest = new ScriptTransactionRequest({ 309 | gasLimit: 10000, 310 | gasPrice: 1 311 | }); 312 | transactionRequest.addCoinOutput( 313 | recipientWallet.address, 314 | amountToTransfer, 315 | ALT_ASSET_ID 316 | ); 317 | 318 | // fund transaction 319 | const resources = await predicate.getResourcesToSpend([ 320 | { 321 | assetId: BaseAssetId, 322 | amount: bn(1_000_000) 323 | }, 324 | { 325 | assetId: ALT_ASSET_ID, 326 | amount: bn(1_000_000) 327 | } 328 | ]); 329 | transactionRequest.addResources(resources); 330 | 331 | // Connect ETH account 332 | await connector.connect(); 333 | 334 | // TODO: The user accounts mapping must be populated in order to check if the account is valid 335 | // Temporary hack here? 336 | await connector.accounts(); 337 | 338 | // Send transaction using EvmWalletConnector 339 | await connector.sendTransaction(predicateAccount1, transactionRequest); 340 | 341 | // Check balances are correct 342 | const predicateAltBalanceFinal = await predicate.getBalance(ALT_ASSET_ID); 343 | const recipientBalanceFinal = 344 | await recipientWallet.getBalance(ALT_ASSET_ID); 345 | 346 | expect(predicateAltBalanceFinal.toString()).eq( 347 | predicateAltBalanceInitial.sub(amountToTransfer).toString() 348 | ); 349 | expect(recipientBalanceFinal.toString()).eq( 350 | recipientBalanceInitial.add(amountToTransfer).toString() 351 | ); 352 | }); 353 | 354 | it('transfer when a different valid signer is passed in', async () => { 355 | let predicate = await createPredicate( 356 | ethAccount2, 357 | fuelProvider, 358 | bytecode, 359 | abi 360 | ); 361 | 362 | const fundingWallet = new WalletUnlocked('0x01', fuelProvider); 363 | 364 | // Transfer base asset coins to predicate 365 | await fundingWallet 366 | .transfer(predicate.address, 1_000_000, BaseAssetId, { 367 | gasLimit: 10000, 368 | gasPrice: 1 369 | }) 370 | .then((resp) => resp.wait()); 371 | // Transfer alt asset coins to predicate 372 | await fundingWallet 373 | .transfer(predicate.address, 1_000_000, ALT_ASSET_ID, { 374 | gasLimit: 10000, 375 | gasPrice: 1 376 | }) 377 | .then((resp) => resp.wait()); 378 | 379 | // Check predicate balances 380 | const predicateETHBalanceInitial = await predicate.getBalance(); 381 | const predicateAltBalanceInitial = 382 | await predicate.getBalance(ALT_ASSET_ID); 383 | 384 | // Check predicate has the balance required 385 | expect(predicateETHBalanceInitial.gte(1000000)); 386 | expect(predicateAltBalanceInitial.gte(1000000)); 387 | 388 | // Amount to transfer 389 | const amountToTransfer = 10; 390 | 391 | // Create a recipient Wallet 392 | const recipientWallet = Wallet.generate({ provider: fuelProvider }); 393 | const recipientBalanceInitial = 394 | await recipientWallet.getBalance(ALT_ASSET_ID); 395 | 396 | // Create transfer from predicate to recipient 397 | const transactionRequest = new ScriptTransactionRequest({ 398 | gasLimit: 10000, 399 | gasPrice: 1 400 | }); 401 | transactionRequest.addCoinOutput( 402 | recipientWallet.address, 403 | amountToTransfer, 404 | ALT_ASSET_ID 405 | ); 406 | 407 | // fund transaction 408 | const resources = await predicate.getResourcesToSpend([ 409 | { 410 | assetId: BaseAssetId, 411 | amount: bn(1_000_000) 412 | }, 413 | { 414 | assetId: ALT_ASSET_ID, 415 | amount: bn(1_000_000) 416 | } 417 | ]); 418 | transactionRequest.addResources(resources); 419 | 420 | // Connect ETH account 421 | await connector.connect(); 422 | 423 | // TODO: The user accounts mapping must be populated in order to check if the account is valid 424 | // Temporary hack here? 425 | await connector.accounts(); 426 | 427 | // Send transaction using EvmWalletConnector 428 | await connector.sendTransaction(predicateAccount2, transactionRequest); 429 | 430 | // Check balances are correct 431 | const predicateAltBalanceFinal = await predicate.getBalance(ALT_ASSET_ID); 432 | const recipientBalanceFinal = 433 | await recipientWallet.getBalance(ALT_ASSET_ID); 434 | 435 | expect(predicateAltBalanceFinal.toString()).eq( 436 | predicateAltBalanceInitial.sub(amountToTransfer).toString() 437 | ); 438 | expect(recipientBalanceFinal.toString()).eq( 439 | recipientBalanceInitial.add(amountToTransfer).toString() 440 | ); 441 | }); 442 | 443 | it('errors when an invalid signer is passed in', async () => { 444 | let predicate = await createPredicate( 445 | ethAccount1, 446 | fuelProvider, 447 | bytecode, 448 | abi 449 | ); 450 | 451 | const fundingWallet = new WalletUnlocked('0x01', fuelProvider); 452 | 453 | // Transfer base asset coins to predicate 454 | await fundingWallet 455 | .transfer(predicate.address, 1_000_000, BaseAssetId, { 456 | gasLimit: 10000, 457 | gasPrice: 1 458 | }) 459 | .then((resp) => resp.wait()); 460 | // Transfer alt asset coins to predicate 461 | await fundingWallet 462 | .transfer(predicate.address, 1_000_000, ALT_ASSET_ID, { 463 | gasLimit: 10000, 464 | gasPrice: 1 465 | }) 466 | .then((resp) => resp.wait()); 467 | 468 | // Check predicate balances 469 | const predicateETHBalanceInitial = await predicate.getBalance(); 470 | const predicateAltBalanceInitial = 471 | await predicate.getBalance(ALT_ASSET_ID); 472 | 473 | // Check predicate has the balance required 474 | expect(predicateETHBalanceInitial.gte(1000000)); 475 | expect(predicateAltBalanceInitial.gte(1000000)); 476 | 477 | // Amount to transfer 478 | const amountToTransfer = 10; 479 | 480 | // Create a recipient Wallet 481 | const recipientWallet = Wallet.generate({ provider: fuelProvider }); 482 | 483 | // Create transfer from predicate to recipient 484 | const transactionRequest = new ScriptTransactionRequest({ 485 | gasLimit: 10000, 486 | gasPrice: 1 487 | }); 488 | transactionRequest.addCoinOutput( 489 | recipientWallet.address, 490 | amountToTransfer, 491 | ALT_ASSET_ID 492 | ); 493 | 494 | // fund transaction 495 | const resources = await predicate.getResourcesToSpend([ 496 | { 497 | assetId: BaseAssetId, 498 | amount: bn(1_000_000) 499 | }, 500 | { 501 | assetId: ALT_ASSET_ID, 502 | amount: bn(1_000_000) 503 | } 504 | ]); 505 | transactionRequest.addResources(resources); 506 | 507 | // Connect ETH account 508 | await connector.connect(); 509 | 510 | // TODO: The user accounts mapping must be populated in order to check if the account is valid 511 | // Temporary hack here? 512 | await connector.accounts(); 513 | 514 | await expect( 515 | connector.sendTransaction( 516 | predicateAccount2.replaceAll('h', 'X'), 517 | transactionRequest 518 | ) 519 | ).to.be.rejectedWith('Invalid account'); 520 | }); 521 | }); 522 | 523 | describe('assets()', () => { 524 | it('returns an empty array', async () => { 525 | expect(await connector.assets()).to.deep.equal([]); 526 | }); 527 | }); 528 | 529 | describe('addAsset()', () => { 530 | it('returns false', async () => { 531 | const asset: Asset = { 532 | name: '', 533 | symbol: '', 534 | icon: '', 535 | networks: [] 536 | }; 537 | expect(await connector.addAsset(asset)).to.be.false; 538 | }); 539 | }); 540 | 541 | describe('addAssets()', () => { 542 | it('returns false', async () => { 543 | expect(await connector.addAssets([])).to.be.false; 544 | }); 545 | }); 546 | 547 | describe('addAbi()', () => { 548 | it('returns false', async () => { 549 | expect(await connector.addAbi({})).to.be.false; 550 | }); 551 | }); 552 | 553 | describe('getAbi()', () => { 554 | it('throws error', async () => { 555 | await expect(connector.getAbi('contractId')).to.be.rejectedWith( 556 | 'Cannot get contractId ABI for a predicate' 557 | ); 558 | }); 559 | }); 560 | 561 | describe('hasAbi()', () => { 562 | it('returns false', async () => { 563 | expect(await connector.hasAbi('contractId')).to.be.false; 564 | }); 565 | }); 566 | 567 | describe('network()', () => { 568 | it('returns the fuel network info', async () => { 569 | let network = await connector.currentNetwork(); 570 | 571 | expect(network.chainId.toString()).to.be.equal( 572 | (await fuelProvider.getNetwork()).chainId.toString() 573 | ); 574 | expect(network.url).to.be.equal(fuelProvider.url); 575 | }); 576 | }); 577 | 578 | describe('networks()', () => { 579 | it('returns an array of fuel network info', async () => { 580 | let networks = await connector.networks(); 581 | let network = networks.pop(); 582 | 583 | expect(network!.chainId.toString()).to.be.equal( 584 | (await connector.fuelProvider!.getNetwork()).chainId.toString() 585 | ); 586 | expect(network!.url).to.be.equal(fuelProvider.url); 587 | }); 588 | }); 589 | 590 | describe('addNetwork()', () => { 591 | it('throws error', async () => { 592 | await expect(connector.addNetwork('')).to.be.rejectedWith( 593 | 'Not implemented' 594 | ); 595 | }); 596 | }); 597 | }); 598 | -------------------------------------------------------------------------------- /packages/wallet-connector-evm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@fuels/ts-config/base.json", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "lib": ["DOM", "ES2022"], 6 | "module": "es2022", 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "noUncheckedIndexedAccess": true, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "skipLibCheck": true, 13 | "outDir": "./dist", 14 | }, 15 | "exclude": [ 16 | "./dist/**/*" 17 | ] 18 | } -------------------------------------------------------------------------------- /packages/wallet-connector-evm/vite.config.js: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | import dts from 'vite-plugin-dts'; 5 | 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | export default defineConfig({ 13 | plugins: [dts({ 14 | include: [resolve(__dirname, 'src/')], 15 | })], 16 | build: { 17 | lib: { 18 | // Could also be a dictionary or array of multiple entry points 19 | entry: resolve(__dirname, 'src/index.ts'), 20 | name: '@fuels/wallet-connector-evm', 21 | // the proper extensions will be added 22 | fileName: 'wallet-connector-evm', 23 | }, 24 | }, 25 | }) -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'examples/*' 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": [".env", ".env.production", ".env.test"], 4 | "globalEnv": ["NODE_ENV"], 5 | "pipeline": { 6 | "build": { 7 | "dependsOn": ["^build"], 8 | "outputs": ["dist/**", "build/**"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "installCommand": "pnpm install", 4 | "buildCommand": "pnpm build", 5 | "outputDirectory": "examples/cra-dapp/build", 6 | "trailingSlash": true 7 | } 8 | --------------------------------------------------------------------------------