├── .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 | You need to enable JavaScript to run this app.
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 | Get some coins
95 |
96 | )}
97 |
increment()}
99 | disabled={isLoadingCall || !hasBalance}
100 | >
101 | {isLoadingCall
102 | ? 'Incrementing...'
103 | : 'Increment the counter on a contract'}
104 |
105 |
handleTransfer()}
107 | disabled={isLoading || !hasBalance}
108 | >
109 | {isLoading
110 | ? 'Transferring...'
111 | : `Transfer ${DEFAULT_AMOUNT.format()} ETH`}
112 |
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 | {
203 | console.log('connect');
204 | connect();
205 | }}
206 | >
207 | {isConnecting ? 'Connecting' : 'Connect'}
208 |
209 | {isConnected && (
210 | disconnect()}>Disconnect
211 | )}
212 | setTheme(lightTheme ? 'dark' : 'light')}>
213 | {lightTheme ? '🌙' : '☀️'}
214 |
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 |
--------------------------------------------------------------------------------