Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ Scaffold-Base is a fork of Scaffold-ETH2 ready to ship to Base. This fork provid

![Scaffold-Base)](https://github.com/damianmarti/se-2/assets/466652/eac667a7-68fb-4f69-a427-126f7de4114d)

## Smart Wallet ERC 4337

Smart Wallet ERC 4337 support using Account Kit from Alchemy https://accountkit.alchemy.com


![localhost_3000_smartWallet](https://github.com/BuidlGuidl/scaffold-base/assets/466652/ae576140-afac-4652-8c09-504a0f1c521c)

## Documentation

We highly recommend the Scaffold-ETH2 docs as the primary guideline.

# (forked from 🏗 Scaffold-ETH2)
Expand Down
101 changes: 101 additions & 0 deletions packages/nextjs/app/smartWallet/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"use client";

import { useState } from "react";
import Image from "next/image";
import { blo } from "blo";
import type { NextPage } from "next";
import { QRCodeSVG } from "qrcode.react";
import { parseEther } from "viem";
import { Address, AddressInput, Balance, EtherInput } from "~~/components/scaffold-eth";
import QrCodeSkeleton from "~~/components/scaffold-eth/QRCodeSkeleton";
import { useSmartAccount } from "~~/hooks/scaffold-eth/useSmartAccount";
import { useSmartTransactor } from "~~/hooks/scaffold-eth/useSmartTransactor";
import { notification } from "~~/utils/scaffold-eth";

const SmartWallet: NextPage = () => {
const { scaAddress, scaSigner } = useSmartAccount();
const [etherInput, setEtherInput] = useState("");
const [toAddress, setToAddress] = useState("");
const transactor = useSmartTransactor();
const [isTxnLoading, setIsTxnLoading] = useState(false);

const handleSendEther = async () => {
if (!scaSigner) {
notification.error("Cannot access smart account");
return;
}
setIsTxnLoading(true);

try {
const userOperationPromise = scaSigner.sendUserOperation({
value: parseEther(etherInput),
target: toAddress as `0x${string}`,
data: "0x",
});

await transactor(() => userOperationPromise);
} catch (e) {
notification.error("Oops, something went wrong");
console.error("Error sending transaction: ", e);
} finally {
setIsTxnLoading(false);
}
};

return (
<div className="flex items-center flex-col flex-grow pt-10">
<div className="px-5 mb-6">
<h1 className="text-center">
<div className="block text-4xl font-bold">
<div className="inline-block relative w-10 h-10 align-bottom mr-2">
<Image alt="Base logo" className="cursor-pointer" fill src="/Base_Symbol_Blue.svg" />
</div>
Smart Wallet
</div>
</h1>
</div>

{scaAddress ? (
<div className="space-y-4 flex flex-col items-center bg-base-100 border-base-300 border shadow-md shadow-secondary rounded-3xl px-8 py-8 w-[24rem] max-w-sm">
{scaAddress ? (
<>
<div className="relative shadow-xl rounded-xl">
<QRCodeSVG className="rounded-xl" value={scaAddress} size={230} />
<Image
alt={scaAddress}
className="rounded-xl absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 shadow-2xl border-2 border-black"
src={blo(scaAddress as `0x${string}`)}
width={50}
height={50}
/>
</div>
<Address address={scaAddress} size="lg" />
<div className="flex gap-1 items-center">
<span className="text-xl font-semibold">Balance:</span>
<Balance address={scaAddress} className="px-0 py-0 mt-1 text-lg" />
</div>
</>
) : (
<QrCodeSkeleton />
)}
<div className="divider text-xl">Send ETH</div>
<AddressInput value={toAddress} placeholder="Receiver's address" onChange={setToAddress} />
<EtherInput placeholder="Value to send" value={etherInput} onChange={setEtherInput} />
<button
className="btn btn-primary rounded-xl"
disabled={!scaAddress || isTxnLoading}
onClick={handleSendEther}
>
{isTxnLoading ? <span className="loading loading-spinner"></span> : "Send"}
</button>
</div>
) : (
<div className="alert alert-warning w-[24rem]">
<p>Smart Wallet is not available on this network. Please switch to a supported network.</p>
</div>
)}
</div>
);
};

export default SmartWallet;
4 changes: 4 additions & 0 deletions packages/nextjs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const menuLinks: HeaderMenuLink[] = [
label: "Home",
href: "/",
},
{
label: "Smart Wallet",
href: "/smartWallet",
},
{
label: "Debug Contracts",
href: "/debug",
Expand Down
16 changes: 16 additions & 0 deletions packages/nextjs/components/scaffold-eth/QRCodeSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";

const QrCodeSkeleton = () => {
return (
<div className="flex flex-col items-center gap-6">
<div className="animate-pulse">
<div className="bg-gray-200 rounded-xl w-60 h-60" />
</div>
<div className="animate-pulse">
<div className="bg-gray-200 rounded-xl w-60 h-8" />
</div>
</div>
);
};

export default QrCodeSkeleton;
2 changes: 2 additions & 0 deletions packages/nextjs/hooks/scaffold-eth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export * from "./useTransactor";
export * from "./useFetchBlocks";
export * from "./useContractLogs";
export * from "./useAutoConnect";
export * from "./useSmartAccount";
export * from "./useSmartTransactor";
58 changes: 58 additions & 0 deletions packages/nextjs/hooks/scaffold-eth/useSmartAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useEffect, useMemo, useState } from "react";
import { loadBurnerSK } from "../scaffold-eth";
import { useTargetNetwork } from "../scaffold-eth/useTargetNetwork";
import { LightSmartContractAccount, getDefaultLightAccountFactoryAddress } from "@alchemy/aa-accounts";
import { AlchemyProvider } from "@alchemy/aa-alchemy";
import { Address, LocalAccountSigner, getDefaultEntryPointAddress } from "@alchemy/aa-core";
import scaffoldConfig from "~~/scaffold.config";

const burnerPK = loadBurnerSK();
const burnerSigner = LocalAccountSigner.privateKeyToAccountSigner(burnerPK);

export const useSmartAccount = () => {
const [scaAddress, setScaAddress] = useState<Address>();
const [scaSigner, setScaSigner] = useState<AlchemyProvider>();
const { targetNetwork: chain } = useTargetNetwork();
const provider = useMemo(
() => {
try {
return new AlchemyProvider({
chain: chain,
apiKey: scaffoldConfig.alchemyApiKey,
opts: {
txMaxRetries: 20,
txRetryIntervalMs: 2_000,
txRetryMulitplier: 1.2,
},
});
} catch (e) {
console.error("Error creating AlchemyProvider");
return undefined;
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[chain.id],
);

useEffect(() => {
const connectedProvider = provider?.connect(provider => {
return new LightSmartContractAccount({
rpcClient: provider,
owner: burnerSigner,
chain,
entryPointAddress: getDefaultEntryPointAddress(chain),
factoryAddress: getDefaultLightAccountFactoryAddress(chain),
});
});
const getScaAddress = async () => {
const address = await connectedProvider?.getAddress();
console.log("🔥 scaAddress", address);
setScaAddress(address);
};
setScaSigner(connectedProvider);
getScaAddress();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chain.id]);

return { provider, scaSigner, scaAddress };
};
140 changes: 140 additions & 0 deletions packages/nextjs/hooks/scaffold-eth/useSmartTransactor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
getBlockExplorerTxLink,
getParsedError,
getUserOpExplorerTxLink,
notification,
} from "../../utils/scaffold-eth";
import { useSmartAccount } from "./useSmartAccount";
import { SendUserOperationResult } from "@alchemy/aa-core";
import { WriteContractResult, getPublicClient } from "@wagmi/core";
import { Hash, TransactionReceipt, WalletClient } from "viem";
import { useWalletClient } from "wagmi";

type TransactionFunc = (
tx: () => Promise<SendUserOperationResult | `0x${string}`>,

options?: {
onBlockConfirmation?: (txnReceipt: TransactionReceipt) => void;
blockConfirmations?: number;
},
) => Promise<Hash | undefined>;

/**
* Custom notification content for TXs.
*/
export const TxnNotification = ({
message,
blockExplorerLink,
userOpExplorerLink,
}: {
message: string;
blockExplorerLink?: string;
userOpExplorerLink?: string;
}) => {
return (
<div className={`flex flex-col ml-1 cursor-default`}>
<p className="my-0">{message}</p>
{blockExplorerLink && blockExplorerLink.length > 0 ? (
<a href={blockExplorerLink} target="_blank" rel="noreferrer" className="block link text-md">
check out transaction
</a>
) : null}
{userOpExplorerLink && userOpExplorerLink.length > 0 ? (
<a href={userOpExplorerLink} target="_blank" rel="noreferrer" className="block link text-md">
inspect userOp
</a>
) : null}
</div>
);
};

/**
* Runs Transaction passed in to returned function showing UI feedback.
* @param _walletClient - Optional wallet client to use. If not provided, will use the one from useWalletClient.
* @returns function that takes in transaction function as callback, shows UI feedback for transaction and returns a promise of the transaction hash
*/
export const useSmartTransactor = (_walletClient?: WalletClient): TransactionFunc => {
let walletClient = _walletClient;
const { data } = useWalletClient();
const { provider } = useSmartAccount();
if (walletClient === undefined && data) {
walletClient = data;
}

const result: TransactionFunc = async (tx, options) => {
if (!walletClient) {
notification.error("Cannot access account");
console.error("⚡️ ~ file: useTransactor.tsx ~ error");
return;
}

let notificationId = null;
let userOpHash: Awaited<WriteContractResult>["hash"] | undefined = undefined;
try {
if (!provider) {
notification.error("Cannot access smart account");
return;
}

const network = await walletClient.getChainId();
// Get full transaction from public client
const publicClient = getPublicClient();

if (typeof tx === "function") {
// Tx is already prepared by the caller
const result = await tx();
if (typeof result === "string") {
userOpHash = result;
} else {
userOpHash = result.hash;
}
} else {
throw new Error("Incorrect transaction passed to transactor");
}
const userOpTxnURL = network ? getUserOpExplorerTxLink(network, userOpHash) : "";

notificationId = notification.loading(
<TxnNotification message="Waiting for transaction to complete." userOpExplorerLink={userOpTxnURL} />,
);

const txnHash = await provider.waitForUserOperationTransaction(userOpHash);
const blockExplorerTxURL = network ? getBlockExplorerTxLink(network, txnHash) : "";

let transactionReceipt;

if (options?.blockConfirmations) {
transactionReceipt = await publicClient.waitForTransactionReceipt({
hash: txnHash,
confirmations: options?.blockConfirmations,
});
}

notification.remove(notificationId);

notification.success(
<TxnNotification
message="Transaction completed successfully!"
blockExplorerLink={blockExplorerTxURL}
userOpExplorerLink={userOpTxnURL}
/>,
{
icon: "🎉",
duration: Infinity,
},
);
if (options?.onBlockConfirmation && transactionReceipt) options.onBlockConfirmation(transactionReceipt);
} catch (error: any) {
if (notificationId) {
notification.remove(notificationId);
}
console.error("⚡️ ~ file: useTransactor.ts ~ error", error);
const message = getParsedError(error);
notification.error(message);
notification.error("Please try again");
}

return userOpHash;
};

return result;
};
5 changes: 4 additions & 1 deletion packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"vercel:yolo": "vercel --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true"
},
"dependencies": {
"@alchemy/aa-accounts": "^1.2.3",
"@alchemy/aa-alchemy": "^1.2.3",
"@alchemy/aa-core": "^1.2.3",
"@ethersproject/providers": "^5.7.2",
"@heroicons/react": "^2.0.11",
"@rainbow-me/rainbowkit": "1.3.5",
Expand All @@ -31,7 +34,7 @@
"react-hot-toast": "^2.4.0",
"use-debounce": "^8.0.4",
"usehooks-ts": "^2.13.0",
"viem": "1.19.9",
"viem": "^1.21.0",
"wagmi": "1.4.12",
"zustand": "^4.1.2"
},
Expand Down
21 changes: 21 additions & 0 deletions packages/nextjs/utils/scaffold-eth/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,27 @@ export const NETWORKS_EXTRA_DATA: Record<string, ChainAttributes> = {
},
};

/**
* Gives the block explorer UserOp transaction URL, returns empty string if the network is a local chain
*/
export function getUserOpExplorerTxLink(chainId: number, txnHash: string) {
const chainNames = Object.keys(chains);

const targetChainArr = chainNames.filter(chainName => {
const wagmiChain = chains[chainName as keyof typeof chains];
return wagmiChain.id === chainId;
});

if (targetChainArr.length === 0) {
return "";
}

const targetChain = targetChainArr[0] as keyof typeof chains;
if (chains[targetChain].id === chains.baseGoerli.id)
return `https://app.jiffyscan.xyz/userOpHash/${txnHash}?network=base-testnet`;
return `https://app.jiffyscan.xyz/userOpHash/${txnHash}?network=${chains[targetChain].network}`;
}

/**
* Gives the block explorer transaction URL, returns empty string if the network is a local chain
*/
Expand Down
Loading