Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat: add working end-to-end flow #34

Merged
merged 4 commits into from
Nov 15, 2023
Merged
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions contracts/script/bash/deploy_factory.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# forge script DeploySimpleAccountFactory --private-key $PRIVATE_KEY --rpc-url https://goerli.base.org/ --etherscan-api-key $ETHERSCAN_API_KEY --verify --slow --broadcast
32 changes: 23 additions & 9 deletions contracts/src/SimpleAccountFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ pragma solidity ^0.8.12;

import "openzeppelin-contracts/contracts/utils/Create2.sol";
import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import "src/SimpleAccount.sol";

struct User {
uint256 id;
bytes32[2] publicKey;
address account;
}

/**
* A sample factory contract for SimpleAccount
* A UserOperations "initCode" holds the address of the factory, and a method call (to createAccount, in this sample factory).
Expand All @@ -15,23 +20,33 @@ import "src/SimpleAccount.sol";
contract SimpleAccountFactory {
SimpleAccount public immutable accountImplem;
IEntryPoint public immutable entryPoint;
bytes32 public constant SALT = keccak256("hocuspocusxyz");

mapping(uint256 id => User user) public users;

constructor(IEntryPoint _entryPoint) {
entryPoint = _entryPoint;
accountImplem = new SimpleAccount(_entryPoint);
}

function saveUser(uint256 id, bytes32[2] memory publicKey) external {
users[id] = User(id, publicKey, this.getAddress(publicKey));
}

function getUser(uint256 id) external view returns (User memory) {
return users[id];
}

/**
* Create an account, and return its address.
* Returns the address even if the account is already deployed.
* Note that during UserOperation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation.
*/
function createAccount(
bytes32[2] memory publicKey,
uint256 salt
) public payable returns (SimpleAccount) {
address addr = getAddress(publicKey, salt);
bytes32[2] memory publicKey
) external payable returns (SimpleAccount) {
address addr = getAddress(publicKey);

// Prefund the account with msg.value
if (msg.value > 0) {
Expand All @@ -47,7 +62,7 @@ contract SimpleAccountFactory {
return
SimpleAccount(
payable(
new ERC1967Proxy{salt: bytes32(salt)}(
new ERC1967Proxy{salt: SALT}(
address(accountImplem),
abi.encodeCall(SimpleAccount.initialize, (publicKey))
)
Expand All @@ -59,12 +74,11 @@ contract SimpleAccountFactory {
* Calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(
bytes32[2] memory publicKey,
uint256 salt
bytes32[2] memory publicKey
) public view returns (address) {
return
Create2.computeAddress(
bytes32(salt),
SALT,
keccak256(
abi.encodePacked(
type(ERC1967Proxy).creationCode,
Expand Down
38 changes: 10 additions & 28 deletions contracts/test/SendUserOp.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ contract SendUserOpTest is Test {
payable(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789)
);
factory = SimpleAccountFactory(
0xCD7DA03e26Fa4b7BcB43B4e5Ed65bE5cC9d844B0
0x7236f1BB9BE463437261AA3f74008Bdf76d4ceC1
);
}

Expand All @@ -49,7 +49,6 @@ contract SendUserOpTest is Test {
);

function testSimpleUserOp() public {
// vm.etch(address(0x1234), type(P256Verifier).runtimeCode);
bytes32[2] memory publicKey = [
bytes32(
0xe7f630b0eb3594e991cfadbd4047cd5fecddf379b4a4458e3ea2b9566e09882a
Expand All @@ -61,7 +60,7 @@ contract SendUserOpTest is Test {

uint8 version = 1;
uint48 validUntil = 0;
bytes32 expectedUserOpHash = hex"5c4665f4794a8f0edf8dd366539911aca9defe9aa54d06303cfe47cca393bd7b";
bytes32 expectedUserOpHash = hex"72fe91f1b68f75ce391ac973c52d8c525356199dbc5bef6c7bc6f8e2308ead87";
bytes memory challengeToSign = abi.encodePacked(
version,
validUntil,
Expand All @@ -74,15 +73,13 @@ contract SendUserOpTest is Test {
abi.encode( // signature
Utils.rawSignatureToSignature({
challenge: challengeToSign,
r: 0xf91f09739d7fe162dc7ad35f3117b6ed18181fa9ea817bf8ffdc8c03e004527e,
s: 0xd3b053205ced70fc403953092db25b64ac43d48ee2f30147a2134de7ead0c446
r: 0x813d6d26f828f855a570eff45308c8bde0d5a417d0f3e07484b0d90efef19382,
s: 0x282e1b0004a893bf6d22fec8cf97190f591ffec78a3b0ab3ea45dfe9fc035d29
})
)
);

uint256 salt = 123;

SimpleAccount account = factory.createAccount(publicKey, salt);
SimpleAccount account = factory.createAccount(publicKey);
vm.deal(address(account), 1 ether);

// dummy op
Expand Down Expand Up @@ -139,10 +136,6 @@ contract SendUserOpTest is Test {
}

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

bytes32[2] memory publicKey = [
bytes32(
0xe7f630b0eb3594e991cfadbd4047cd5fecddf379b4a4458e3ea2b9566e09882a
Expand All @@ -154,7 +147,7 @@ contract SendUserOpTest is Test {

uint8 version = 1;
uint48 validUntil = 0;
bytes32 expectedUserOpHash = hex"ed8154bc00355192a1f1f3a21ec5442bd05e3bb1c0c6ab089d6e138f88125d6a";
bytes32 expectedUserOpHash = hex"ed8c67fc3b6e6eb7b867c90201f13fa61a45f8eeaea636057443e64f56013f31";
bytes memory challengeToSign = abi.encodePacked(
version,
validUntil,
Expand All @@ -167,25 +160,23 @@ contract SendUserOpTest is Test {
abi.encode( // signature
Utils.rawSignatureToSignature({
challenge: challengeToSign,
r: 0xa82d88cd9b64be0e6014041c263e7c3dfe879432cf50366fd027018bf9a6f2e6,
s: 0x3457a4b5cdd4b0806d0bb609b2274e268e30b43f772473363aa7b2799119b0d1
r: 0xbced80a2f0cc4f977e145107744da4475b7f127e9d0ec73c77785279874ca8dc,
s: 0x68597940751e2f8cfd60433a9a22fdb4fe704a3148e47ee69b7a5909ab4b3948
})
)
);

uint256 salt = 123;

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

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

// send 42 wei to bigq dev
Expand Down Expand Up @@ -244,14 +235,5 @@ contract SendUserOpTest is Test {
// 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);
}
}
26 changes: 19 additions & 7 deletions contracts/test/SimpleFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity ^0.8.13;
import "forge-std/Test.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 {SimpleAccountFactory, User} from "src/SimpleAccountFactory.sol";
import {SimpleAccount} from "src/SimpleAccount.sol";

contract SimpleFactoryTest is Test {
Expand All @@ -23,19 +23,31 @@ contract SimpleFactoryTest is Test {
// random public key
bytes32[2] memory pubKey = [bytes32(uint256(1)), bytes32(uint256(1))];

// random salt
uint256 salt = 123;

// deploy the account
SimpleAccount account = factory.createAccount{value: 0}(pubKey, salt);
SimpleAccount account = factory.createAccount{value: 0}(pubKey);

// deploy again - should return the same address
SimpleAccount account2 = factory.createAccount{value: 2}(pubKey, salt);
SimpleAccount account2 = factory.createAccount{value: 2}(pubKey);
assertEq(address(account), address(account2));
assertEq(entrypoint.getDepositInfo(address(account)).deposit, 2);

// compute address manually
address expectedAddress = factory.getAddress(pubKey, salt);
address expectedAddress = factory.getAddress(pubKey);
assertEq(address(account), expectedAddress);
}

function testUserCreation() public {
// random public key
bytes32[2] memory pubKey = [bytes32(uint256(1)), bytes32(uint256(1))];

// create user
factory.saveUser(1, pubKey);

// get user
User memory user = factory.getUser(1);
assertEq(user.id, 1);
assertEq(user.publicKey[0], pubKey[0]);
assertEq(user.publicKey[1], pubKey[1]);
assertEq(user.account, factory.getAddress(pubKey));
}
}
1 change: 1 addition & 0 deletions front/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
STACKUP_BUNDLER_API_KEY=""
RELAYER_PRIVATE_KEY=""
2 changes: 2 additions & 0 deletions front/.env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
STACKUP_BUNDLER_API_KEY="9e0f13b10e97cbc4609b356ba3cb10f066225df054eb8dfd4bf0ba54aec9f650"
RELAYER_PRIVATE_KEY="0xa641a081f7667ae1761619d690911324847f968cbfc4e9d03291a8b3b5f0caa1"
2 changes: 2 additions & 0 deletions front/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ To learn more about [Next.js](https://nextjs.org) or [wagmi](https://wagmi.sh),
- [RainbowKit Documentation](https://rainbowkit.com/docs/introduction) – learn more about RainbowKit (configuration, theming, advanced usage, etc).
- [Next.js Documentation](https://nextjs.org/docs) learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

https://ngrok.com/docs/getting-started/
9 changes: 4 additions & 5 deletions front/next.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/** @type {import('next').NextConfig} */

const fs = require('fs');
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 };
config.resolve.fallback = { fs: false, net: false, tls: false };
return config;
},
};
}
2,607 changes: 0 additions & 2,607 deletions front/src/abis/factory.json

This file was deleted.

19 changes: 2 additions & 17 deletions front/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
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";
import HomePage from "@/components/HomePage";

export default async function Home() {
return (
<Flex align="center" direction="column">
<Box>
<h1>Hocus Pocus XYZ</h1>
<br />
<WCInput />
<SessionList />
<PassKey />
<SendTransaction />
</Box>
</Flex>
);
return <HomePage />;
}
59 changes: 59 additions & 0 deletions front/src/app/api/price/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { stringify } from "viem";
import fs from "fs";

export async function GET(req: Request) {
const searchParams = new URL(req.url).searchParams;
const ids = searchParams.get("ids");
const currencies = searchParams.get("currencies");

const { isCached, priceCached } = getFromCache(ids, currencies);
if (isCached) {
return Response.json(JSON.parse(priceCached));
}

const price = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=${currencies}`,
);

const priceJson = await price.json();

saveToCache(ids, currencies, priceJson);

return Response.json(JSON.parse(stringify(priceJson)));
}

function saveToCache(
ids: string | null,
currencies: string | null,
priceJson: Response | null,
): void {
const key = `${ids}-${currencies}`;
// create cache folder if not exist
if (!fs.existsSync("./cache")) {
fs.mkdirSync("./cache");
}
// save to local files
fs.writeFileSync(`./cache/${key}.json`, JSON.stringify({ ...priceJson, timestamp: Date.now() }));
}

function getFromCache(
ids: string | null,
currencies: string | null,
): { isCached: boolean; priceCached: string } {
const key = `${ids}-${currencies}`;
// retrieve from local files
try {
const priceCached = fs.readFileSync(`./cache/${key}.json`, "utf8");
const priceCachedJson = JSON.parse(priceCached);

const timestamp = priceCachedJson.timestamp;
const now = Date.now();
// cache for 1 minute
if (now - timestamp > 1 * 60 * 1000) {
return { isCached: false, priceCached: "" };
}
return { isCached: true, priceCached };
} catch (e) {
return { isCached: false, priceCached: "" };
}
}
21 changes: 21 additions & 0 deletions front/src/app/api/users/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PUBLIC_CLIENT } from "@/constants/client";
import { FACTORY_ABI, FACTORY_ADDRESS } from "@/constants/factory";
import { Hex, stringify, toHex } from "viem";

export async function GET(_req: Request, { params }: { params: { id: Hex } }) {
const { id } = params;
if (!id) {
return Response.json(JSON.parse(stringify({ error: "id is required" })));
}

const user = await PUBLIC_CLIENT.readContract({
address: FACTORY_ADDRESS,
abi: FACTORY_ABI,
functionName: "getUser",
args: [BigInt(id)],
});

const balance = await PUBLIC_CLIENT.getBalance({ address: user.account });

return Response.json(JSON.parse(stringify({ ...user, id: toHex(user.id), balance })));
}
Loading