Skip to content

Commit a1c9e8c

Browse files
committed
feat: import wallet, walletconnect
1 parent dc64fa1 commit a1c9e8c

File tree

16 files changed

+902
-94
lines changed

16 files changed

+902
-94
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A lightweight, but fully featured passkey-based ethereum wallet built on Coinbas
88
- Connect to apps with Coinbase Wallet SDK, Mobile Wallet Protocol, and WalletConnect
99
- Supports most common wallet features (sign messages, sign transactions, etc.)
1010
- Multichain support
11+
- Import existing Smart Wallets via recovery EOA
1112

1213
### Planned
1314

@@ -44,6 +45,20 @@ Run the Next.js app
4445
pnpm run dev
4546
```
4647

48+
### Fork testing
49+
50+
To run a self-bundler for testing, run the following command:
51+
52+
```
53+
anvil --fork-url https://mainnet.base.org --block-time 2
54+
```
55+
56+
```
57+
docker compose up -d rundler
58+
```
59+
60+
61+
4762
## Looking for the old repo?
4863

4964
https://github.com/stephancill/open-browser-wallet-old

env.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ declare global {
55
PIMLICO_API_KEY: string;
66
REDIS_URL: string;
77
DATABASE_URL: string;
8+
/** Private key used to submit user operations directly to the entry point */
9+
BUNDLER_PRIVATE_KEY: `0x${string}` | undefined;
10+
/** EVM RPC URL for a specific chain */
11+
[`NEXT_PUBLIC_EVM_RPC_URL_${number}`]: string | undefined;
12+
[`EVM_BUNDLER_RPC_URL_${number}`]: string | undefined;
813
}
914
}
1015
}

src/app/api/bundler/[...path]/route.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/app/api/bundler/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createProxyRequestHandler } from "@/lib/utils";
2+
3+
export const POST = createProxyRequestHandler(
4+
(req) =>
5+
`https://api.pimlico.io/v2/${req.nextUrl.searchParams.get("chainId")}/rpc?apikey=${process.env.PIMLICO_API_KEY}`,
6+
{
7+
searchParams: {
8+
apikey: process.env.PIMLICO_API_KEY,
9+
},
10+
}
11+
);
12+
13+
// export const POST = createProxyRequestHandler((req) => "http://localhost:3009");

src/app/api/bundler/self/route.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* This route submits a user operation directly to the entry point
3+
*/
4+
import { NextRequest } from "next/server";
5+
import {
6+
createPublicClient,
7+
createWalletClient,
8+
decodeEventLog,
9+
getAddress,
10+
http,
11+
} from "viem";
12+
import { chains } from "@/lib/wagmi";
13+
import { privateKeyToAccount } from "viem/accounts";
14+
import { entryPoint06Abi, entryPoint06Address } from "viem/account-abstraction";
15+
import { withAuth } from "@/lib/auth";
16+
import { getTransportByChainId } from "@/lib/utils";
17+
import { coinbaseSmartWalletAbi } from "@/abi/coinbaseSmartWallet";
18+
import { Hex } from "webauthn-p256";
19+
import { db } from "@/lib/db";
20+
21+
export const POST = withAuth(async (request: NextRequest, user) => {
22+
const { method, params } = await request.json();
23+
const chainId = request.nextUrl.searchParams.get("chainId");
24+
25+
if (!process.env.BUNDLER_PRIVATE_KEY) {
26+
console.error("BUNDLER_PRIVATE_KEY is not set");
27+
return new Response("Self-bundling is not supported", { status: 500 });
28+
}
29+
30+
if (method !== "eth_sendUserOperationSelf") {
31+
return new Response("Method not allowed", { status: 405 });
32+
}
33+
34+
if (!chainId) {
35+
return new Response("chainId search param is required", { status: 400 });
36+
}
37+
38+
const [userOp, entryPoint] = params;
39+
40+
if (entryPoint !== entryPoint06Address) {
41+
return new Response("Unsupported entry point", { status: 400 });
42+
}
43+
44+
const chain = chains.find((chain) => chain.id === parseInt(chainId));
45+
46+
if (!chain) {
47+
return new Response("Unsupported chain", { status: 400 });
48+
}
49+
50+
const bundlerAccount = privateKeyToAccount(process.env.BUNDLER_PRIVATE_KEY);
51+
52+
const walletClient = createWalletClient({
53+
chain,
54+
transport: getTransportByChainId(chain.id),
55+
account: bundlerAccount,
56+
});
57+
58+
const publicClient = createPublicClient({
59+
chain,
60+
transport: getTransportByChainId(chain.id),
61+
});
62+
63+
const handleOpsHash = await walletClient.writeContract({
64+
abi: entryPoint06Abi,
65+
address: entryPoint06Address,
66+
functionName: "handleOps",
67+
args: [[userOp], bundlerAccount.address],
68+
});
69+
70+
// Check for ownerAdd events
71+
const receipt = await publicClient.waitForTransactionReceipt({
72+
hash: handleOpsHash,
73+
});
74+
75+
const userOpHash = receipt.logs.reduce<Hex | undefined>((hash, log) => {
76+
if (hash) return hash;
77+
try {
78+
const event = decodeEventLog({
79+
abi: entryPoint06Abi,
80+
data: log.data,
81+
topics: log.topics,
82+
});
83+
84+
if (
85+
event.eventName === "UserOperationEvent" &&
86+
getAddress(event.args.sender) === getAddress(user.walletAddress)
87+
) {
88+
return event.args.userOpHash;
89+
}
90+
} catch (error) {
91+
// Ignore decoding errors
92+
}
93+
return undefined;
94+
}, undefined);
95+
96+
if (!userOpHash) {
97+
throw new Error("UserOperationEvent not found in transaction logs");
98+
}
99+
100+
const addOwnerLogs = receipt.logs.filter((log) => {
101+
try {
102+
const event = decodeEventLog({
103+
abi: coinbaseSmartWalletAbi,
104+
data: log.data,
105+
topics: log.topics,
106+
});
107+
return (
108+
event.eventName === "AddOwner" &&
109+
getAddress(log.address) === getAddress(user.walletAddress)
110+
);
111+
} catch (error) {
112+
return false;
113+
}
114+
});
115+
116+
const addOwnerTransactions: {
117+
transactionHash: Hex;
118+
owner: Hex;
119+
}[] = addOwnerLogs.map((log: any) => {
120+
const event = decodeEventLog({
121+
abi: coinbaseSmartWalletAbi,
122+
data: log.data,
123+
topics: log.topics,
124+
});
125+
126+
if (event.eventName !== "AddOwner") {
127+
throw new Error("Invalid event name");
128+
}
129+
130+
return {
131+
transactionHash: log.transactionHash,
132+
owner: event.args.owner,
133+
};
134+
});
135+
136+
if (addOwnerTransactions.length > 0 && user.importedAccountData) {
137+
const rows = await db
138+
.updateTable("users")
139+
.set({
140+
importedAccountData: {
141+
...user.importedAccountData,
142+
addOwnerTransactions: [
143+
...user.importedAccountData?.addOwnerTransactions,
144+
...addOwnerTransactions,
145+
],
146+
},
147+
})
148+
.where("id", "=", user.id)
149+
.returningAll()
150+
.execute();
151+
152+
console.log(`Updated ${rows.length} `);
153+
} else {
154+
console.warn("No ownerAdd events found");
155+
}
156+
157+
if (receipt.status !== "success") {
158+
return Response.json({ error: "Transaction failed" }, { status: 400 });
159+
}
160+
161+
return Response.json({
162+
jsonrpc: "2.0",
163+
id: 1,
164+
// Note: eth_sendUserOperation should return the userOpHash, but we return handleOps tx hash instead because of a limitation in local testing
165+
result: [handleOpsHash, userOpHash],
166+
});
167+
});

src/app/api/sign-up/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,20 @@ export async function POST(req: NextRequest) {
8787
),
8888
});
8989

90-
// TODO: Simulate all userOps to see which one results in deployed contract, for now we assume it's the first one (might be possible to do offline)
9190
const deployUserOp = (
9291
await getUserOpsFromTransaction({
9392
bundlerClient,
9493
// @ts-ignore -- idk
9594
client: baseClient,
9695
transactionHash: deployTransaction.transactionHash,
9796
})
98-
).find((userOp) => userOp.userOperation.initCode);
97+
).find((userOp) => {
98+
return (
99+
userOp.userOperation.initCode &&
100+
userOp.userOperation.initCode !== "0x" &&
101+
getAddress(userOp.userOperation.sender) === walletAddress
102+
);
103+
});
99104

100105
if (!deployUserOp) {
101106
return Response.json(

0 commit comments

Comments
 (0)