From f1bb61acb61ae96ffd35a7ec9ba2fbb7d727af7e Mon Sep 17 00:00:00 2001 From: Rinat Date: Fri, 13 Dec 2024 21:44:28 +0100 Subject: [PATCH 1/2] feat: challenge 6 extension --- README.md | 252 ++++++++++++++++++ extension/README.md.args.mjs | 244 +++++++++++++++++ extension/package.json | 5 + extension/packages/backend-local/.gitignore | 1 + extension/packages/backend-local/index.ts | 51 ++++ extension/packages/backend-local/package.json | 24 ++ .../packages/backend-local/tsconfig.json | 13 + .../hardhat/contracts/MetaMultiSigWallet.sol | 175 ++++++++++++ .../deploy/00_deploy_meta_multisig_wallet.ts | 41 +++ extension/packages/nextjs/app/create/page.tsx | 218 +++++++++++++++ .../packages/nextjs/app/layout.tsx.args.mjs | 4 + .../_components/TransactionEventItem.tsx | 29 ++ .../nextjs/app/multisig/_components/index.ts | 1 + .../packages/nextjs/app/multisig/page.tsx | 38 +++ extension/packages/nextjs/app/owners/page.tsx | 136 ++++++++++ .../packages/nextjs/app/page.tsx.args.mjs | 47 ++++ .../app/pool/_components/TransactionItem.tsx | 251 +++++++++++++++++ .../nextjs/app/pool/_components/index.ts | 1 + extension/packages/nextjs/app/pool/page.tsx | 115 ++++++++ .../nextjs/components/Header.tsx.args.mjs | 25 ++ .../ScaffoldEthAppWithProviders.tsx.args.mjs | 1 + extension/packages/nextjs/public/hero.png | Bin 0 -> 32166 bytes .../nextjs/public/thumbnail-challenge-6.png | Bin 0 -> 32799 bytes .../nextjs/styles/globals.css.args.mjs | 1 + .../nextjs/tailwind.config.js.args.mjs | 68 +++++ .../scaffold-eth/getMetadata.ts.args.mjs | 2 + 26 files changed, 1743 insertions(+) create mode 100644 README.md create mode 100644 extension/README.md.args.mjs create mode 100644 extension/package.json create mode 100644 extension/packages/backend-local/.gitignore create mode 100644 extension/packages/backend-local/index.ts create mode 100644 extension/packages/backend-local/package.json create mode 100644 extension/packages/backend-local/tsconfig.json create mode 100644 extension/packages/hardhat/contracts/MetaMultiSigWallet.sol create mode 100644 extension/packages/hardhat/deploy/00_deploy_meta_multisig_wallet.ts create mode 100644 extension/packages/nextjs/app/create/page.tsx create mode 100644 extension/packages/nextjs/app/layout.tsx.args.mjs create mode 100644 extension/packages/nextjs/app/multisig/_components/TransactionEventItem.tsx create mode 100644 extension/packages/nextjs/app/multisig/_components/index.ts create mode 100644 extension/packages/nextjs/app/multisig/page.tsx create mode 100644 extension/packages/nextjs/app/owners/page.tsx create mode 100644 extension/packages/nextjs/app/page.tsx.args.mjs create mode 100644 extension/packages/nextjs/app/pool/_components/TransactionItem.tsx create mode 100644 extension/packages/nextjs/app/pool/_components/index.ts create mode 100644 extension/packages/nextjs/app/pool/page.tsx create mode 100644 extension/packages/nextjs/components/Header.tsx.args.mjs create mode 100644 extension/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx.args.mjs create mode 100644 extension/packages/nextjs/public/hero.png create mode 100644 extension/packages/nextjs/public/thumbnail-challenge-6.png create mode 100644 extension/packages/nextjs/styles/globals.css.args.mjs create mode 100644 extension/packages/nextjs/tailwind.config.js.args.mjs create mode 100644 extension/packages/nextjs/utils/scaffold-eth/getMetadata.ts.args.mjs diff --git a/README.md b/README.md new file mode 100644 index 00000000..72a0ea34 --- /dev/null +++ b/README.md @@ -0,0 +1,252 @@ +# 🚩 Challenge 6: πŸ‘› Multisig Wallet + +![readme-6](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/577a8abd-a098-499f-9903-fb6e4c9337e9) + +πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§ A multisig wallet is a smart contract that acts like a wallet, allowing us to secure assets by requiring multiple accounts to "vote" on transactions. Think of it as a treasure chest that can only be opened when all key parties agree. + +πŸ“œ The contract keeps track of all transactions. Each transaction can be confirmed or rejected by the signers (smart contract owners). Only transactions that receive enough confirmations can be "executed" by the signers. + +🌟 The final deliverable is a multisig wallet where you can propose adding and removing signers, transferring funds to other accounts, and updating the required number of signers to execute a transaction. After any of the signers propose a transaction, it's up to the signers to confirm and execute it. Deploy your contracts to a testnet, then build and upload your app to a public web server. + +πŸ’¬ Meet other builders working on this challenge and get help in the [Multisig Build Cohort telegram](https://t.me/+zKllN8OlGuxmYzFh). + +--- + +## πŸ“œ Quest Journal 🧭 + +In this challenge you'll have access to a fully functional Multisig Wallet for inspiration, unlike previous challenges where certain code sections were intentionally left incomplete. + +The objective is to allow builders to create their unique versions while referring to this existing build when encountering difficulties. + +### πŸ₯… Goals: + +- [ ] Can you edit and deploy the contract with a 2/3 multisig with two of your addresses and the buidlguidl multisig as the third signer? (buidlguidl.eth is like your backup recovery.) +- [ ] Can you propose basic transactions with the frontend that sends them to the backend? +- [ ] Can you β€œvote” on the transaction as other signers? +- [ ] Can you execute the transaction and does it do the right thing? +- [ ] Can you add and remove signers with a custom dialog (that just sends you to the create transaction dialog with the correct calldata) + +### βš”οΈ Side Quests: + +- [ ] **Multisig as a service**
+ Create a deploy button with a copy-paste dialog for sharing so anyone can make a multisig at your URL with your frontend. + +- [ ] **Create custom signer roles for your Wallet**
+ You may not want every signer to create new transfers, only allow them to sign existing transactions or a mega-admin role who will be able to veto any transaction. + +- [ ] **Integrate this MultiSig wallet into other Scaffold ETH-2 builds**
+ Find a Scaffold ETH-2 build that could make use of a Multisig wallet and try to integrate it! + +--- + +## πŸ‘‡πŸΌ Quick Break-Down πŸ‘› + +This is a smart contract that acts as an offchain signature-based shared wallet amongst different signers that showcases use of meta-transaction knowledge and ECDSA `recover()`. + +> If you are unfamiliar with these concepts, check out all the [ETH.BUILD videos](https://www.youtube.com/watch?v=CbbcISQvy1E&ab_channel=AustinGriffith) by Austin Griffith, especially the Meta Transactions one! + +❗ [OpenZepplin's ECDSA Library](https://docs.openzeppelin.com/contracts/2.x/api/cryptography#ECDSA) provides an easy way to verify signed messages, in this challenge we'll be using it to verify the signatures of the signers of the multisig wallet. + +At a high-level, the contract core functions are carried out as follows: + +**Offchain: β›“πŸ™…πŸ»β€β™‚οΈ** - Generation of a packed hash (bytes32) for a function call with specific parameters through a public view function . - It is signed by one of the signers associated to the multisig, and added to an array of signatures (`bytes[] memory signatures`) + +**Onchain: β›“πŸ™†πŸ»β€β™‚οΈ** + +- `bytes[] memory signatures` is then passed into `executeTransaction` as well as the necessary info to use `recover()` to obtain the public address that ought to line up with one of the signers of the wallet. + - This method, plus some conditional logic to avoid any duplicate entries from a single signer, is how votes for a specific transaction (hashed tx) are assessed. +- If it's a success, the tx is passed to the `call(){}` function of the deployed MetaMultiSigWallet contract (this contract), thereby passing the `onlySelf` modifier for any possible calls to internal txs such as (`addSigner()`,`removeSigner()`,`transferFunds()`,`updateSignaturesRequired()`). + +**Cool Stuff that is Showcased: 😎** + +- Showcases how the `call(){}` function is an external call that ought to increase the nonce of an external contract, as [they increment differently](https://ethereum.stackexchange.com/questions/764/do-contracts-also-have-a-nonce) from user accounts. +- Normal internal functions, such as changing the signers, and adding or removing signers, are treated as external function calls when `call()` is used with the respective transaction hash. +- Showcases use of an array (see constructor) populating a mapping to store pertinent information within the deployed smart contract storage location within the EVM in a more efficient manner. + +--- + +## Checkpoint 0: πŸ“¦ Environment πŸ“š + +Before you begin, you need to install the following tools: + +- [Node (v18 LTS)](https://nodejs.org/en/download/) +- Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install)) +- [Git](https://git-scm.com/downloads) + +Then download the challenge to your computer and install dependencies by running: + +```sh +git clone https://github.com/scaffold-eth/se-2-challenges.git challenge-6-multisig +cd challenge-6-multisig +git checkout challenge-6-multisig +yarn install +``` + +> in the same terminal, start your local network (a blockchain emulator in your computer): + +```sh +yarn chain +``` + +> in a second terminal window, πŸ›° deploy your contract (locally): + +```sh +cd challenge-6-multisig +yarn deploy +``` + +> in a third terminal window, start your πŸ“± frontend: + +```sh +cd challenge-6-multisig +yarn start +``` + +πŸ“± Open http://localhost:3000 to see the app. + +> In a fourth terminal window: + +❗ This command is only required in your local environment (Hardhat chain). + +```bash +yarn backend-local + +``` + +When deployed to any other chain, it will automatically use our deployed backend ([repo](https://github.com/scaffold-eth/se-2-challenges/tree/multisig-backend)) from `https://backend.multisig.holdings:49832/`. + +> πŸ‘©β€πŸ’» Rerun `yarn deploy --reset` whenever you want to deploy new contracts to the frontend, update your current contracts with changes, or re-deploy it to get a fresh contract address. + +πŸ” Now you are ready to edit your smart contract `MetaMultiSigWallet.sol` in `packages/hardhat/contracts` + +--- + +## Checkpoint 1: πŸ“ Configure Owners πŸ–‹ + +πŸ” The first step for this multisig wallet is to configure the owners, who will be able to propose, sign and execute transactions. + +πŸ—οΈ This is done in the constructor of the contract, where you can pass in an array of addresses that will be the signers of the wallet, and a number of signatures required to execute a transaction. + +> πŸ› οΈ Modify the contract constructor arguments at the deploy script `00_deploy_meta_multisig_wallet.ts` in `packages/hardhat/deploy`. Just set the first signer using your frontend address. + +> πŸ”„ Will need to run `yarn deploy --reset` to deploy a fresh contract with the first signer configured. + +You can set the rest of the signers in the frontend, using the "Owners" tab: + +![multisig-1](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/bc65bf00-93de-4f24-b42b-c78596cd54e0) + +In this tab you can start your transaction proposal to either add or remove owners. + +> πŸ“ Fill the form and click on "Create Tx". + +![multisig-2](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/a74bb0c9-62de-4a12-932a-a5498bf12ecb) + +This will take you to a populated transaction at "Create" page: + +![multisig-3](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/5d4adfb8-66a6-49bb-b72c-3b4062f8e804) + +> Create & sign the new transaction, clicking in the "Create" button: + +![multisig-4](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/f8ef3f85-c543-468f-a008-6c4c8b9cf20a) + +You will see the new transaction in the pool (this is all offchain). + +You won't be able to sign it because on creation it already has one signature (from the frontend account). + +> Click on the ellipsses button [...] to read the details of the transaction. + +![multisig-5](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/25974706-a127-45f4-8a17-6f99b9e97104) + +> ⛽️ Give your account some gas at the faucet and execute the transaction. + +β˜‘ Click on "Exec" to execute it, will be marked as "Completed" on the "Pool" tab, and will appear in the "Multisig" tab with the rest of executed transactions. + +![multisig-6a](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/edf9218c-5b10-49b7-a564-e415c0d2f042) + +![multisig-6b](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/7a7e5324-d5d1-4f10-918c-bfd7c72a52f8) + +## Checkpoint 2: Transfer Funds πŸ’Έ + +> πŸ’° Use the faucet to send your multisig contract some funds. +> You can find the address in the "Multisig" and "Debug Contracts" tabs. + +> Create a transaction in the "Create" tab to send some funds to one of your signers, or to any other address of your choice: + +![multisig-7](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/8b514add-fbe5-4a45-ae68-7659c827a5bf) + +πŸ–‹ This time we will need a second signature (remember we've just updated the number of signatures required to execute a transaction to 2). + +![multisig-8](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/2b7d8501-edfd-47d6-a6d2-937e7bb84caa) + +> Open another browser and access with a different owner of the multisig. Sign the transaction with enough owners: + +![multisig-9](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/ad667a69-499a-4ed4-8a40-52d500c94a5b) + +(You'll notice you don't need ⛽️gas to sign transactions). + +> Execute the transaction to transfer the funds: + +![multisig-10](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/2be26eda-ea09-4a0d-9f0e-d2151cfa26a4) + +--- + +## Checkpoint 3: πŸ’Ύ Deploy your contracts! πŸ›° + +πŸ“‘ Edit the `defaultNetwork` to [your choice of public EVM networks](https://ethereum.org/en/developers/docs/networks/) in `packages/hardhat/hardhat.config.ts` + +πŸ” You will need to generate a **deployer address** using `yarn generate` This creates a mnemonic and saves it locally. + +πŸ‘©β€πŸš€ Use `yarn account` to view your deployer account balances. + +⛽️ You will need to send ETH to your deployer address with your wallet, or get it from a public faucet of your chosen network. + +πŸš€ Run `yarn deploy` to deploy your smart contract to a public network (selected in `hardhat.config.ts`) + +> πŸ’¬ Hint: You can set the `defaultNetwork` in `hardhat.config.ts` to `sepolia` or `optimismSepolia` **OR** you can `yarn deploy --network sepolia` or `yarn deploy --network optimismSepolia`. + +> πŸ’¬ Hint: For faster loading of the Multisig tabs, consider updating the `fromBlock` passed to `useScaffoldEventHistory` (in the different components we're using it) to `blocknumber - 10` at which your contract was deployed. Example: `fromBlock: 3750241n` (where `n` represents its a [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)). To find this blocknumber, search your contract's address on Etherscan and find the `Contract Creation` transaction line. + +--- + +## Checkpoint 4: 🚒 Ship your frontend! 🚁 + +✏️ Edit your frontend config in `packages/nextjs/scaffold.config.ts` to change the `targetNetwork` to `chains.sepolia` (or `chains.optimismSepolia` if you deployed to OP Sepolia) + +πŸ’» View your frontend at http://localhost:3000 and verify you see the correct network. + +πŸ“‘ When you are ready to ship the frontend app... + +πŸ“¦ Run `yarn vercel` to package up your frontend and deploy. + +> Follow the steps to deploy to Vercel. Once you log in (email, github, etc), the default options should work. It'll give you a public URL. + +> If you want to redeploy to the same production URL you can run `yarn vercel --prod`. If you omit the `--prod` flag it will deploy it to a preview/test URL. + +> 🦊 Since we have deployed to a public testnet, you will now need to connect using a wallet you own or use a burner wallet. By default πŸ”₯ `burner wallets` are only available on `hardhat` . You can enable them on every chain by setting `onlyLocalBurnerWallet: false` in your frontend config (`scaffold.config.ts` in `packages/nextjs/`) + +#### Configuration of Third-Party Services for Production-Grade Apps. + +By default, πŸ— Scaffold-ETH 2 provides predefined API keys for popular services such as Alchemy and Etherscan. This allows you to begin developing and testing your applications more easily, avoiding the need to register for these services. +This is great to complete your **SpeedRunEthereum**. + +For production-grade applications, it's recommended to obtain your own API keys (to prevent rate limiting issues). You can configure these at: + +- πŸ”·`ALCHEMY_API_KEY` variable in `packages/hardhat/.env` and `packages/nextjs/.env.local`. You can create API keys from the [Alchemy dashboard](https://dashboard.alchemy.com/). + +- πŸ“ƒ`ETHERSCAN_API_KEY` variable in `packages/hardhat/.env` with your generated API key. You can get your key [here](https://etherscan.io/myapikey). + +> πŸ’¬ Hint: It's recommended to store env's for nextjs in Vercel/system env config for live apps and use .env.local for local testing. + +--- + +## Checkpoint 5: πŸ“œ Contract Verification + +Run the `yarn verify --network your_network` command to verify your contracts on etherscan πŸ›° + +--- + +> πŸ‘©β€β€οΈβ€πŸ‘¨ Share your public url with friends, add signers and send some tasty ETH to a few lucky ones πŸ˜‰!! + +> πŸƒ Head to your next challenge [here](https://speedrunethereum.com). + +> πŸ’¬ Problems, questions, comments on the stack? Post them to the [πŸ— scaffold-eth developers chat](https://t.me/joinchat/F7nCRK3kI93PoCOk) diff --git a/extension/README.md.args.mjs b/extension/README.md.args.mjs new file mode 100644 index 00000000..f80e015e --- /dev/null +++ b/extension/README.md.args.mjs @@ -0,0 +1,244 @@ +export const skipQuickStart = true; + +export const extraContents = ` +# 🚩 Challenge 6: πŸ‘› Multisig Wallet + +![readme-6](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/577a8abd-a098-499f-9903-fb6e4c9337e9) + +πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§ A multisig wallet is a smart contract that acts like a wallet, allowing us to secure assets by requiring multiple accounts to "vote" on transactions. Think of it as a treasure chest that can only be opened when all key parties agree. + +πŸ“œ The contract keeps track of all transactions. Each transaction can be confirmed or rejected by the signers (smart contract owners). Only transactions that receive enough confirmations can be "executed" by the signers. + +🌟 The final deliverable is a multisig wallet where you can propose adding and removing signers, transferring funds to other accounts, and updating the required number of signers to execute a transaction. After any of the signers propose a transaction, it's up to the signers to confirm and execute it. Deploy your contracts to a testnet, then build and upload your app to a public web server. + +πŸ’¬ Meet other builders working on this challenge and get help in the [Multisig Build Cohort telegram](https://t.me/+zKllN8OlGuxmYzFh). + +--- + +## πŸ“œ Quest Journal 🧭 + +In this challenge you'll have access to a fully functional Multisig Wallet for inspiration, unlike previous challenges where certain code sections were intentionally left incomplete. + +The objective is to allow builders to create their unique versions while referring to this existing build when encountering difficulties. + +### πŸ₯… Goals: + +- [ ] Can you edit and deploy the contract with a 2/3 multisig with two of your addresses and the buidlguidl multisig as the third signer? (buidlguidl.eth is like your backup recovery.) +- [ ] Can you propose basic transactions with the frontend that sends them to the backend? +- [ ] Can you β€œvote” on the transaction as other signers? +- [ ] Can you execute the transaction and does it do the right thing? +- [ ] Can you add and remove signers with a custom dialog (that just sends you to the create transaction dialog with the correct calldata) + +### βš”οΈ Side Quests: + +- [ ] **Multisig as a service**
+ Create a deploy button with a copy-paste dialog for sharing so anyone can make a multisig at your URL with your frontend. + +- [ ] **Create custom signer roles for your Wallet**
+ You may not want every signer to create new transfers, only allow them to sign existing transactions or a mega-admin role who will be able to veto any transaction. + +- [ ] **Integrate this MultiSig wallet into other Scaffold ETH-2 builds**
+ Find a Scaffold ETH-2 build that could make use of a Multisig wallet and try to integrate it! + +--- + +## πŸ‘‡πŸΌ Quick Break-Down πŸ‘› + +This is a smart contract that acts as an offchain signature-based shared wallet amongst different signers that showcases use of meta-transaction knowledge and ECDSA \`recover()\`. + +> If you are unfamiliar with these concepts, check out all the [ETH.BUILD videos](https://www.youtube.com/watch?v=CbbcISQvy1E&ab_channel=AustinGriffith) by Austin Griffith, especially the Meta Transactions one! + +❗ [OpenZepplin's ECDSA Library](https://docs.openzeppelin.com/contracts/2.x/api/cryptography#ECDSA) provides an easy way to verify signed messages, in this challenge we'll be using it to verify the signatures of the signers of the multisig wallet. + +At a high-level, the contract core functions are carried out as follows: + +**Offchain: β›“πŸ™…πŸ»β€β™‚οΈ** - Generation of a packed hash (bytes32) for a function call with specific parameters through a public view function . - It is signed by one of the signers associated to the multisig, and added to an array of signatures (\`bytes[] memory signatures\`) + +**Onchain: β›“πŸ™†πŸ»β€β™‚οΈ** + +- \`bytes[] memory signatures\` is then passed into \`executeTransaction\` as well as the necessary info to use \`recover()\` to obtain the public address that ought to line up with one of the signers of the wallet. + - This method, plus some conditional logic to avoid any duplicate entries from a single signer, is how votes for a specific transaction (hashed tx) are assessed. +- If it's a success, the tx is passed to the \`call(){}\` function of the deployed MetaMultiSigWallet contract (this contract), thereby passing the \`onlySelf\` modifier for any possible calls to internal txs such as (\`addSigner()\`,\`removeSigner()\`,\`transferFunds()\`,\`updateSignaturesRequired()\`). + +**Cool Stuff that is Showcased: 😎** + +- Showcases how the \`call(){}\` function is an external call that ought to increase the nonce of an external contract, as [they increment differently](https://ethereum.stackexchange.com/questions/764/do-contracts-also-have-a-nonce) from user accounts. +- Normal internal functions, such as changing the signers, and adding or removing signers, are treated as external function calls when \`call()\` is used with the respective transaction hash. +- Showcases use of an array (see constructor) populating a mapping to store pertinent information within the deployed smart contract storage location within the EVM in a more efficient manner. + +--- + +## Checkpoint 0: πŸ“¦ Environment πŸ“š + +\`\`\`sh +npx create-eth@latest -e sre-challenge-6 challenge-6-multisig +cd challenge-6-multisig +\`\`\` + +> in the same terminal, start your local network (a blockchain emulator in your computer): + +\`\`\`sh +yarn chain +\`\`\` + +> in a second terminal window, πŸ›° deploy your contract (locally): + +\`\`\`sh +yarn deploy +\`\`\` + +> in a third terminal window, start your πŸ“± frontend: + +\`\`\`sh +yarn start +\`\`\` + +πŸ“± Open http://localhost:3000 to see the app. + +> In a fourth terminal window: + +❗ This command is only required in your local environment (Hardhat chain). + +\`\`\`bash +yarn backend-local + +\`\`\` + +When deployed to any other chain, it will automatically use our deployed backend ([repo](https://github.com/scaffold-eth/se-2-challenges/tree/multisig-backend)) from \`https://backend.multisig.holdings:49832/\`. + +> πŸ‘©β€πŸ’» Rerun \`yarn deploy --reset\` whenever you want to deploy new contracts to the frontend, update your current contracts with changes, or re-deploy it to get a fresh contract address. + +πŸ” Now you are ready to edit your smart contract \`MetaMultiSigWallet.sol\` in \`packages/hardhat/contracts\` + +--- + +## Checkpoint 1: πŸ“ Configure Owners πŸ–‹ + +πŸ” The first step for this multisig wallet is to configure the owners, who will be able to propose, sign and execute transactions. + +πŸ—οΈ This is done in the constructor of the contract, where you can pass in an array of addresses that will be the signers of the wallet, and a number of signatures required to execute a transaction. + +> πŸ› οΈ Modify the contract constructor arguments at the deploy script \`00_deploy_meta_multisig_wallet.ts\` in \`packages/hardhat/deploy\`. Just set the first signer using your frontend address. + +> πŸ”„ Will need to run \`yarn deploy --reset\` to deploy a fresh contract with the first signer configured. + +You can set the rest of the signers in the frontend, using the "Owners" tab: + +![multisig-1](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/bc65bf00-93de-4f24-b42b-c78596cd54e0) + +In this tab you can start your transaction proposal to either add or remove owners. + +> πŸ“ Fill the form and click on "Create Tx". + +![multisig-2](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/a74bb0c9-62de-4a12-932a-a5498bf12ecb) + +This will take you to a populated transaction at "Create" page: + +![multisig-3](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/5d4adfb8-66a6-49bb-b72c-3b4062f8e804) + +> Create & sign the new transaction, clicking in the "Create" button: + +![multisig-4](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/f8ef3f85-c543-468f-a008-6c4c8b9cf20a) + +You will see the new transaction in the pool (this is all offchain). + +You won't be able to sign it because on creation it already has one signature (from the frontend account). + +> Click on the ellipsses button [...] to read the details of the transaction. + +![multisig-5](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/25974706-a127-45f4-8a17-6f99b9e97104) + +> ⛽️ Give your account some gas at the faucet and execute the transaction. + +β˜‘ Click on "Exec" to execute it, will be marked as "Completed" on the "Pool" tab, and will appear in the "Multisig" tab with the rest of executed transactions. + +![multisig-6a](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/edf9218c-5b10-49b7-a564-e415c0d2f042) + +![multisig-6b](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/7a7e5324-d5d1-4f10-918c-bfd7c72a52f8) + +## Checkpoint 2: Transfer Funds πŸ’Έ + +> πŸ’° Use the faucet to send your multisig contract some funds. +> You can find the address in the "Multisig" and "Debug Contracts" tabs. + +> Create a transaction in the "Create" tab to send some funds to one of your signers, or to any other address of your choice: + +![multisig-7](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/8b514add-fbe5-4a45-ae68-7659c827a5bf) + +πŸ–‹ This time we will need a second signature (remember we've just updated the number of signatures required to execute a transaction to 2). + +![multisig-8](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/2b7d8501-edfd-47d6-a6d2-937e7bb84caa) + +> Open another browser and access with a different owner of the multisig. Sign the transaction with enough owners: + +![multisig-9](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/ad667a69-499a-4ed4-8a40-52d500c94a5b) + +(You'll notice you don't need ⛽️gas to sign transactions). + +> Execute the transaction to transfer the funds: + +![multisig-10](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/2be26eda-ea09-4a0d-9f0e-d2151cfa26a4) + +--- + +## Checkpoint 3: πŸ’Ύ Deploy your contracts! πŸ›° + +πŸ“‘ Edit the \`defaultNetwork\` to [your choice of public EVM networks](https://ethereum.org/en/developers/docs/networks/) in \`packages/hardhat/hardhat.config.ts\` + +πŸ” You will need to generate a **deployer address** using \`yarn generate\` This creates a mnemonic and saves it locally. + +πŸ‘©β€πŸš€ Use \`yarn account\` to view your deployer account balances. + +⛽️ You will need to send ETH to your deployer address with your wallet, or get it from a public faucet of your chosen network. + +πŸš€ Run \`yarn deploy\` to deploy your smart contract to a public network (selected in \`hardhat.config.ts\`) + +> πŸ’¬ Hint: You can set the \`defaultNetwork\` in \`hardhat.config.ts\` to \`sepolia\` or \`optimismSepolia\` **OR** you can \`yarn deploy --network sepolia\` or \`yarn deploy --network optimismSepolia\`. + +> πŸ’¬ Hint: For faster loading of the Multisig tabs, consider updating the \`fromBlock\` passed to \`useScaffoldEventHistory\` (in the different components we're using it) to \`blocknumber - 10\` at which your contract was deployed. Example: \`fromBlock: 3750241n\` (where \`n\` represents its a [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)). To find this blocknumber, search your contract's address on Etherscan and find the \`Contract Creation\` transaction line. + +--- + +## Checkpoint 4: 🚒 Ship your frontend! 🚁 + +✏️ Edit your frontend config in \`packages/nextjs/scaffold.config.ts\` to change the \`targetNetwork\` to \`chains.sepolia\` (or \`chains.optimismSepolia\` if you deployed to OP Sepolia) + +πŸ’» View your frontend at http://localhost:3000 and verify you see the correct network. + +πŸ“‘ When you are ready to ship the frontend app... + +πŸ“¦ Run \`yarn vercel\` to package up your frontend and deploy. + +> Follow the steps to deploy to Vercel. Once you log in (email, github, etc), the default options should work. It'll give you a public URL. + +> If you want to redeploy to the same production URL you can run \`yarn vercel --prod\`. If you omit the \`--prod\` flag it will deploy it to a preview/test URL. + +> 🦊 Since we have deployed to a public testnet, you will now need to connect using a wallet you own or use a burner wallet. By default πŸ”₯ \`burner wallets\` are only available on \`hardhat\` . You can enable them on every chain by setting \`onlyLocalBurnerWallet: false\` in your frontend config (\`scaffold.config.ts\` in \`packages/nextjs/\`) + +#### Configuration of Third-Party Services for Production-Grade Apps. + +By default, πŸ— Scaffold-ETH 2 provides predefined API keys for popular services such as Alchemy and Etherscan. This allows you to begin developing and testing your applications more easily, avoiding the need to register for these services. +This is great to complete your **SpeedRunEthereum**. + +For production-grade applications, it's recommended to obtain your own API keys (to prevent rate limiting issues). You can configure these at: + +- πŸ”·\`ALCHEMY_API_KEY\` variable in \`packages/hardhat/.env\` and \`packages/nextjs/.env.local\`. You can create API keys from the [Alchemy dashboard](https://dashboard.alchemy.com/). + +- πŸ“ƒ\`ETHERSCAN_API_KEY\` variable in \`packages/hardhat/.env\` with your generated API key. You can get your key [here](https://etherscan.io/myapikey). + +> πŸ’¬ Hint: It's recommended to store env's for nextjs in Vercel/system env config for live apps and use .env.local for local testing. + +--- + +## Checkpoint 5: πŸ“œ Contract Verification + +Run the \`yarn verify --network your_network\` command to verify your contracts on etherscan πŸ›° + +--- + +> πŸ‘©β€β€οΈβ€πŸ‘¨ Share your public url with friends, add signers and send some tasty ETH to a few lucky ones πŸ˜‰!! + +> πŸƒ Head to your next challenge [here](https://speedrunethereum.com). + +> πŸ’¬ Problems, questions, comments on the stack? Post them to the [πŸ— scaffold-eth developers chat](https://t.me/joinchat/F7nCRK3kI93PoCOk) +`; diff --git a/extension/package.json b/extension/package.json new file mode 100644 index 00000000..c510079f --- /dev/null +++ b/extension/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "backend-local": "yarn workspace backend-local dev" + } +} diff --git a/extension/packages/backend-local/.gitignore b/extension/packages/backend-local/.gitignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/extension/packages/backend-local/.gitignore @@ -0,0 +1 @@ +dist diff --git a/extension/packages/backend-local/index.ts b/extension/packages/backend-local/index.ts new file mode 100644 index 00000000..8e4d39d3 --- /dev/null +++ b/extension/packages/backend-local/index.ts @@ -0,0 +1,51 @@ +import express from "express"; +import cors from "cors"; +import bodyParser from "body-parser"; +import dotenv from "dotenv"; +import { AddressInfo } from "net"; + +dotenv.config(); + +type Transaction = { + // [TransactionData type from next app]. Didn't add it since not in use + // and it should be updated when next type changes + [key: string]: any; +}; + +const app = express(); + +const transactions: { [key: string]: Transaction } = {}; + +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +app.get("/:key", async (req, res) => { + const { key } = req.params; + console.log("Get /", key); + res.status(200).send(transactions[key] || {}); +}); + +app.post("/", async (req, res) => { + console.log("Post /", req.body); + res.send(req.body); + const key = `${req.body.address}_${req.body.chainId}`; + console.log("key:", key); + if (!transactions[key]) { + transactions[key] = {}; + } + transactions[key][req.body.hash] = req.body; + console.log("transactions", transactions); +}); + +const PORT = process.env.PORT || 49832; +const server = app + .listen(PORT, () => { + console.log( + "HTTP Listening on port:", + (server.address() as AddressInfo).port + ); + }) + .on("error", (error) => { + console.error("Error occurred starting the server: ", error); + }); diff --git a/extension/packages/backend-local/package.json b/extension/packages/backend-local/package.json new file mode 100644 index 00000000..2bb74052 --- /dev/null +++ b/extension/packages/backend-local/package.json @@ -0,0 +1,24 @@ +{ + "name": "backend-local", + "version": "0.0.1", + "type": "module", + "scripts": { + "build": "tsc", + "dev": "nodemon --exec node --loader ts-node/esm ./index.ts", + "start": "node dist/index.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/dotenv": "^8.2.0", + "@types/express": "^4.17.21", + "@types/node": "^20.10.6", + "nodemon": "^3.0.2", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/extension/packages/backend-local/tsconfig.json b/extension/packages/backend-local/tsconfig.json new file mode 100644 index 00000000..f4423dfa --- /dev/null +++ b/extension/packages/backend-local/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "module": "NodeNext", + "outDir": "./dist" + } +} diff --git a/extension/packages/hardhat/contracts/MetaMultiSigWallet.sol b/extension/packages/hardhat/contracts/MetaMultiSigWallet.sol new file mode 100644 index 00000000..7c4cd159 --- /dev/null +++ b/extension/packages/hardhat/contracts/MetaMultiSigWallet.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT + +// Off-chain signature gathering multisig that streams funds - @austingriffith +// +// started from πŸ— scaffold-eth - meta-multi-sig-wallet example https://github.com/austintgriffith/scaffold-eth/tree/meta-multi-sig +// (off-chain signature based multi-sig) +// added a very simple streaming mechanism where `onlySelf` can open a withdraw-based stream +// + +pragma solidity >=0.8.0 <0.9.0; +// Not needed to be explicitly imported in Solidity 0.8.x +// pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +contract MetaMultiSigWallet { + using MessageHashUtils for bytes32; + + event Deposit(address indexed sender, uint amount, uint balance); + event ExecuteTransaction( + address indexed owner, + address payable to, + uint256 value, + bytes data, + uint256 nonce, + bytes32 hash, + bytes result + ); + event Owner(address indexed owner, bool added); + mapping(address => bool) public isOwner; + uint public signaturesRequired; + uint public nonce; + uint public chainId; + + constructor(uint256 _chainId, address[] memory _owners, uint _signaturesRequired) { + require(_signaturesRequired > 0, "constructor: must be non-zero sigs required"); + signaturesRequired = _signaturesRequired; + for (uint i = 0; i < _owners.length; i++) { + address owner = _owners[i]; + require(owner != address(0), "constructor: zero address"); + require(!isOwner[owner], "constructor: owner not unique"); + isOwner[owner] = true; + emit Owner(owner, isOwner[owner]); + } + chainId = _chainId; + } + + modifier onlySelf() { + require(msg.sender == address(this), "Not Self"); + _; + } + + function addSigner(address newSigner, uint256 newSignaturesRequired) public onlySelf { + require(newSigner != address(0), "addSigner: zero address"); + require(!isOwner[newSigner], "addSigner: owner not unique"); + require(newSignaturesRequired > 0, "addSigner: must be non-zero sigs required"); + isOwner[newSigner] = true; + signaturesRequired = newSignaturesRequired; + emit Owner(newSigner, isOwner[newSigner]); + } + + function removeSigner(address oldSigner, uint256 newSignaturesRequired) public onlySelf { + require(isOwner[oldSigner], "removeSigner: not owner"); + require(newSignaturesRequired > 0, "removeSigner: must be non-zero sigs required"); + isOwner[oldSigner] = false; + signaturesRequired = newSignaturesRequired; + emit Owner(oldSigner, isOwner[oldSigner]); + } + + function updateSignaturesRequired(uint256 newSignaturesRequired) public onlySelf { + require(newSignaturesRequired > 0, "updateSignaturesRequired: must be non-zero sigs required"); + signaturesRequired = newSignaturesRequired; + } + + function getTransactionHash( + uint256 _nonce, + address to, + uint256 value, + bytes memory data + ) public view returns (bytes32) { + return keccak256(abi.encodePacked(address(this), chainId, _nonce, to, value, data)); + } + + function executeTransaction( + address payable to, + uint256 value, + bytes memory data, + bytes[] memory signatures + ) public returns (bytes memory) { + require(isOwner[msg.sender], "executeTransaction: only owners can execute"); + bytes32 _hash = getTransactionHash(nonce, to, value, data); + nonce++; + uint256 validSignatures; + address duplicateGuard; + for (uint i = 0; i < signatures.length; i++) { + address recovered = recover(_hash, signatures[i]); + require(recovered > duplicateGuard, "executeTransaction: duplicate or unordered signatures"); + duplicateGuard = recovered; + if (isOwner[recovered]) { + validSignatures++; + } + } + + require(validSignatures >= signaturesRequired, "executeTransaction: not enough valid signatures"); + + (bool success, bytes memory result) = to.call{ value: value }(data); + require(success, "executeTransaction: tx failed"); + + emit ExecuteTransaction(msg.sender, to, value, data, nonce - 1, _hash, result); + return result; + } + + function recover(bytes32 _hash, bytes memory _signature) public pure returns (address) { + return ECDSA.recover(MessageHashUtils.toEthSignedMessageHash(_hash), _signature); + } + + receive() external payable { + emit Deposit(msg.sender, msg.value, address(this).balance); + } + + // + // new streaming stuff + // + + event OpenStream(address indexed to, uint256 amount, uint256 frequency); + event CloseStream(address indexed to); + event Withdraw(address indexed to, uint256 amount, string reason); + + struct Stream { + uint256 amount; + uint256 frequency; + uint256 last; + } + mapping(address => Stream) public streams; + + function streamWithdraw(uint256 amount, string memory reason) public { + require(streams[msg.sender].amount > 0, "withdraw: no open stream"); + _streamWithdraw(payable(msg.sender), amount, reason); + } + + function _streamWithdraw(address payable to, uint256 amount, string memory reason) private { + uint256 totalAmountCanWithdraw = streamBalance(to); + require(totalAmountCanWithdraw >= amount, "withdraw: not enough"); + streams[to].last = + streams[to].last + + (((block.timestamp - streams[to].last) * amount) / totalAmountCanWithdraw); + emit Withdraw(to, amount, reason); + (bool success, ) = to.call{ value: amount }(""); + require(success, "withdraw: failed to send"); + } + + function streamBalance(address to) public view returns (uint256) { + return (streams[to].amount * (block.timestamp - streams[to].last)) / streams[to].frequency; + } + + function openStream(address to, uint256 amount, uint256 frequency) public onlySelf { + require(streams[to].amount == 0, "openStream: stream already open"); + require(amount > 0, "openStream: no amount"); + require(frequency > 0, "openStream: no frequency"); + + streams[to].amount = amount; + streams[to].frequency = frequency; + streams[to].last = block.timestamp; + + emit OpenStream(to, amount, frequency); + } + + function closeStream(address payable to) public onlySelf { + require(streams[to].amount > 0, "closeStream: stream already closed"); + _streamWithdraw(to, streams[to].amount, "stream closed"); + delete streams[to]; + emit CloseStream(to); + } +} diff --git a/extension/packages/hardhat/deploy/00_deploy_meta_multisig_wallet.ts b/extension/packages/hardhat/deploy/00_deploy_meta_multisig_wallet.ts new file mode 100644 index 00000000..ff3510ac --- /dev/null +++ b/extension/packages/hardhat/deploy/00_deploy_meta_multisig_wallet.ts @@ -0,0 +1,41 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; + +/** + * Deploys a "MetaMultiSigWallet" contract + * + * @param hre HardhatRuntimeEnvironment object. + */ +const deployMetaMultiSigWallet: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + /* + On localhost, the deployer account is the one that comes with Hardhat, which is already funded. + + When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account + should have sufficient balance to pay for the gas fees for contract creation. + + You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY + with a random private key in the .env file (then used on hardhat.config.ts) + You can run the `yarn account` command to check your balance in every network. + */ + const { deployer } = await hre.getNamedAccounts(); + const { deploy } = hre.deployments; + + await deploy("MetaMultiSigWallet", { + from: deployer, + // Contract constructor arguments + args: [31337, ["**YOUR FRONTEND ADDRESS**"], 1], + log: true, + // autoMine: can be passed to the deploy function to make the deployment process faster on local networks by + // automatically mining the contract deployment transaction. There is no effect on live networks. + autoMine: true, + }); + + // Get the deployed contract + // const metaMultiSigWallet = await hre.ethers.getContract("MetaMultiSigWallet", deployer); +}; + +export default deployMetaMultiSigWallet; + +// Tags are useful if you have multiple deploy files and only want to run one of them. +// e.g. yarn deploy --tags MetaMultiSigWallet +deployMetaMultiSigWallet.tags = ["MetaMultiSigWallet"]; diff --git a/extension/packages/nextjs/app/create/page.tsx b/extension/packages/nextjs/app/create/page.tsx new file mode 100644 index 00000000..f9271409 --- /dev/null +++ b/extension/packages/nextjs/app/create/page.tsx @@ -0,0 +1,218 @@ +"use client"; + +import { type FC, useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { DEFAULT_TX_DATA, METHODS, Method, PredefinedTxData } from "../owners/page"; +import { useIsMounted, useLocalStorage } from "usehooks-ts"; +import { Address, parseEther } from "viem"; +import { useChainId, useWalletClient } from "wagmi"; +import * as chains from "wagmi/chains"; +import { AddressInput, EtherInput, InputBase } from "~~/components/scaffold-eth"; +import { useDeployedContractInfo, useScaffoldContract, useScaffoldReadContract } from "~~/hooks/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { notification } from "~~/utils/scaffold-eth"; + +export type TransactionData = { + chainId: number; + address: Address; + nonce: bigint; + to: string; + amount: string; + data: `0x${string}`; + hash: `0x${string}`; + signatures: `0x${string}`[]; + signers: Address[]; + validSignatures?: { signer: Address; signature: Address }[]; + requiredApprovals: bigint; +}; + +export const getPoolServerUrl = (id: number) => + id === chains.hardhat.id ? "http://localhost:49832/" : "https://backend.multisig.holdings:49832/"; + +const CreatePage: FC = () => { + const isMounted = useIsMounted(); + const router = useRouter(); + const chainId = useChainId(); + const { data: walletClient } = useWalletClient(); + const { targetNetwork } = useTargetNetwork(); + + const poolServerUrl = getPoolServerUrl(targetNetwork.id); + + const [ethValue, setEthValue] = useState(""); + const { data: contractInfo } = useDeployedContractInfo("MetaMultiSigWallet"); + + const [predefinedTxData, setPredefinedTxData] = useLocalStorage("predefined-tx-data", { + methodName: "transferFunds", + signer: "", + newSignaturesNumber: "", + amount: "0", + }); + + const { data: nonce } = useScaffoldReadContract({ + contractName: "MetaMultiSigWallet", + functionName: "nonce", + }); + + const { data: signaturesRequired } = useScaffoldReadContract({ + contractName: "MetaMultiSigWallet", + functionName: "signaturesRequired", + }); + + const txTo = predefinedTxData.methodName === "transferFunds" ? predefinedTxData.signer : contractInfo?.address; + + const { data: metaMultiSigWallet } = useScaffoldContract({ + contractName: "MetaMultiSigWallet", + }); + + const handleCreate = async () => { + try { + if (!walletClient) { + console.log("No wallet client!"); + return; + } + + const newHash = (await metaMultiSigWallet?.read.getTransactionHash([ + nonce as bigint, + String(txTo), + BigInt(predefinedTxData.amount as string), + predefinedTxData.callData as `0x${string}`, + ])) as `0x${string}`; + + const signature = await walletClient.signMessage({ + message: { raw: newHash }, + }); + + const recover = (await metaMultiSigWallet?.read.recover([newHash, signature])) as Address; + + const isOwner = await metaMultiSigWallet?.read.isOwner([recover]); + + if (isOwner) { + if (!contractInfo?.address || !predefinedTxData.amount || !txTo) { + return; + } + + const txData: TransactionData = { + chainId: chainId, + address: contractInfo.address, + nonce: nonce || 0n, + to: txTo, + amount: predefinedTxData.amount, + data: predefinedTxData.callData as `0x${string}`, + hash: newHash, + signatures: [signature], + signers: [recover], + requiredApprovals: signaturesRequired || 0n, + }; + + await fetch(poolServerUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify( + txData, + // stringifying bigint + (key, value) => (typeof value === "bigint" ? value.toString() : value), + ), + }); + + setPredefinedTxData(DEFAULT_TX_DATA); + + setTimeout(() => { + router.push("/pool"); + }, 777); + } else { + notification.info("Only owners can propose transactions"); + } + } catch (e) { + notification.error("Error while proposing transaction"); + console.log(e); + } + }; + + useEffect(() => { + if (predefinedTxData && !predefinedTxData.callData && predefinedTxData.methodName !== "transferFunds") { + setPredefinedTxData({ + ...predefinedTxData, + methodName: "transferFunds", + callData: "", + }); + } + }, [predefinedTxData, setPredefinedTxData]); + + return isMounted() ? ( +
+
+
+
+ + { + null; + }} + /> +
+ +
+
+ + +
+ + setPredefinedTxData({ ...predefinedTxData, signer: signer })} + /> + + {predefinedTxData.methodName === "transferFunds" && ( + { + setPredefinedTxData({ ...predefinedTxData, amount: String(parseEther(val)) }); + setEthValue(val); + }} + /> + )} + + { + null; + }} + disabled + /> + + +
+
+
+
+ ) : null; +}; + +export default CreatePage; diff --git a/extension/packages/nextjs/app/layout.tsx.args.mjs b/extension/packages/nextjs/app/layout.tsx.args.mjs new file mode 100644 index 00000000..03a3e495 --- /dev/null +++ b/extension/packages/nextjs/app/layout.tsx.args.mjs @@ -0,0 +1,4 @@ +export const metadata = { + title: "Challenge #6 | SpeedRunEthereum", + description: "Built with πŸ— Scaffold-ETH 2", +}; diff --git a/extension/packages/nextjs/app/multisig/_components/TransactionEventItem.tsx b/extension/packages/nextjs/app/multisig/_components/TransactionEventItem.tsx new file mode 100644 index 00000000..f0f6109d --- /dev/null +++ b/extension/packages/nextjs/app/multisig/_components/TransactionEventItem.tsx @@ -0,0 +1,29 @@ +import { type FC } from "react"; +import { Address as AddressType, formatEther } from "viem"; +import { Address, BlockieAvatar } from "~~/components/scaffold-eth"; + +type TransactionEventItemProps = { + hash: `0x${string}`; + nonce: bigint; + owner: AddressType; + to: AddressType; + value: bigint; +}; + +export const TransactionEventItem: FC = ({ hash, nonce, owner, to, value }) => { + return ( +
+
# {String(nonce)}
+
+ Tx {hash.slice(0, 7)} +
+
+ To
+
+
{formatEther(value)} Ξ
+
+ Executed by
+
+
+ ); +}; diff --git a/extension/packages/nextjs/app/multisig/_components/index.ts b/extension/packages/nextjs/app/multisig/_components/index.ts new file mode 100644 index 00000000..78c98b0d --- /dev/null +++ b/extension/packages/nextjs/app/multisig/_components/index.ts @@ -0,0 +1 @@ +export * from "./TransactionEventItem"; diff --git a/extension/packages/nextjs/app/multisig/page.tsx b/extension/packages/nextjs/app/multisig/page.tsx new file mode 100644 index 00000000..7ba896c5 --- /dev/null +++ b/extension/packages/nextjs/app/multisig/page.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { type FC } from "react"; +import { TransactionEventItem } from "./_components"; +import { QRCodeSVG } from "qrcode.react"; +import { Address, Balance } from "~~/components/scaffold-eth"; +import { useDeployedContractInfo, useScaffoldEventHistory } from "~~/hooks/scaffold-eth"; + +const Multisig: FC = () => { + const { data: contractInfo } = useDeployedContractInfo("MetaMultiSigWallet"); + + const contractAddress = contractInfo?.address; + + const { data: executeTransactionEvents } = useScaffoldEventHistory({ + contractName: "MetaMultiSigWallet", + eventName: "ExecuteTransaction", + fromBlock: 0n, + }); + + return ( +
+
+ + +
+
+ +
+
Events:
+ {executeTransactionEvents?.map(txEvent => ( + )} /> + ))} +
+
+ ); +}; + +export default Multisig; diff --git a/extension/packages/nextjs/app/owners/page.tsx b/extension/packages/nextjs/app/owners/page.tsx new file mode 100644 index 00000000..bd161c2e --- /dev/null +++ b/extension/packages/nextjs/app/owners/page.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { type FC, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useIsMounted, useLocalStorage } from "usehooks-ts"; +import { Abi, encodeFunctionData } from "viem"; +import { Address, AddressInput, IntegerInput } from "~~/components/scaffold-eth"; +import { useDeployedContractInfo, useScaffoldEventHistory, useScaffoldReadContract } from "~~/hooks/scaffold-eth"; + +export type Method = "addSigner" | "removeSigner" | "transferFunds"; +export const METHODS: Method[] = ["addSigner", "removeSigner", "transferFunds"]; +export const OWNERS_METHODS = METHODS.filter(m => m !== "transferFunds"); + +export const DEFAULT_TX_DATA = { + methodName: OWNERS_METHODS[0], + signer: "", + newSignaturesNumber: "", +}; + +export type PredefinedTxData = { + methodName: Method; + signer: string; + newSignaturesNumber: string; + to?: string; + amount?: string; + callData?: `0x${string}` | ""; +}; + +const Owners: FC = () => { + const isMounted = useIsMounted(); + + const router = useRouter(); + + const [predefinedTxData, setPredefinedTxData] = useLocalStorage( + "predefined-tx-data", + DEFAULT_TX_DATA, + ); + + const { data: contractInfo } = useDeployedContractInfo("MetaMultiSigWallet"); + + const { data: signaturesRequired } = useScaffoldReadContract({ + contractName: "MetaMultiSigWallet", + functionName: "signaturesRequired", + }); + + const { data: ownerEventsHistory } = useScaffoldEventHistory({ + contractName: "MetaMultiSigWallet", + eventName: "Owner", + fromBlock: 0n, + }); + + useEffect(() => { + if (predefinedTxData.methodName === "transferFunds") { + setPredefinedTxData(DEFAULT_TX_DATA); + } + }, [predefinedTxData.methodName, setPredefinedTxData]); + + return isMounted() ? ( +
+
+
+
Signatures required: {String(signaturesRequired)}
+ +
+ {ownerEventsHistory?.map((event, i) => ( +
+
+ {event.args.added ? "Added πŸ‘" : "Removed πŸ‘Ž"} +
+ ))} +
+ +
+
+ + +
+ + setPredefinedTxData({ ...predefinedTxData, signer: s })} + /> + + setPredefinedTxData({ ...predefinedTxData, newSignaturesNumber: s as string })} + disableMultiplyBy1e18 + /> + + +
+
+
+
+ ) : null; +}; + +export default Owners; diff --git a/extension/packages/nextjs/app/page.tsx.args.mjs b/extension/packages/nextjs/app/page.tsx.args.mjs new file mode 100644 index 00000000..60fdd15d --- /dev/null +++ b/extension/packages/nextjs/app/page.tsx.args.mjs @@ -0,0 +1,47 @@ +export const imports = `import Image from "next/image";`; + +export const description = ` +
+
+

+ SpeedRunEthereum + Challenge #6: πŸ‘› Multisig Wallet +

+
+ challenge banner +
+

+ πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§ A multisig wallet it's a smart contract that acts like a wallet, allowing us to secure assets by + requiring multiple accounts to "vote" on transactions. Think of it as a treasure chest that can + only be opened when all key parties agree. +

+

+ πŸ“œ The contract keeps track of all transactions. Each transaction can be confirmed or rejected by the + signers (smart contract owners). Only transactions that receive enough confirmations can be + "executed" by the signers. +

+

+ 🌟 The final deliverable is a multisig wallet where you can propose adding and removing signers, + transferring funds to other accounts, and updating the required number of signers to execute a + transaction. After any of the signers propose a transaction, it's up to the signers to confirm and + execute it. Deploy your contracts to a testnet, then build and upload your app to a public web server. +

+

+ πŸ’¬ Meet other builders working on this challenge and get help in the{" "} + + Multisig Build Cohort telegram + +

+
+
+
+
+`; + +export const externalExtensionName = "SpeedRunEthereum Challenge #6"; diff --git a/extension/packages/nextjs/app/pool/_components/TransactionItem.tsx b/extension/packages/nextjs/app/pool/_components/TransactionItem.tsx new file mode 100644 index 00000000..3dad0d93 --- /dev/null +++ b/extension/packages/nextjs/app/pool/_components/TransactionItem.tsx @@ -0,0 +1,251 @@ +import { type FC } from "react"; +import { Address, BlockieAvatar } from "../../../components/scaffold-eth"; +import { Abi, decodeFunctionData, formatEther } from "viem"; +import { DecodeFunctionDataReturnType } from "viem/_types/utils/abi/decodeFunctionData"; +import { useAccount, useWalletClient } from "wagmi"; +import { TransactionData, getPoolServerUrl } from "~~/app/create/page"; +import { + useDeployedContractInfo, + useScaffoldContract, + useScaffoldReadContract, + useTransactor, +} from "~~/hooks/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { notification } from "~~/utils/scaffold-eth"; + +type TransactionItemProps = { tx: TransactionData; completed: boolean; outdated: boolean }; + +export const TransactionItem: FC = ({ tx, completed, outdated }) => { + const { address } = useAccount(); + const { data: walletClient } = useWalletClient(); + const transactor = useTransactor(); + const { targetNetwork } = useTargetNetwork(); + const poolServerUrl = getPoolServerUrl(targetNetwork.id); + + const { data: signaturesRequired } = useScaffoldReadContract({ + contractName: "MetaMultiSigWallet", + functionName: "signaturesRequired", + }); + + const { data: nonce } = useScaffoldReadContract({ + contractName: "MetaMultiSigWallet", + functionName: "nonce", + }); + + const { data: metaMultiSigWallet } = useScaffoldContract({ + contractName: "MetaMultiSigWallet", + walletClient, + }); + + const { data: contractInfo } = useDeployedContractInfo("MetaMultiSigWallet"); + + const txnData = + contractInfo?.abi && tx.data + ? decodeFunctionData({ abi: contractInfo.abi as Abi, data: tx.data }) + : ({} as DecodeFunctionDataReturnType); + + const hasSigned = tx.signers.indexOf(address as string) >= 0; + const hasEnoughSignatures = signaturesRequired ? tx.signatures.length >= Number(signaturesRequired) : false; + + const getSortedSigList = async (allSigs: `0x${string}`[], newHash: `0x${string}`) => { + const sigList = []; + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const s in allSigs) { + const recover = (await metaMultiSigWallet?.read.recover([newHash, allSigs[s]])) as `0x${string}`; + + sigList.push({ signature: allSigs[s], signer: recover }); + } + + sigList.sort((a, b) => { + return BigInt(a.signer) > BigInt(b.signer) ? 1 : -1; + }); + + const finalSigList: `0x${string}`[] = []; + const finalSigners: `0x${string}`[] = []; + const used: Record = {}; + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const s in sigList) { + if (!used[sigList[s].signature]) { + finalSigList.push(sigList[s].signature); + finalSigners.push(sigList[s].signer); + } + used[sigList[s].signature] = true; + } + + return [finalSigList, finalSigners]; + }; + + return ( + <> + +
+
+
+
+
Function Signature:
+ {txnData.functionName || "transferFunds"} +
+
+ {txnData.args ? ( + <> +

Arguments

+
+ Updated signer:
+
+
Updated signatures required: {String(txnData.args?.[1])}
+ + ) : ( + <> +
+ Transfer to:
+
+
Amount: {formatEther(BigInt(tx.amount))} Ξ
+ + )} +
+
+
Sig hash
{" "} +
+ {tx.hash.slice(0, 7)} +
+
+
+ +
+
+
+
+ +
+
+
# {String(tx.nonce)}
+
+ {tx.hash.slice(0, 7)} +
+ +
+ +
{formatEther(BigInt(tx.amount))} Ξ
+ + {String(signaturesRequired) && ( + + {tx.signatures.length}/{String(tx.requiredApprovals)} {hasSigned ? "βœ…" : ""} + + )} + + {completed ? ( +
Completed
+ ) : outdated ? ( +
Outdated
+ ) : ( + <> +
+ +
+ +
+ +
+ + )} + + +
+ +
+
Function name: {txnData.functionName || "transferFunds"}
+ +
+ Addressed to:
+
+
+
+ + ); +}; diff --git a/extension/packages/nextjs/app/pool/_components/index.ts b/extension/packages/nextjs/app/pool/_components/index.ts new file mode 100644 index 00000000..b0cf3f96 --- /dev/null +++ b/extension/packages/nextjs/app/pool/_components/index.ts @@ -0,0 +1 @@ +export * from "./TransactionItem"; diff --git a/extension/packages/nextjs/app/pool/page.tsx b/extension/packages/nextjs/app/pool/page.tsx new file mode 100644 index 00000000..56ae2171 --- /dev/null +++ b/extension/packages/nextjs/app/pool/page.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { type FC, useMemo, useState } from "react"; +import { TransactionData, getPoolServerUrl } from "../create/page"; +import { TransactionItem } from "./_components"; +import { useInterval } from "usehooks-ts"; +import { useChainId } from "wagmi"; +import { + useDeployedContractInfo, + useScaffoldContract, + useScaffoldEventHistory, + useScaffoldReadContract, +} from "~~/hooks/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { notification } from "~~/utils/scaffold-eth"; + +const Pool: FC = () => { + const [transactions, setTransactions] = useState(); + // const [subscriptionEventsHashes, setSubscriptionEventsHashes] = useState<`0x${string}`[]>([]); + const { targetNetwork } = useTargetNetwork(); + const poolServerUrl = getPoolServerUrl(targetNetwork.id); + const { data: contractInfo } = useDeployedContractInfo("MetaMultiSigWallet"); + const chainId = useChainId(); + const { data: nonce } = useScaffoldReadContract({ + contractName: "MetaMultiSigWallet", + functionName: "nonce", + }); + + const { data: eventsHistory } = useScaffoldEventHistory({ + contractName: "MetaMultiSigWallet", + eventName: "ExecuteTransaction", + fromBlock: 0n, + watch: true, + }); + + const { data: metaMultiSigWallet } = useScaffoldContract({ + contractName: "MetaMultiSigWallet", + }); + + const historyHashes = useMemo(() => eventsHistory?.map(ev => ev.args.hash) || [], [eventsHistory]); + + useInterval(() => { + const getTransactions = async () => { + try { + const res: { [key: string]: TransactionData } = await ( + await fetch(`${poolServerUrl}${contractInfo?.address}_${chainId}`) + ).json(); + + const newTransactions: TransactionData[] = []; + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const i in res) { + const validSignatures = []; + // eslint-disable-next-line guard-for-in, no-restricted-syntax + for (const s in res[i].signatures) { + const signer = (await metaMultiSigWallet?.read.recover([ + res[i].hash as `0x${string}`, + res[i].signatures[s], + ])) as `0x${string}`; + + const isOwner = await metaMultiSigWallet?.read.isOwner([signer as string]); + + if (signer && isOwner) { + validSignatures.push({ signer, signature: res[i].signatures[s] }); + } + } + const update: TransactionData = { ...res[i], validSignatures }; + newTransactions.push(update); + } + setTransactions(newTransactions); + } catch (e) { + notification.error("Error fetching transactions"); + console.log(e); + } + }; + + getTransactions(); + }, 3777); + + const lastTx = useMemo( + () => + transactions + ?.filter(tx => historyHashes.includes(tx.hash)) + .sort((a, b) => (BigInt(a.nonce) < BigInt(b.nonce) ? 1 : -1))[0], + [historyHashes, transactions], + ); + + return ( +
+
+
+
Pool
+ +
Nonce: {nonce !== undefined ? `#${nonce}` : "Loading..."}
+ +
+ {transactions === undefined + ? "Loading..." + : transactions.map(tx => { + return ( + + ); + })} +
+
+
+
+ ); +}; + +export default Pool; diff --git a/extension/packages/nextjs/components/Header.tsx.args.mjs b/extension/packages/nextjs/components/Header.tsx.args.mjs new file mode 100644 index 00000000..d3f8f193 --- /dev/null +++ b/extension/packages/nextjs/components/Header.tsx.args.mjs @@ -0,0 +1,25 @@ +export const menuIconImports = `import { Bars3CenterLeftIcon, CheckBadgeIcon, PencilIcon, PlusCircleIcon } from "@heroicons/react/24/outline";`; + +export const menuObjects = `{ + label: "Multisig", + href: "/multisig", + icon: , + }, + { + label: "Owners", + href: "/owners", + icon: , + }, + { + label: "Create", + href: "/create", + icon: , + }, + { + label: "Pool", + href: "/pool", + icon: , + }`; + +export const logoTitle = "SRE Challenges"; +export const logoSubtitle = "#6 Multisig Wallet"; diff --git a/extension/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx.args.mjs b/extension/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx.args.mjs new file mode 100644 index 00000000..d33bccd8 --- /dev/null +++ b/extension/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx.args.mjs @@ -0,0 +1 @@ +export const globalClassNames = "font-space-grotesk"; \ No newline at end of file diff --git a/extension/packages/nextjs/public/hero.png b/extension/packages/nextjs/public/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c238d2517965c75180c33c74438b7438921350 GIT binary patch literal 32166 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
5^lD<6i zUfYo7%&=LZH*#`_)Sdg0c?T(HDjn>sYX6a=^Pac>Vr3;%6SWg<8#2*}6uR5{*bI4z z!y7uT4mUj6z2bqt$snA^_?Grog-kR!N&#GDj#ZF%Dm9- z(!*O|^jVl}XI#~;Xc5>XL>L8fhUxx^?B34)VFy1=>}@{} z7H_2W+sxG(1F{!u#qLQh+j25@)0(y~#I(3A&)^5A&dF)nt+Qth2kEA`kXd;ZTj)C# z1{3l9O?gIX0zk4g6lDIoZaULnzq{!S?iaAYwP+}mVD5z8`XVjQU@=ULD9~27lbUGh z)&d*NuFzytBv_210Y(7I+bfT0DsN6Nlr^+fQ{;i;R!f*eCxvi@-fO~tc>k~VzB8=J zt!q;cDjpRSQ8`jo6hzvgcMuC*P>^0CVx)!8dsPG!q)8KLB1L*9fl#6%HFP8aLT>>= zh!7wINHS0KJ@0%o*Y#cV&hMG^1C$VWSbOia)_t$CPeuaxcy$r8FZ3cl2yUuMrsZU2 z`U0l5hmY=2;(ZUO$BEts+{t_W+;A`u6EPAdC&Pxuxq5rLqc~FuX0sLEciGx zof`y-H09BBrBO4K4v^N@KG#g|Fzve7GCBeqc7y;y_lkSIN^ZCtcl)@b%!T}gZx85O zp1tnKZE+~`QuWhkvf;yjgn=G=-4_&ODB$+T$*t;)B@XfpQ>&T7<0tR{ zsT!s>2*B#!GAjg-RKP!v);Vi7onhSu7$X3(FnCdh75i032kh#i-+Yom!~iZ6X=4*G z9l&_C=V-VT5KJ85^#-UL{wO8ZbP!T^+5lLTBVje_@y@_Y3<>+pU{Zne`~5aO>g#zE4fd3@3 z02cgTq8t&;c+4+(6@i`DXI03V!~IOap95cdD8|@=zvbtJ|N0KVHU5_WKl+EE2jv0_ z;G^N0?Xz7QJW4L2*s;m$!X}mHFF@0(`

0TsyZ$g$s%s%?~#K-o2`2U3UN^e$z@J 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 literal 0 HcmV?d00001 diff --git a/extension/packages/nextjs/public/thumbnail-challenge-6.png b/extension/packages/nextjs/public/thumbnail-challenge-6.png new file mode 100644 index 0000000000000000000000000000000000000000..d52e94e9b7a03ab84716d53846529a4ef40fd86a GIT binary patch literal 32799 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 z Date: Mon, 16 Dec 2024 18:01:46 +0100 Subject: [PATCH 2/2] fix: review --- README.md | 4 +- extension/README.md.args.mjs | 7 +-- .../hardhat/contracts/MetaMultiSigWallet.sol | 62 +------------------ .../packages/nextjs/app/multisig/page.tsx | 12 +++- .../app/pool/_components/TransactionItem.tsx | 3 +- extension/packages/nextjs/app/pool/page.tsx | 24 ++++--- 6 files changed, 32 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 72a0ea34..312c785c 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,8 @@ Before you begin, you need to install the following tools: Then download the challenge to your computer and install dependencies by running: ```sh -git clone https://github.com/scaffold-eth/se-2-challenges.git challenge-6-multisig +npx create-eth@latest -e sre-challenge-6 challenge-6-multisig cd challenge-6-multisig -git checkout challenge-6-multisig -yarn install ``` > in the same terminal, start your local network (a blockchain emulator in your computer): diff --git a/extension/README.md.args.mjs b/extension/README.md.args.mjs index f80e015e..86ffc0df 100644 --- a/extension/README.md.args.mjs +++ b/extension/README.md.args.mjs @@ -70,12 +70,7 @@ At a high-level, the contract core functions are carried out as follows: ## Checkpoint 0: πŸ“¦ Environment πŸ“š -\`\`\`sh -npx create-eth@latest -e sre-challenge-6 challenge-6-multisig -cd challenge-6-multisig -\`\`\` - -> in the same terminal, start your local network (a blockchain emulator in your computer): +> Start your local network (a blockchain emulator in your computer): \`\`\`sh yarn chain diff --git a/extension/packages/hardhat/contracts/MetaMultiSigWallet.sol b/extension/packages/hardhat/contracts/MetaMultiSigWallet.sol index 7c4cd159..ea5b2cf2 100644 --- a/extension/packages/hardhat/contracts/MetaMultiSigWallet.sol +++ b/extension/packages/hardhat/contracts/MetaMultiSigWallet.sol @@ -1,15 +1,13 @@ // SPDX-License-Identifier: MIT -// Off-chain signature gathering multisig that streams funds - @austingriffith +// Off-chain signature gathering multisig that streams funds - @austingriffith // // started from πŸ— scaffold-eth - meta-multi-sig-wallet example https://github.com/austintgriffith/scaffold-eth/tree/meta-multi-sig -// (off-chain signature based multi-sig) -// added a very simple streaming mechanism where `onlySelf` can open a withdraw-based stream +// (off-chain signature based multi-sig) +// added a very simple streaming mechanism where `onlySelf` can open a withdraw-based stream // pragma solidity >=0.8.0 <0.9.0; -// Not needed to be explicitly imported in Solidity 0.8.x -// pragma experimental ABIEncoderV2; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; @@ -118,58 +116,4 @@ contract MetaMultiSigWallet { receive() external payable { emit Deposit(msg.sender, msg.value, address(this).balance); } - - // - // new streaming stuff - // - - event OpenStream(address indexed to, uint256 amount, uint256 frequency); - event CloseStream(address indexed to); - event Withdraw(address indexed to, uint256 amount, string reason); - - struct Stream { - uint256 amount; - uint256 frequency; - uint256 last; - } - mapping(address => Stream) public streams; - - function streamWithdraw(uint256 amount, string memory reason) public { - require(streams[msg.sender].amount > 0, "withdraw: no open stream"); - _streamWithdraw(payable(msg.sender), amount, reason); - } - - function _streamWithdraw(address payable to, uint256 amount, string memory reason) private { - uint256 totalAmountCanWithdraw = streamBalance(to); - require(totalAmountCanWithdraw >= amount, "withdraw: not enough"); - streams[to].last = - streams[to].last + - (((block.timestamp - streams[to].last) * amount) / totalAmountCanWithdraw); - emit Withdraw(to, amount, reason); - (bool success, ) = to.call{ value: amount }(""); - require(success, "withdraw: failed to send"); - } - - function streamBalance(address to) public view returns (uint256) { - return (streams[to].amount * (block.timestamp - streams[to].last)) / streams[to].frequency; - } - - function openStream(address to, uint256 amount, uint256 frequency) public onlySelf { - require(streams[to].amount == 0, "openStream: stream already open"); - require(amount > 0, "openStream: no amount"); - require(frequency > 0, "openStream: no frequency"); - - streams[to].amount = amount; - streams[to].frequency = frequency; - streams[to].last = block.timestamp; - - emit OpenStream(to, amount, frequency); - } - - function closeStream(address payable to) public onlySelf { - require(streams[to].amount > 0, "closeStream: stream already closed"); - _streamWithdraw(to, streams[to].amount, "stream closed"); - delete streams[to]; - emit CloseStream(to); - } } diff --git a/extension/packages/nextjs/app/multisig/page.tsx b/extension/packages/nextjs/app/multisig/page.tsx index 7ba896c5..e29affb5 100644 --- a/extension/packages/nextjs/app/multisig/page.tsx +++ b/extension/packages/nextjs/app/multisig/page.tsx @@ -4,7 +4,10 @@ import { type FC } from "react"; import { TransactionEventItem } from "./_components"; import { QRCodeSVG } from "qrcode.react"; import { Address, Balance } from "~~/components/scaffold-eth"; -import { useDeployedContractInfo, useScaffoldEventHistory } from "~~/hooks/scaffold-eth"; +import { + useDeployedContractInfo, + useScaffoldEventHistory, +} from "~~/hooks/scaffold-eth"; const Multisig: FC = () => { const { data: contractInfo } = useDeployedContractInfo("MetaMultiSigWallet"); @@ -27,8 +30,11 @@ const Multisig: FC = () => {

diff --git a/extension/packages/nextjs/app/pool/_components/TransactionItem.tsx b/extension/packages/nextjs/app/pool/_components/TransactionItem.tsx index 3dad0d93..ad358242 100644 --- a/extension/packages/nextjs/app/pool/_components/TransactionItem.tsx +++ b/extension/packages/nextjs/app/pool/_components/TransactionItem.tsx @@ -1,7 +1,6 @@ import { type FC } from "react"; import { Address, BlockieAvatar } from "../../../components/scaffold-eth"; -import { Abi, decodeFunctionData, formatEther } from "viem"; -import { DecodeFunctionDataReturnType } from "viem/_types/utils/abi/decodeFunctionData"; +import { Abi, DecodeFunctionDataReturnType, decodeFunctionData, formatEther } from "viem"; import { useAccount, useWalletClient } from "wagmi"; import { TransactionData, getPoolServerUrl } from "~~/app/create/page"; import { diff --git a/extension/packages/nextjs/app/pool/page.tsx b/extension/packages/nextjs/app/pool/page.tsx index 56ae2171..3751fd20 100644 --- a/extension/packages/nextjs/app/pool/page.tsx +++ b/extension/packages/nextjs/app/pool/page.tsx @@ -37,7 +37,10 @@ const Pool: FC = () => { contractName: "MetaMultiSigWallet", }); - const historyHashes = useMemo(() => eventsHistory?.map(ev => ev.args.hash) || [], [eventsHistory]); + const historyHashes = useMemo( + () => eventsHistory?.map((ev) => ev.args.hash) || [], + [eventsHistory] + ); useInterval(() => { const getTransactions = async () => { @@ -57,7 +60,9 @@ const Pool: FC = () => { res[i].signatures[s], ])) as `0x${string}`; - const isOwner = await metaMultiSigWallet?.read.isOwner([signer as string]); + const isOwner = await metaMultiSigWallet?.read.isOwner([ + signer as string, + ]); if (signer && isOwner) { validSignatures.push({ signer, signature: res[i].signatures[s] }); @@ -79,9 +84,9 @@ const Pool: FC = () => { const lastTx = useMemo( () => transactions - ?.filter(tx => historyHashes.includes(tx.hash)) + ?.filter((tx) => historyHashes.includes(tx.hash)) .sort((a, b) => (BigInt(a.nonce) < BigInt(b.nonce) ? 1 : -1))[0], - [historyHashes, transactions], + [historyHashes, transactions] ); return ( @@ -95,13 +100,18 @@ const Pool: FC = () => {
{transactions === undefined ? "Loading..." - : transactions.map(tx => { + : transactions.map((tx) => { return ( ); })}