Skip to content

Commit

Permalink
feat: flow is all frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahschwartz committed Sep 11, 2024
1 parent da63c60 commit e83b67c
Show file tree
Hide file tree
Showing 19 changed files with 667 additions and 319 deletions.
71 changes: 10 additions & 61 deletions code/webauthn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<YOUR_ACCOUNT_ADDRESS_HERE>
ACCOUNT_PK=0x<YOUR_ACCOUNT_PRIVATE_KEY_HERE>
NEW_R1_OWNER_PUBLIC_KEY=0x<YOUR_PUB_KEY_HERE>
```

### 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`.
73 changes: 73 additions & 0 deletions code/webauthn/contracts/contracts/GeneralPaymaster.sol
Original file line number Diff line number Diff line change
@@ -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 {}
}
31 changes: 31 additions & 0 deletions code/webauthn/contracts/deploy/deployPaymaster.ts
Original file line number Diff line number Diff line change
@@ -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');
}
10 changes: 5 additions & 5 deletions code/webauthn/contracts/deploy/registerR1Owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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!');

Expand Down
1 change: 1 addition & 0 deletions code/webauthn/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion code/webauthn/frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'] });

Expand All @@ -11,7 +12,7 @@ export function Layout({ children, isHome }: { children: React.ReactNode; isHome
{!isHome && (
<div style={{ margin: '2rem 1rem' }}>
<Link
style={buttonStyles}
style={{ ...buttonStyles, background: BUTTON_COLORS[7] }}
href="/"
>
Home
Expand Down
43 changes: 43 additions & 0 deletions code/webauthn/frontend/src/hooks/useAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { FC, ReactNode } from 'react';
import { createContext, useContext, useState } from 'react';

export type AccountContext = string | null;

const AccountCtx = createContext<AccountContext>(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<AccountProviderProps> = ({ children }) => {
const [state, setState] = useState<AccountContext>(() => {
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 (
<AccountCtx.Provider value={state}>
<SetAccountCtx.Provider value={setAccount}>{children}</SetAccountCtx.Provider>
</AccountCtx.Provider>
);
};
34 changes: 34 additions & 0 deletions code/webauthn/frontend/src/hooks/useWallet.tsx
Original file line number Diff line number Diff line change
@@ -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<WalletContext>(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<WalletProviderProps> = ({ children }) => {
const [state, setState] = useState<WalletContext>(null);

const setWallet = (wallet: WalletContext) => {
setState(wallet);
};

return (
<WalletCtx.Provider value={state}>
<SetWalletCtx.Provider value={setWallet}>{children}</SetWalletCtx.Provider>
</WalletCtx.Provider>
);
};
18 changes: 17 additions & 1 deletion code/webauthn/frontend/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -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 <Component {...pageProps} />;
const networkProvider = new Provider('http://localhost:8011');
return (
<>
<AccountProvider>
<WalletProvider>
<Component
{...pageProps}
provider={networkProvider}
/>
;
</WalletProvider>
</AccountProvider>
</>
);
}
Loading

0 comments on commit e83b67c

Please sign in to comment.