Skip to content

Commit

Permalink
feat: add userOps logic (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
yum0e authored Nov 8, 2023
1 parent 4e60f51 commit f5cd042
Show file tree
Hide file tree
Showing 19 changed files with 3,227 additions and 61 deletions.
4 changes: 2 additions & 2 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ src = "src"
out = "out"
libs = ["lib"]

bytecode_hash="none"
solc_version="0.8.21"
bytecode_hash = "none"
solc_version = "0.8.21"

[rpc_endpoints]
base_goerli = "https://goerli.base.org"
12 changes: 6 additions & 6 deletions contracts/src/SimpleAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ struct Signature {
uint256 s;
}

contract SimpleAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
struct Call {
address dest;
uint256 value;
bytes data;
}
struct Call {
address dest;
uint256 value;
bytes data;
}

contract SimpleAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
struct PublicKey {
bytes32 X;
bytes32 Y;
Expand Down
139 changes: 131 additions & 8 deletions contracts/test/SendUserOp.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,25 @@ import {Utils} from "test/Utils.sol";
// use Openzeppelin v4.8.1 to avoid `Failed to resolve file` error
import "account-abstraction/core/EntryPoint.sol";
import {SimpleAccountFactory} from "src/SimpleAccountFactory.sol";
import {SimpleAccount} from "src/SimpleAccount.sol";
import {SimpleAccount, Call} from "src/SimpleAccount.sol";

contract SendUserOpTest is Test {
using UserOperationLib for UserOperation;

EntryPoint public entryPoint;
SimpleAccountFactory public factory;
address bigqDevAddress = 0x061060a65146b3265C62fC8f3AE977c9B27260fF;

function setUp() public {
// setup fork
vm.createSelectFork("base_goerli");

entryPoint = new EntryPoint();
factory = new SimpleAccountFactory(entryPoint);
entryPoint = EntryPoint(
payable(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789)
);
factory = SimpleAccountFactory(
0xCD7DA03e26Fa4b7BcB43B4e5Ed65bE5cC9d844B0
);
}

/***
Expand All @@ -44,18 +49,19 @@ contract SendUserOpTest is Test {
);

function testSimpleUserOp() public {
// vm.etch(address(0x1234), type(P256Verifier).runtimeCode);
bytes32[2] memory publicKey = [
bytes32(
0x2a14910d2f67abb47fbaaaabbb73585e9cd25a0cfab7cb77901a5f189070c797
0xe7f630b0eb3594e991cfadbd4047cd5fecddf379b4a4458e3ea2b9566e09882a
),
bytes32(
0xad7ae3e847f90cb64392f3945f067ab3e0171831b07be8147c1e098621feff9c
0x3e9775709101f2b294ddec0536f0f260570b6f009bff2096995d3e1d986239dd
)
];

uint8 version = 1;
uint48 validUntil = 0;
bytes32 expectedUserOpHash = hex"7b3ae99bbc71fbac65fa6e95aeb48fc586d2a46d0381ff9b1110b2a0fa1ca0a4";
bytes32 expectedUserOpHash = hex"5c4665f4794a8f0edf8dd366539911aca9defe9aa54d06303cfe47cca393bd7b";
bytes memory challengeToSign = abi.encodePacked(
version,
validUntil,
Expand All @@ -68,8 +74,8 @@ contract SendUserOpTest is Test {
abi.encode( // signature
Utils.rawSignatureToSignature({
challenge: challengeToSign,
r: 0xb1c9a080371f3824da69b249fc27cbc6c152e05f0c7ba699879dc58e9808662b,
s: 0x63e3d5ad24f282481769f6537eb376a57b2e59da2f068751ba3c54bab23dd547
r: 0xf91f09739d7fe162dc7ad35f3117b6ed18181fa9ea817bf8ffdc8c03e004527e,
s: 0xd3b053205ced70fc403953092db25b64ac43d48ee2f30147a2134de7ead0c446
})
)
);
Expand Down Expand Up @@ -131,4 +137,121 @@ contract SendUserOpTest is Test {
uint256 validationData = account2.validateUserOp(op, hash, 0);
assertEq(validationData, 0);
}

function testUserOpWithInitCode() public {
// fake verifier
// P256Verifier verifier = new P256Verifier();
// vm.etch(address(0x1234), type(P256Verifier).runtimeCode);

bytes32[2] memory publicKey = [
bytes32(
0xe7f630b0eb3594e991cfadbd4047cd5fecddf379b4a4458e3ea2b9566e09882a
),
bytes32(
0x3e9775709101f2b294ddec0536f0f260570b6f009bff2096995d3e1d986239dd
)
];

uint8 version = 1;
uint48 validUntil = 0;
bytes32 expectedUserOpHash = hex"ed8154bc00355192a1f1f3a21ec5442bd05e3bb1c0c6ab089d6e138f88125d6a";
bytes memory challengeToSign = abi.encodePacked(
version,
validUntil,
expectedUserOpHash
);

bytes memory ownerSig = abi.encodePacked(
version,
validUntil,
abi.encode( // signature
Utils.rawSignatureToSignature({
challenge: challengeToSign,
r: 0xa82d88cd9b64be0e6014041c263e7c3dfe879432cf50366fd027018bf9a6f2e6,
s: 0x3457a4b5cdd4b0806d0bb609b2274e268e30b43f772473363aa7b2799119b0d1
})
)
);

uint256 salt = 123;

// account not deployed yet
// we want to test the initCode feature of UserOperation
SimpleAccount account = SimpleAccount(
payable(0x60587a33099742fb5D3e97174804e7Ab11A30118)
);
vm.deal(address(account), 1 ether);

// get init code
bytes memory initCode = abi.encodePacked(
address(factory),
abi.encodeCall(factory.createAccount, (publicKey, salt))
);

// send 42 wei to bigq dev
Call[] memory calls = new Call[](1);
calls[0] = Call({dest: bigqDevAddress, value: 42, data: hex""});

bytes memory callData = abi.encodeCall(
SimpleAccount.executeBatch,
(calls)
);

// dummy op
UserOperation memory op = UserOperation({
sender: address(0),
nonce: 0,
initCode: hex"",
callData: callData,
callGasLimit: 200_000,
verificationGasLimit: 2_342_060, // 2_000_000 + 150_000 + initCode gas
preVerificationGas: 65_000,
maxFeePerGas: 3e9,
maxPriorityFeePerGas: 1e9,
paymasterAndData: hex"",
// signature must be empty when calculating hash
signature: hex""
});

// fill data
op.sender = address(account);
op.initCode = initCode;

bytes32 hash = entryPoint.getUserOpHash(op);
assertEq(expectedUserOpHash, hash);

// add signature to op after calculating hash
op.signature = ownerSig;

// compute balance before userOp validation and execution
uint256 balanceBefore = bigqDevAddress.balance;

UserOperation[] memory ops = new UserOperation[](1);
ops[0] = op;

vm.expectEmit(true, true, true, false);
emit UserOperationEvent(
hash,
address(account),
address(0),
0, // These and following are not checked.
false,
0 gwei,
0
);
entryPoint.handleOps(ops, payable(address(account)));

// compute balance after userOp validation and execution
uint256 balanceAfter = bigqDevAddress.balance;
assertEq(balanceAfter - balanceBefore, 42);

// // code coverage can't handle indirect calls
// // call validateUserOp directly
// SimpleAccount account2 = new SimpleAccount(account.entryPoint());
// vm.store(address(account2), 0, 0); // set _initialized = 0
// account2.initialize(publicKey);
// vm.prank(address(entryPoint));
// uint256 validationData = account2.validateUserOp(op, hash, 0);
// assertEq(validationData, 0);
}
}
1 change: 1 addition & 0 deletions contracts/test/Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ library Utils {
'","origin":"http://localhost:3000","crossOrigin":false}'
)
);

uint256 challengeLocation = 23;
uint256 responseTypeLocation = 1;

Expand Down
2 changes: 1 addition & 1 deletion front/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1 @@
NEXT_PUBLIC_ALCHEMY_API_KEY=
STACKUP_BUNDLER_API_KEY=""
3 changes: 3 additions & 0 deletions front/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: false,
env: {
STACKUP_BUNDLER_API_KEY: process.env.STACKUP_BUNDLER_API_KEY,
},
webpack: (config) => {
config.resolve.fallback = { fs: false, net: false, tls: false };
return config;
Expand Down
2,607 changes: 2,607 additions & 0 deletions front/src/abis/factory.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions front/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Box, Flex } from "@radix-ui/themes";
import SessionList from "@/components/SessionList";
import WCInput from "@/components/WCInput";
import PassKey from "@/components/PassKey";
import { SendTransaction } from "@/components/SendTransaction";

export default async function Home() {
return (
Expand All @@ -10,6 +12,8 @@ export default async function Home() {
<br />
<WCInput />
<SessionList />
<PassKey />
<SendTransaction />
</Box>
</Flex>
);
Expand Down
6 changes: 5 additions & 1 deletion front/src/components/PassKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export default function PassKey() {
}

async function onGet() {
setCredential(await webauthn.get());
setCredential(
await webauthn.get(
`0x01000000000000${"ed8154bc00355192a1f1f3a21ec5442bd05e3bb1c0c6ab089d6e138f88125d6a"}`,
),
);
}

return (
Expand Down
24 changes: 16 additions & 8 deletions front/src/components/SendTransaction.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
"use client";

import { parseEther } from "viem";
import { Hex, PublicClient, createPublicClient, http, parseEther } from "viem";
import { useSendTransaction, useWaitForTransaction } from "wagmi";

import { stringify } from "../utils/stringify";
import { UserOpBuilder } from "@/libs/smart-wallet/service/userOps";
import { baseGoerli } from "viem/chains";
import { SmartWalletProvider } from "@/libs/smart-wallet/SmartWalletProvider";
import { smartWallet } from "@/libs/smart-wallet";

const builder = new UserOpBuilder(baseGoerli);

export function SendTransaction() {
const { data, error, isLoading, isError, sendTransaction } =
useSendTransaction();
const { data, error, isLoading, isError, sendTransaction } = useSendTransaction();
const {
data: receipt,
isLoading: isPending,
Expand All @@ -17,15 +22,18 @@ export function SendTransaction() {
return (
<>
<form
onSubmit={(e) => {
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const address = formData.get("address") as string;
const address = formData.get("address") as Hex;
const value = formData.get("value") as `${number}`;
sendTransaction({
to: address,
value: parseEther(value),

const res = await smartWallet.client.sendUserOperation({
to: address ?? "0x1878EA9134D500A3cEF3E89589ECA3656EECf48f",
value: value ?? BigInt(11),
});

console.log("res", res);
}}
>
<input name="address" placeholder="address" />
Expand Down
9 changes: 6 additions & 3 deletions front/src/libs/smart-wallet/config/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { fallback, http } from "viem";

const alchemy = http("https://eth-mainnet.g.alchemy.com/v2/...");
const infura = http("https://mainnet.infura.io/v3/...");
const publicRpc = http("https://goerli.base.org");
const localhost = http("http://localhost:8545");
const stackUpBundlerRpcUrl = http(
`https://api.stackup.sh/v1/node/${process.env.STACKUP_BUNDLER_API_KEY}`,
);

export const transport = fallback([alchemy, infura]);
export const transport = stackUpBundlerRpcUrl;
2 changes: 1 addition & 1 deletion front/src/libs/smart-wallet/hook/useSmartWalletHook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function useSmartWalletHook() {

smartWallet.client.watchEvent({
address: address,
onLogs: (logs) => {
onLogs: (logs: any) => {
console.log("logs", logs);
},
});
Expand Down
Loading

0 comments on commit f5cd042

Please sign in to comment.