From e83b67c162a284f9a84aa05cff8e5033fe9878a5 Mon Sep 17 00:00:00 2001 From: Sarah Schwartz <58856580+sarahschwartz@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:57:24 -0600 Subject: [PATCH] feat: flow is all frontend --- code/webauthn/README.md | 71 ++------- .../contracts/contracts/GeneralPaymaster.sol | 73 +++++++++ .../contracts/deploy/deployPaymaster.ts | 31 ++++ .../contracts/deploy/registerR1Owner.ts | 10 +- code/webauthn/contracts/package.json | 1 + .../frontend/src/components/Layout.tsx | 3 +- .../frontend/src/hooks/useAccount.tsx | 43 +++++ .../webauthn/frontend/src/hooks/useWallet.tsx | 34 ++++ code/webauthn/frontend/src/pages/_app.tsx | 18 ++- .../frontend/src/pages/create-account.tsx | 104 ++++++++++++ code/webauthn/frontend/src/pages/index.tsx | 32 ++-- code/webauthn/frontend/src/pages/mint.tsx | 81 ++++++---- code/webauthn/frontend/src/pages/register.tsx | 150 +++++++++++------- code/webauthn/frontend/src/pages/transfer.tsx | 139 ++++++++-------- code/webauthn/frontend/src/styles/globals.css | 61 ------- code/webauthn/frontend/utils/constants.ts | 16 ++ .../webauthn/frontend/utils/getTransaction.ts | 23 --- code/webauthn/frontend/utils/tx.ts | 76 +++++++++ code/webauthn/frontend/utils/webauthn.ts | 20 +++ 19 files changed, 667 insertions(+), 319 deletions(-) create mode 100644 code/webauthn/contracts/contracts/GeneralPaymaster.sol create mode 100644 code/webauthn/contracts/deploy/deployPaymaster.ts create mode 100644 code/webauthn/frontend/src/hooks/useAccount.tsx create mode 100644 code/webauthn/frontend/src/hooks/useWallet.tsx create mode 100644 code/webauthn/frontend/src/pages/create-account.tsx create mode 100644 code/webauthn/frontend/utils/constants.ts delete mode 100644 code/webauthn/frontend/utils/getTransaction.ts create mode 100644 code/webauthn/frontend/utils/tx.ts diff --git a/code/webauthn/README.md b/code/webauthn/README.md index 7daa856f..0c1b784f 100644 --- a/code/webauthn/README.md +++ b/code/webauthn/README.md @@ -21,80 +21,29 @@ era_test_node run ### Deploying a New Smart Account -Create a `.env` file and add a testing private key for deployment and a testing wallet to receive funds from the smart account. -The private key and receiver account in the example below are both listed in the rich wallets list: https://docs.zksync.io/build/test-and-debug/in-memory-node#pre-configured-rich-wallets - -```env -WALLET_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 -RECEIVER_ACCOUNT=0xa61464658AfeAf65CccaaFD3a512b69A83B77618 -``` - -Compile and deploy a new smart account using the `compile` and `transfer` scripts: - -```shell -cd contracts -npm run compile -npm run transfer -``` - -From the logged output of the `transfer` script, copy the private key after "SC Account owner pk:" -and the account address after "SC Account deployed on address" -and save them to the `ACCOUNT_PK` and `ACCOUNT_ADDRESS` variables your `.env` file. - -```env -ACCOUNT_ADDRESS=0x... -ACCOUNT_PK=0x... -``` - -You'll come back to this later. - -### Registering a Webauthn Key - Go to the frontend folder and start a development server: ```shell npm run dev ``` -Open the app at `http://localhost:3000` and click the 'Register Account' button to navigate to the `/register` page. -Enter a name for your passkey, and click the `Register New Passkey` button. +> Note: The next steps depend on the app running at port `3000`. -Note: it's important that you don't use another port. This step depends on the app running at port `3000`. +Open the app at `http://localhost:3000` and click the 'Create Account' button to navigate to the `/create-account` page. +Click the button to create a new account. +You can optionally save the private key logged. -Once you see the message `Registered public key! Click me to copy.`, click it and add the private key to the `.env` in the `contracts` folder. - -```env -NEW_R1_OWNER_PUBLIC_KEY=0x... -``` - -Your final `.env` file should look like this: - -```env -WALLET_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 -RECEIVER_ACCOUNT=0xa61464658AfeAf65CccaaFD3a512b69A83B77618 -ACCOUNT_ADDRESS=0x -ACCOUNT_PK=0x -NEW_R1_OWNER_PUBLIC_KEY=0x -``` - -### Adding the Registered Key to Your Smart Contract Account - -Back in the `contracts` folder, run the `register` script to register the public key as a signer. - -```shell -npm run register -``` - -The output should say `R1 Owner updated successfully`. +### Registering a Webauthn Key -### Sending a Txn with Webauthn +Go back to the home page and click the "Register Passkey" button -In the `frontend` folder inside `src/pages/transfer.tsx`, update the `ACCOUNT_ADDRESS` variable with your deployed account address -(same as the `ACCOUNT_ADDRESS` variable in the `contracts/.env` file). +### Siging Transactions with Webauthn -Go back to the home page of the frontend app running at `http://localhost:3000/` and click the `Transfer Funds` link. +Go back to the home page and click the `Transfer Funds` link. Enter any amount into the input and try transfering the ETH. +You can also click the `Mint NFT` button on the home page to try minting an NFT. + ### Managing Registered Passkeys You can delete and edit your registered keys in Google Chrome by going to `chrome://settings/passkeys`. diff --git a/code/webauthn/contracts/contracts/GeneralPaymaster.sol b/code/webauthn/contracts/contracts/GeneralPaymaster.sol new file mode 100644 index 00000000..4161b021 --- /dev/null +++ b/code/webauthn/contracts/contracts/GeneralPaymaster.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; +import {IPaymasterFlow} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol"; +import {TransactionHelper, Transaction} from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; + +import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol"; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +/// @author Matter Labs +/// @notice This contract does not include any validations other than using the paymaster general flow. +contract GeneralPaymaster is IPaymaster { + modifier onlyBootloader() { + require( + msg.sender == BOOTLOADER_FORMAL_ADDRESS, + "Only bootloader can call this method" + ); + // Continue execution if called from the bootloader. + _; + } + + function validateAndPayForPaymasterTransaction( + bytes32, + bytes32, + Transaction calldata _transaction + ) + external + payable + onlyBootloader + returns (bytes4 magic, bytes memory context) + { + // By default we consider the transaction as accepted. + magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC; + require( + _transaction.paymasterInput.length >= 4, + "The standard paymaster input must be at least 4 bytes long" + ); + + bytes4 paymasterInputSelector = bytes4( + _transaction.paymasterInput[0:4] + ); + if (paymasterInputSelector == IPaymasterFlow.general.selector) { + // Note, that while the minimal amount of ETH needed is tx.gasPrice * tx.gasLimit, + // neither paymaster nor account are allowed to access this context variable. + uint256 requiredETH = _transaction.gasLimit * + _transaction.maxFeePerGas; + + // The bootloader never returns any data, so it can safely be ignored here. + (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{ + value: requiredETH + }(""); + require( + success, + "Failed to transfer tx fee to the Bootloader. Paymaster balance might not be enough." + ); + } else { + revert("Unsupported paymaster flow in paymasterParams."); + } + } + + function postTransaction( + bytes calldata _context, + Transaction calldata _transaction, + bytes32, + bytes32, + ExecutionResult _txResult, + uint256 _maxRefundedGas + ) external payable override onlyBootloader {} + + receive() external payable {} +} diff --git a/code/webauthn/contracts/deploy/deployPaymaster.ts b/code/webauthn/contracts/deploy/deployPaymaster.ts new file mode 100644 index 00000000..0d1aa684 --- /dev/null +++ b/code/webauthn/contracts/deploy/deployPaymaster.ts @@ -0,0 +1,31 @@ +import { Wallet, Provider } from 'zksync-ethers'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { Deployer } from '@matterlabs/hardhat-zksync-deploy'; + +// load env file +import dotenv from 'dotenv'; +import { ethers } from 'ethers'; +dotenv.config(); + +const DEPLOYER_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY || ''; + +export default async function (hre: HardhatRuntimeEnvironment) { + // @ts-expect-error target config file which can be testnet or local + const provider = new Provider(hre.network.config.url); + const wallet = new Wallet(DEPLOYER_PRIVATE_KEY, provider); + const deployer = new Deployer(hre, wallet); + const artifact = await deployer.loadArtifact('GeneralPaymaster'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const constructorArguments: any[] = []; + const contract = await deployer.deploy(artifact, constructorArguments); + const paymasterAddress = await contract.getAddress(); + console.log('PAYMASTER CONTRACT ADDRESS: ', paymasterAddress); + + const tx = await wallet.sendTransaction({ + to: paymasterAddress, + value: ethers.parseEther('10'), + }); + + await tx.wait(); + console.log('DONE DEPLOYING & FUNDING PAYMASTER'); +} diff --git a/code/webauthn/contracts/deploy/registerR1Owner.ts b/code/webauthn/contracts/deploy/registerR1Owner.ts index a3048cc9..ce8f0111 100644 --- a/code/webauthn/contracts/deploy/registerR1Owner.ts +++ b/code/webauthn/contracts/deploy/registerR1Owner.ts @@ -26,7 +26,7 @@ export default async function (hre: HardhatRuntimeEnvironment) { const data = contract.interface.encodeFunctionData('updateR1Owner', [NEW_R1_OWNER_PUBLIC_KEY]); const transferAmount = '0'; - const ethTransferTx = { + const registerTx = { from: ACCOUNT_ADDRESS, to: ACCOUNT_ADDRESS, chainId: (await provider.getNetwork()).chainId, @@ -40,19 +40,19 @@ export default async function (hre: HardhatRuntimeEnvironment) { gasLimit: BigInt(20000000), data, }; - const signedTxHash = EIP712Signer.getSignedDigest(ethTransferTx); + const signedTxHash = EIP712Signer.getSignedDigest(registerTx); console.log(`Signed tx hash: ${signedTxHash}`); const signature = ethers.concat([ethers.Signature.from(wallet.signingKey.sign(signedTxHash)).serialized]); console.log(`Signature: ${signature}`); - ethTransferTx.customData = { - ...ethTransferTx.customData, + registerTx.customData = { + ...registerTx.customData, customSignature: signature, }; // make the call console.log('Registering new R1 Owner'); - const sentTx = await provider.broadcastTransaction(types.Transaction.from(ethTransferTx).serialized); + const sentTx = await provider.broadcastTransaction(types.Transaction.from(registerTx).serialized); await sentTx.wait(); console.log('Registration completed!'); diff --git a/code/webauthn/contracts/package.json b/code/webauthn/contracts/package.json index a54d3575..ebe33c3e 100644 --- a/code/webauthn/contracts/package.json +++ b/code/webauthn/contracts/package.json @@ -8,6 +8,7 @@ "scripts": { "deploy": "hardhat deploy-zksync --script deploy.ts", "deploy:NFT": "hardhat deploy-zksync --script deployMyNFT.ts", + "deploy:paymaster": "hardhat deploy-zksync --script deployPaymaster.ts", "transfer": "hardhat deploy-zksync --script deployAndTransfer.ts", "register": "hardhat deploy-zksync --script registerR1Owner.ts", "compile": "hardhat compile", diff --git a/code/webauthn/frontend/src/components/Layout.tsx b/code/webauthn/frontend/src/components/Layout.tsx index dd633581..afb8b19b 100644 --- a/code/webauthn/frontend/src/components/Layout.tsx +++ b/code/webauthn/frontend/src/components/Layout.tsx @@ -2,6 +2,7 @@ import { buttonStyles } from '@/pages'; import { Inter } from 'next/font/google'; import Link from 'next/link'; import React from 'react'; +import { BUTTON_COLORS } from '../../utils/constants'; const inter = Inter({ subsets: ['latin'] }); @@ -11,7 +12,7 @@ export function Layout({ children, isHome }: { children: React.ReactNode; isHome {!isHome && (
Home diff --git a/code/webauthn/frontend/src/hooks/useAccount.tsx b/code/webauthn/frontend/src/hooks/useAccount.tsx new file mode 100644 index 00000000..7815efd9 --- /dev/null +++ b/code/webauthn/frontend/src/hooks/useAccount.tsx @@ -0,0 +1,43 @@ +import type { FC, ReactNode } from 'react'; +import { createContext, useContext, useState } from 'react'; + +export type AccountContext = string | null; + +const AccountCtx = createContext(null); +const SetAccountCtx = createContext<(value: AccountContext) => void>(() => {}); + +export function useAccount() { + return useContext(AccountCtx); +} + +export function useSetAccount() { + return useContext(SetAccountCtx); +} + +interface AccountProviderProps { + children: ReactNode; +} + +const storageLabel = 'smart-account-address'; + +export const AccountProvider: FC = ({ children }) => { + const [state, setState] = useState(() => { + if (typeof window !== 'undefined') { + return (sessionStorage.getItem(storageLabel) as AccountContext) || null; + } + return null; + }); + + const setAccount = (account: AccountContext) => { + setState(account); + if (typeof window !== 'undefined') { + sessionStorage.setItem(storageLabel, account as string); + } + }; + + return ( + + {children} + + ); +}; diff --git a/code/webauthn/frontend/src/hooks/useWallet.tsx b/code/webauthn/frontend/src/hooks/useWallet.tsx new file mode 100644 index 00000000..341ea04d --- /dev/null +++ b/code/webauthn/frontend/src/hooks/useWallet.tsx @@ -0,0 +1,34 @@ +import type { Wallet } from 'zksync-ethers'; +import type { FC, ReactNode } from 'react'; +import { createContext, useContext, useState } from 'react'; + +export type WalletContext = Wallet | null; + +const WalletCtx = createContext(null); +const SetWalletCtx = createContext<(value: WalletContext) => void>(() => {}); + +export function useWallet() { + return useContext(WalletCtx); +} + +export function useSetWallet() { + return useContext(SetWalletCtx); +} + +interface WalletProviderProps { + children: ReactNode; +} + +export const WalletProvider: FC = ({ children }) => { + const [state, setState] = useState(null); + + const setWallet = (wallet: WalletContext) => { + setState(wallet); + }; + + return ( + + {children} + + ); +}; diff --git a/code/webauthn/frontend/src/pages/_app.tsx b/code/webauthn/frontend/src/pages/_app.tsx index c14313e8..fcf2c4dd 100644 --- a/code/webauthn/frontend/src/pages/_app.tsx +++ b/code/webauthn/frontend/src/pages/_app.tsx @@ -1,6 +1,22 @@ import '@/styles/globals.css'; import type { AppProps } from 'next/app'; +import { AccountProvider } from '../hooks/useAccount'; +import { WalletProvider } from '../hooks/useWallet'; +import { Provider } from 'zksync-ethers'; export default function App({ Component, pageProps }: AppProps) { - return ; + const networkProvider = new Provider('http://localhost:8011'); + return ( + <> + + + + ; + + + + ); } diff --git a/code/webauthn/frontend/src/pages/create-account.tsx b/code/webauthn/frontend/src/pages/create-account.tsx new file mode 100644 index 00000000..c8be1a7f --- /dev/null +++ b/code/webauthn/frontend/src/pages/create-account.tsx @@ -0,0 +1,104 @@ +import React, { useEffect, useState } from 'react'; +import { Layout } from '../components/Layout'; +import { buttonStyles } from './index'; +import { AA_FACTORY_ADDRESS, BUTTON_COLORS } from '../../utils/constants'; +import { containerStyles, headerStyles } from './register'; +import { type Provider, utils, Wallet } from 'zksync-ethers'; +import { ethers } from 'ethers'; +import * as factoryAbiJSON from '../../../contracts/artifacts-zk/contracts/AAFactory.sol/AAFactory.json'; +import { getPaymasterOverrides } from '../../utils/tx'; +import { useAccount, useSetAccount } from '@/hooks/useAccount'; +import { useSetWallet, useWallet } from '@/hooks/useWallet'; + +export default function CreateAccount({ provider }: { provider: Provider }) { + const [isMounted, setIsMounted] = useState(false); + const account = useAccount(); + const setAccount = useSetAccount(); + const setWallet = useSetWallet(); + const accountWallet = useWallet(); + + useEffect(() => { + setIsMounted(true); + }, []); + + async function sendTestFunds(to: string) { + const richWallet = new Wallet('0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110', provider); + const tx = { + to, + value: ethers.utils.parseEther('100'), + }; + const txResponse = await richWallet.sendTransaction(tx); + await txResponse.wait(); + } + + async function deployNewAccount() { + const owner = Wallet.createRandom().connect(provider); + const aaFactory = new ethers.Contract(AA_FACTORY_ADDRESS, factoryAbiJSON.abi, owner); + const salt = '0x0000000000000000000000000000000000000000000000000000000000000000'; + const overrides = await getPaymasterOverrides(); + const tx = await aaFactory.deployAccount(salt, owner.address, overrides); + await tx.wait(); + + const abiCoder = new ethers.utils.AbiCoder(); + const accountAddress = utils.create2Address( + AA_FACTORY_ADDRESS, + await aaFactory.aaBytecodeHash(), + salt, + abiCoder.encode(['address'], [owner.address]) + ); + + return { accountAddress, owner }; + } + + async function createAccount() { + try { + const { accountAddress, owner } = await deployNewAccount(); + if (provider.connection.url.includes('localhost')) { + await sendTestFunds(accountAddress); + } + setWallet(owner); + setAccount(accountAddress); + } catch (error) { + console.log('ERROR:', error); + alert('Error creating account'); + } + } + + return ( + +

Create an Account

+
+ +
+ {account && isMounted && ( +
+
Your current account is:
+
{account}
+
+ )} + {accountWallet && ( +
+
Your current account private key is:
+
{accountWallet?.privateKey}
+
+ )} +
+ ); +} diff --git a/code/webauthn/frontend/src/pages/index.tsx b/code/webauthn/frontend/src/pages/index.tsx index 9a3337c6..e17df5a7 100644 --- a/code/webauthn/frontend/src/pages/index.tsx +++ b/code/webauthn/frontend/src/pages/index.tsx @@ -1,5 +1,7 @@ import Link from 'next/link'; import { Layout } from '../components/Layout'; +import { BUTTON_COLORS } from '../../utils/constants'; +import React from 'react'; export default function Home() { return ( @@ -7,19 +9,31 @@ export default function Home() {

Sign Txns with WebAuthn Demo

+ Create Account + + - Register Account + Register Passkey Transfer Funds Mint NFT @@ -31,17 +45,16 @@ export default function Home() { export const buttonStyles = { padding: '1rem 2rem', - backgroundColor: '#0621ba', borderRadius: '5px', textDecoration: 'none', cursor: 'pointer', - borderColor: 'transparent', + color: 'black', + border: '1px solid white', fontSize: '1.2rem', width: '300px', margin: 'auto', textAlign: 'center', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -} as any; +} as React.CSSProperties; const containerStyles = { display: 'flex', @@ -49,5 +62,4 @@ const containerStyles = { flexDirection: 'column', gap: '1.5rem', marginTop: '1rem', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -} as any; +} as React.CSSProperties; diff --git a/code/webauthn/frontend/src/pages/mint.tsx b/code/webauthn/frontend/src/pages/mint.tsx index a2e9c814..d5c8645d 100644 --- a/code/webauthn/frontend/src/pages/mint.tsx +++ b/code/webauthn/frontend/src/pages/mint.tsx @@ -1,22 +1,26 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Provider } from 'zksync-ethers'; +import type { Provider } from 'zksync-ethers'; import { getDataToSign, signAndSend } from '../../utils/sign'; -import { getTransaction } from '../../utils/getTransaction'; -import React from 'react'; +import { getTransaction } from '../../utils/tx'; +import React, { useEffect, useState } from 'react'; import { Layout } from '../components/Layout'; import { buttonStyles } from '.'; import { containerStyles } from './register'; import { ethers } from 'ethers'; import { authenticate } from '../../utils/webauthn'; import * as NFT_ABI_JSON from '../../../contracts/artifacts-zk/contracts/MyNFT.sol/MyNFT.json'; +import { BUTTON_COLORS, NFT_CONTRACT_ADDRESS } from '../../utils/constants'; +import { useAccount } from '@/hooks/useAccount'; -// Update this with your deployed smart contract account address and NFT contract address -const ACCOUNT_ADDRESS = '0x'; -const NFT_CONTRACT_ADDRESS = '0x'; - -export default function Mint() { +export default function Mint({ provider }: { provider: Provider }) { + const [isMounted, setIsMounted] = useState(false); const [mintedSVG, setMintedSVG] = React.useState(null); - const provider = new Provider('http://localhost:8011'); + + const account = useAccount(); + + useEffect(() => { + setIsMounted(true); + }, []); async function mint(e: any) { e.preventDefault(); @@ -26,7 +30,7 @@ export default function Mint() { const functionArgs: any[] = []; const data = contract.interface.encodeFunctionData(functionName, functionArgs); const transferValue = '0'; - const tx = await getTransaction(NFT_CONTRACT_ADDRESS, ACCOUNT_ADDRESS, transferValue, data, provider); + const tx = await getTransaction(NFT_CONTRACT_ADDRESS, account!, transferValue, data, provider); const signedTxHash = getDataToSign(tx); const authResponse = await authenticate(signedTxHash.toString()); const receipt = await signAndSend(provider, tx, authResponse); @@ -53,39 +57,46 @@ export default function Mint() { setMintedSVG(svg); } catch (error) { console.log('ERROR:', error); + alert('Error minting NFT'); } } return ( -
- -
- {mintedSVG && ( + {account && isMounted ? ( <> -

🎉 You minted a ZEEK NFT! 🎉

-
- Minted NFT +
+
+ {mintedSVG && ( + <> +

🎉 You minted a ZEEK NFT! 🎉

+
+ Minted NFT +
+ + )} + ) : ( +

Create an account first.

)} ); diff --git a/code/webauthn/frontend/src/pages/register.tsx b/code/webauthn/frontend/src/pages/register.tsx index fa3b7466..9aeceebf 100644 --- a/code/webauthn/frontend/src/pages/register.tsx +++ b/code/webauthn/frontend/src/pages/register.tsx @@ -1,13 +1,27 @@ -import { platformAuthenticatorIsAvailable, startRegistration } from '@simplewebauthn/browser'; -import { bufferToHex, parseHex } from '../../utils/string'; -import * as cbor from 'cbor'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Layout } from '../components/Layout'; import { buttonStyles } from './index'; +import { BUTTON_COLORS } from '../../utils/constants'; +import { useAccount } from '@/hooks/useAccount'; +import { platformAuthenticatorIsAvailable, startRegistration } from '@simplewebauthn/browser'; +import { getPublicKeyFromAuthenticatorData } from '../../utils/webauthn'; +import { registerKeyInAccount } from '../../utils/tx'; +import { useSetWallet, useWallet } from '@/hooks/useWallet'; +import { Wallet, type Provider } from 'zksync-ethers'; -export default function Register() { +export default function Register({ provider }: { provider: Provider }) { + const [isMounted, setIsMounted] = useState(false); const [registeredPublicKey, setRegisteredPublicKey] = useState(); const [userName, setUserName] = useState(); + const [accountPk, setAccountPk] = useState(); + + const account = useAccount(); + const accountWallet = useWallet(); + const setWallet = useSetWallet(); + + useEffect(() => { + setIsMounted(true); + }, []); async function registerNewPasskey(e: React.MouseEvent) { e.preventDefault(); @@ -37,65 +51,87 @@ export default function Register() { const pubKeyFromAuth = getPublicKeyFromAuthenticatorData(regResp.response.authenticatorData!); console.log('PUB KEY FROM AUTH:', pubKeyFromAuth); setRegisteredPublicKey(pubKeyFromAuth); + await registerKeyInAccount(pubKeyFromAuth, account!, provider, accountWallet!); } catch (error) { console.log('ERROR:', error); + alert('Error registering new passkey'); } } - function getPublicKeyFromAuthenticatorData(authData: string): string { - const authDataBuffer = Buffer.from(authData, 'base64'); - const credentialData = authDataBuffer.subarray(32 + 1 + 4 + 16, authDataBuffer.length); // RP ID Hash + Flags + Counter + AAGUID - const lbase = credentialData.subarray(0, 2).toString('hex'); - const l = parseInt(lbase, 16); - const credentialPubKey = credentialData.subarray(2 + l, credentialData.length); // sizeof(L) + L - return getPublicKeyFromCredentialPublicKey(credentialPubKey); - } - - function getPublicKeyFromCredentialPublicKey(credentialPublicKey: Uint8Array): string { - const publicKey: Map<-2 | -3 | -1 | 1 | 3, Buffer | number> = cbor.decode(credentialPublicKey); - - const x = bufferToHex(publicKey.get(-2) as Buffer); - const y = bufferToHex(publicKey.get(-3) as Buffer); - - return x.concat(parseHex(y)); - } - return (

Register a New Passkey

-
-
- - - setUserName(e.target.value)} - /> -
- -
- -
-
- -
+ {!account && isMounted &&

Please create an account first

} + {account && isMounted && !accountWallet && ( +
+

Add the private key for

+

{account}

+

or create a new account

+
+ + + setAccountPk(e.target.value)} + /> +
+ +
+ +
+
+ )} + {account && isMounted && accountWallet && ( +
+
+ + + setUserName(e.target.value)} + /> +
+ +
+ +
+
+ )} + +
{registeredPublicKey && (

- Registered public key! Click me to copy. + 🎉 Registered public key to account! 🎉

)}
diff --git a/code/webauthn/frontend/src/pages/transfer.tsx b/code/webauthn/frontend/src/pages/transfer.tsx index 3e1c135e..b3011f81 100644 --- a/code/webauthn/frontend/src/pages/transfer.tsx +++ b/code/webauthn/frontend/src/pages/transfer.tsx @@ -1,31 +1,33 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Provider } from 'zksync-ethers'; +import type { Provider } from 'zksync-ethers'; import { getDataToSign, signAndSend } from '../../utils/sign'; -import { getTransaction } from '../../utils/getTransaction'; +import { getTransaction } from '../../utils/tx'; import React, { useEffect, useState } from 'react'; import { Layout } from '../components/Layout'; import { buttonStyles } from '.'; import { containerStyles, headerStyles, inputStyles, labelStyles } from './register'; import { formatEther } from 'ethers/lib/utils'; import { authenticate } from '../../utils/webauthn'; +import { BUTTON_COLORS } from '../../utils/constants'; +import { useAccount } from '@/hooks/useAccount'; -// Update this with your deployed smart contract account address -const ACCOUNT_ADDRESS = '0x'; - -export default function Transfer() { +export default function Transfer({ provider }: { provider: Provider }) { + const [isMounted, setIsMounted] = useState(false); const [transferValue, setTransferValue] = useState('1'); const [smartAccountBalance, setSmartAccountBalance] = useState(); const [receiverAccountBalance, setReceiverAccountBalance] = useState(); const [receiverAddress, setReceiverAddress] = useState('0xa61464658AfeAf65CccaaFD3a512b69A83B77618'); - const provider = new Provider('http://localhost:8011'); + + const account = useAccount(); useEffect(() => { updateBalances(); + setIsMounted(true); }, []); async function updateBalances(newReceiverAddress?: string) { try { - const bal1 = await provider.getBalance(ACCOUNT_ADDRESS); + const bal1 = await provider.getBalance(account!); const bal2 = await provider.getBalance(newReceiverAddress ?? receiverAddress); setSmartAccountBalance(formatEther(bal1)); setReceiverAccountBalance(formatEther(bal2)); @@ -39,7 +41,7 @@ export default function Transfer() { e.preventDefault(); try { const data = '0x'; - const tx = await getTransaction(receiverAddress, ACCOUNT_ADDRESS, transferValue, data, provider); + const tx = await getTransaction(receiverAddress, account!, transferValue, data, provider); const signedTxHash = getDataToSign(tx); const authResponse = await authenticate(signedTxHash.toString()); const receipt = await signAndSend(provider, tx, authResponse); @@ -47,6 +49,7 @@ export default function Transfer() { updateBalances(); } catch (error) { console.log('ERROR:', error); + alert('Error sending ETH'); } } @@ -58,63 +61,69 @@ export default function Transfer() { return (

Transfer Funds

-
-
- - -
-
- - setTransferValue(e.target.value)} - /> -
+ {account && isMounted ? ( + <> + +
+ + +
+
+ + setTransferValue(e.target.value)} + /> +
-
- -
-
-

Account Balances

-
-

Smart Account Balance:

-

{smartAccountBalance} ETH

-
-
-

Receiver Account Balance:

-

{receiverAccountBalance} ETH

-
+
+ +
+ +

Account Balances

+
+

Smart Account Balance:

+

{smartAccountBalance} ETH

+
+
+

Receiver Account Balance:

+

{receiverAccountBalance} ETH

+
+ + ) : ( +
Please create an account first
+ )}
); } diff --git a/code/webauthn/frontend/src/styles/globals.css b/code/webauthn/frontend/src/styles/globals.css index 4d750b87..d2a7c177 100644 --- a/code/webauthn/frontend/src/styles/globals.css +++ b/code/webauthn/frontend/src/styles/globals.css @@ -1,53 +1,3 @@ -:root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', - 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient(to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3)); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient(#ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; - } -} - * { box-sizing: border-box; padding: 0; @@ -60,18 +10,7 @@ body { overflow-x: hidden; } -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); -} - a { color: inherit; text-decoration: none; } - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } -} diff --git a/code/webauthn/frontend/utils/constants.ts b/code/webauthn/frontend/utils/constants.ts new file mode 100644 index 00000000..8ed0fbfc --- /dev/null +++ b/code/webauthn/frontend/utils/constants.ts @@ -0,0 +1,16 @@ +export const AA_FACTORY_ADDRESS = '0x'; + +export const PAYMASTER_ADDRESS = '0x'; + +export const NFT_CONTRACT_ADDRESS = '0x'; + +export const BUTTON_COLORS = [ + 'linear-gradient(90deg, #69b7eb, #b3dbd3, #f4d6db)', + 'linear-gradient(90deg, #cfecd0, #ffc5ca)', + 'linear-gradient(90deg, #f598a8, #f6edb2)', + 'linear-gradient(90deg, #ee5c87, #f1a4b5, #d587b3)', + 'linear-gradient(90deg, #b9deed, #efefef)', + 'linear-gradient(90deg, #aea4e3, #d3ffe8)', + 'linear-gradient(90deg, #faf0cd, #fab397)', + 'linear-gradient(90deg, #cfecd0, #a0cea7, #9ec0db)', +]; diff --git a/code/webauthn/frontend/utils/getTransaction.ts b/code/webauthn/frontend/utils/getTransaction.ts deleted file mode 100644 index b796ba50..00000000 --- a/code/webauthn/frontend/utils/getTransaction.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Provider } from 'zksync-ethers'; -import { ethers } from 'ethers'; -import { DEFAULT_GAS_PER_PUBDATA_LIMIT } from 'zksync-ethers/build/utils'; - -export async function getTransaction(to: string, from: string, value: string, data: string, provider: Provider) { - const gasPrice = await provider.getGasPrice(); - const nonce = await provider.getTransactionCount(from); - const chainId = 260; - return { - to, - from, - value: ethers.utils.parseEther(value), - data, - gasPrice, - gasLimit: BigInt(2000000000), - chainId, - nonce, - type: 113, - customData: { - gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, - }, - }; -} diff --git a/code/webauthn/frontend/utils/tx.ts b/code/webauthn/frontend/utils/tx.ts new file mode 100644 index 00000000..e4c4e095 --- /dev/null +++ b/code/webauthn/frontend/utils/tx.ts @@ -0,0 +1,76 @@ +import { DEFAULT_GAS_PER_PUBDATA_LIMIT, getPaymasterParams } from 'zksync-ethers/build/utils'; +import { EIP712Signer, type Wallet, type Provider, utils, type types } from 'zksync-ethers'; +import { ethers } from 'ethers'; +import * as accountAbiJSON from '../../contracts/artifacts-zk/contracts/Account.sol/Account.json'; +import { PAYMASTER_ADDRESS } from './constants'; + +export async function getTransaction(to: string, from: string, value: string, data: string, provider: Provider) { + const gasPrice = await provider.getGasPrice(); + const chainId = (await provider.getNetwork()).chainId; + const nonce = await provider.getTransactionCount(from); + const overrides = await getPaymasterOverrides(); + return { + to, + from, + value: ethers.utils.parseEther(value), + data, + gasPrice, + gasLimit: BigInt(2000000000), + chainId, + nonce, + type: 113, + customData: overrides.customData as types.Eip712Meta, + }; +} + +export async function getPaymasterOverrides() { + const paymasterParams = getPaymasterParams(PAYMASTER_ADDRESS, { + type: 'General', + innerInput: new Uint8Array(), + }); + return { + customData: { + gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; +} + +export async function registerKeyInAccount(pubKey: string, account: string, provider: Provider, wallet: Wallet) { + try { + const contract = new ethers.Contract(account, accountAbiJSON.abi, provider); + const data = contract.interface.encodeFunctionData('updateR1Owner', [pubKey]); + const transferAmount = '0'; + const overrides = await getPaymasterOverrides(); + + const registerTx = { + to: account, + from: account, + chainId: (await provider.getNetwork()).chainId, + nonce: await provider.getTransactionCount(account), + type: 113, + customData: overrides.customData, + value: ethers.utils.parseEther(transferAmount), + gasPrice: await provider.getGasPrice(), + gasLimit: BigInt(2000000000), + data, + }; + const signedTxHash = EIP712Signer.getSignedDigest(registerTx); + const signingKey: ethers.utils.SigningKey = new ethers.utils.SigningKey(wallet.privateKey); + const walletSignature = signingKey.signDigest(signedTxHash); + const sig = ethers.utils.joinSignature(walletSignature); + const signature = ethers.utils.concat([sig]); + + registerTx.customData = { + ...registerTx.customData, + customSignature: signature, + }; + + const finalTx = utils.serialize(registerTx); + const sentTx = await provider.sendTransaction(finalTx); + await sentTx.wait(); + } catch (error) { + console.error('Error:', error); + } +} diff --git a/code/webauthn/frontend/utils/webauthn.ts b/code/webauthn/frontend/utils/webauthn.ts index 475b0186..1cbeb3a2 100644 --- a/code/webauthn/frontend/utils/webauthn.ts +++ b/code/webauthn/frontend/utils/webauthn.ts @@ -1,4 +1,6 @@ import { startAuthentication } from '@simplewebauthn/browser'; +import { bufferToHex, parseHex } from './string'; +import * as cbor from 'cbor'; export async function authenticate(challenge: string) { const resp = await fetch('http://localhost:3000/api/generate-authentication-options', { @@ -14,3 +16,21 @@ export async function authenticate(challenge: string) { const authResp = await startAuthentication(options); return authResp.response; } + +export function getPublicKeyFromAuthenticatorData(authData: string): string { + const authDataBuffer = Buffer.from(authData, 'base64'); + const credentialData = authDataBuffer.subarray(32 + 1 + 4 + 16, authDataBuffer.length); // RP ID Hash + Flags + Counter + AAGUID + const lbase = credentialData.subarray(0, 2).toString('hex'); + const l = parseInt(lbase, 16); + const credentialPubKey = credentialData.subarray(2 + l, credentialData.length); // sizeof(L) + L + return getPublicKeyFromCredentialPublicKey(credentialPubKey); +} + +function getPublicKeyFromCredentialPublicKey(credentialPublicKey: Uint8Array): string { + const publicKey: Map<-2 | -3 | -1 | 1 | 3, Buffer | number> = cbor.decode(credentialPublicKey); + + const x = bufferToHex(publicKey.get(-2) as Buffer); + const y = bufferToHex(publicKey.get(-3) as Buffer); + + return x.concat(parseHex(y)); +}