Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ PIMLICO_API_KEY=
DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres"

# Persist passkey auth challenges
REDIS_URL="redis://localhost:6379"
REDIS_URL="redis://localhost:6379"
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ A lightweight, but fully featured passkey-based ethereum wallet built on Coinbas
- Connect to apps with Coinbase Wallet SDK, Mobile Wallet Protocol, and WalletConnect
- Supports most common wallet features (sign messages, sign transactions, etc.)
- Multichain support
- Import existing Smart Wallets via recovery EOA

### Planned

- [ ] Paymaster support
- [ ] Phone number login example
- [ ] No backend example
- [ ] Payment intents
- [ ] Account recovery

## Development

Expand Down Expand Up @@ -43,6 +45,20 @@ Run the Next.js app
pnpm run dev
```

### Fork testing

To run a self-bundler for testing, run the following command:

```
anvil --fork-url https://mainnet.base.org --block-time 2
```

```
docker compose up -d rundler
```



## Looking for the old repo?

https://github.com/stephancill/open-browser-wallet-old
5 changes: 5 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ declare global {
PIMLICO_API_KEY: string;
REDIS_URL: string;
DATABASE_URL: string;
/** Private key used to submit user operations directly to the entry point */
BUNDLER_PRIVATE_KEY: `0x${string}` | undefined;
/** EVM RPC URL for a specific chain */
[`NEXT_PUBLIC_EVM_RPC_URL_${number}`]: string | undefined;
[`EVM_BUNDLER_RPC_URL_${number}`]: string | undefined;
}
}
}
Expand Down
65 changes: 65 additions & 0 deletions patches/viem@2.21.14.patch
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
diff --git a/_esm/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.js b/_esm/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.js
index 500152b0f18158da7e08cc60c63a59f0782f9ceb..19c65b5cc45e4fa9a61e1feee54538a41a4d1dcb 100644
--- a/_esm/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.js
+++ b/_esm/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.js
@@ -31,7 +31,7 @@ import { toSmartAccount } from '../toSmartAccount.js';
* })
*/
export async function toCoinbaseSmartAccount(parameters) {
- const { address, client, owners, nonce = 0n } = parameters;
+ const { client, owners, nonce = 0n, signatureOwnerIndex = 0, address } = parameters
const entryPoint = {
abi: entryPoint06Abi,
address: entryPoint06Address,
@@ -88,6 +88,7 @@ export async function toCoinbaseSmartAccount(parameters) {
return '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000170000000000000000000000000000000000000000000000000000000000000001949fc7c88032b9fcb5f6efc7a7b8c63668eae9871b765e23123bb473ff57aa831a7c0d9276168ebcc29f2875a0239cffdf2a9cd1c2007c5c77c071db9264df1d000000000000000000000000000000000000000000000000000000000000002549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008a7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2273496a396e6164474850596759334b7156384f7a4a666c726275504b474f716d59576f4d57516869467773222c226f726967696e223a2268747470733a2f2f7369676e2e636f696e626173652e636f6d222c2263726f73734f726967696e223a66616c73657d00000000000000000000000000000000000000000000';
return wrapSignature({
signature: '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c',
+ ownerIndex: signatureOwnerIndex
});
},
async sign(parameters) {
@@ -100,6 +101,7 @@ export async function toCoinbaseSmartAccount(parameters) {
const signature = await sign({ hash, owner });
return wrapSignature({
signature,
+ ownerIndex: signatureOwnerIndex
});
},
async signMessage(parameters) {
@@ -113,6 +115,7 @@ export async function toCoinbaseSmartAccount(parameters) {
const signature = await sign({ hash, owner });
return wrapSignature({
signature,
+ ownerIndex: signatureOwnerIndex
});
},
async signTypedData(parameters) {
@@ -131,6 +134,7 @@ export async function toCoinbaseSmartAccount(parameters) {
const signature = await sign({ hash, owner });
return wrapSignature({
signature,
+ ownerIndex: signatureOwnerIndex
});
},
async signUserOperation(parameters) {
@@ -148,6 +152,7 @@ export async function toCoinbaseSmartAccount(parameters) {
const signature = await sign({ hash, owner });
return wrapSignature({
signature,
+ ownerIndex: signatureOwnerIndex
});
},
userOperation: {
diff --git a/_esm/account-abstraction/actions/bundler/prepareUserOperation.js b/_esm/account-abstraction/actions/bundler/prepareUserOperation.js
index 307d51f41d2c7b438ded03a636457b3c566112f3..9c30e3fa0b3fe5eb425ebddfa1fcc7a02ba828fe 100644
--- a/_esm/account-abstraction/actions/bundler/prepareUserOperation.js
Expand All @@ -24,3 +77,15 @@ index 03ae0983434fa2d70d302ec269c9c8dfb010a9b0..4a9f0fc5ecc623333e194dc6741c825e
const { schedule } = createBatchScheduler({
id: url_,
wait,
diff --git a/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.ts b/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.ts
index 7995fb62b6cf9eb732c9dc9905db2a702f2bcdb3..37c000c4a75d52e6eff360cffdb695728316fd2d 100644
--- a/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.ts
+++ b/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.ts
@@ -35,6 +35,7 @@ export type ToCoinbaseSmartAccountParameters = {
address?: Address | undefined
client: Client
owners: readonly OneOf<LocalAccount | WebAuthnAccount>[]
+ signatureOwnerIndex?: number | undefined
nonce?: bigint | undefined
}

16 changes: 8 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 0 additions & 8 deletions src/app/api/bundler/[...path]/route.ts

This file was deleted.

13 changes: 13 additions & 0 deletions src/app/api/bundler/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createProxyRequestHandler } from "@/lib/utils";

export const POST = createProxyRequestHandler(
(req) =>
`https://api.pimlico.io/v2/${req.nextUrl.searchParams.get("chainId")}/rpc?apikey=${process.env.PIMLICO_API_KEY}`,
{
searchParams: {
apikey: process.env.PIMLICO_API_KEY,
},
}
);

// export const POST = createProxyRequestHandler((req) => "http://localhost:3009");
167 changes: 167 additions & 0 deletions src/app/api/bundler/self/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* This route submits a user operation directly to the entry point
*/
import { NextRequest } from "next/server";
import {
createPublicClient,
createWalletClient,
decodeEventLog,
getAddress,
http,
} from "viem";
import { chains } from "@/lib/wagmi";
import { privateKeyToAccount } from "viem/accounts";
import { entryPoint06Abi, entryPoint06Address } from "viem/account-abstraction";
import { withAuth } from "@/lib/auth";
import { getTransportByChainId } from "@/lib/utils";
import { coinbaseSmartWalletAbi } from "@/abi/coinbaseSmartWallet";
import { Hex } from "webauthn-p256";
import { db } from "@/lib/db";

export const POST = withAuth(async (request: NextRequest, user) => {
const { method, params } = await request.json();
const chainId = request.nextUrl.searchParams.get("chainId");

if (!process.env.BUNDLER_PRIVATE_KEY) {
console.error("BUNDLER_PRIVATE_KEY is not set");
return new Response("Self-bundling is not supported", { status: 500 });
}

if (method !== "eth_sendUserOperationSelf") {
return new Response("Method not allowed", { status: 405 });
}

if (!chainId) {
return new Response("chainId search param is required", { status: 400 });
}

const [userOp, entryPoint] = params;

if (entryPoint !== entryPoint06Address) {
return new Response("Unsupported entry point", { status: 400 });
}

const chain = chains.find((chain) => chain.id === parseInt(chainId));

if (!chain) {
return new Response("Unsupported chain", { status: 400 });
}

const bundlerAccount = privateKeyToAccount(process.env.BUNDLER_PRIVATE_KEY);

const walletClient = createWalletClient({
chain,
transport: getTransportByChainId(chain.id),
account: bundlerAccount,
});

const publicClient = createPublicClient({
chain,
transport: getTransportByChainId(chain.id),
});

const handleOpsHash = await walletClient.writeContract({
abi: entryPoint06Abi,
address: entryPoint06Address,
functionName: "handleOps",
args: [[userOp], bundlerAccount.address],
});

// Check for ownerAdd events
const receipt = await publicClient.waitForTransactionReceipt({
hash: handleOpsHash,
});

const userOpHash = receipt.logs.reduce<Hex | undefined>((hash, log) => {
if (hash) return hash;
try {
const event = decodeEventLog({
abi: entryPoint06Abi,
data: log.data,
topics: log.topics,
});

if (
event.eventName === "UserOperationEvent" &&
getAddress(event.args.sender) === getAddress(user.walletAddress)
) {
return event.args.userOpHash;
}
} catch (error) {
// Ignore decoding errors
}
return undefined;
}, undefined);

if (!userOpHash) {
throw new Error("UserOperationEvent not found in transaction logs");
}

const addOwnerLogs = receipt.logs.filter((log) => {
try {
const event = decodeEventLog({
abi: coinbaseSmartWalletAbi,
data: log.data,
topics: log.topics,
});
return (
event.eventName === "AddOwner" &&
getAddress(log.address) === getAddress(user.walletAddress)
);
} catch (error) {
return false;
}
});

const addOwnerTransactions: {
transactionHash: Hex;
owner: Hex;
}[] = addOwnerLogs.map((log: any) => {
const event = decodeEventLog({
abi: coinbaseSmartWalletAbi,
data: log.data,
topics: log.topics,
});

if (event.eventName !== "AddOwner") {
throw new Error("Invalid event name");
}

return {
transactionHash: log.transactionHash,
owner: event.args.owner,
};
});

if (addOwnerTransactions.length > 0 && user.importedAccountData) {
const rows = await db
.updateTable("users")
.set({
importedAccountData: {
...user.importedAccountData,
addOwnerTransactions: [
...user.importedAccountData?.addOwnerTransactions,
...addOwnerTransactions,
],
},
})
.where("id", "=", user.id)
.returningAll()
.execute();

console.log(`Updated ${rows.length} `);
} else {
console.warn("No ownerAdd events found");
}

if (receipt.status !== "success") {
return Response.json({ error: "Transaction failed" }, { status: 400 });
}

return Response.json({
jsonrpc: "2.0",
id: 1,
// Note: eth_sendUserOperation should return the userOpHash, but we return handleOps tx hash instead because of a limitation in local testing
result: [handleOpsHash, userOpHash],
});
});
Loading