Skip to content

Commit

Permalink
Viem Bundler (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored Sep 17, 2024
1 parent 5c2f528 commit cf9e610
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 38 deletions.
2 changes: 1 addition & 1 deletion examples/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function loadArgs(): Promise<UserOptions> {
.option("mpcContractId", {
type: "string",
description: "Address of the mpc (signing) contract",
default: "v1.signer-prod.testnet",
default: "v1.signer-dev.testnet",
})
.help()
.alias("help", "h").argv;
Expand Down
2 changes: 1 addition & 1 deletion examples/send-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async function main(): Promise<void> {
process.exit(0); // soft exit with warning!
}

console.log("Signing with Near...");
console.log("Signing with Near at", txManager.mpcContractId);
const signature = await txManager.signTransaction(safeOpHash);

console.log("Executing UserOp...");
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,9 @@
"dependencies": {
"@safe-global/safe-deployments": "^1.37.0",
"@safe-global/safe-modules-deployments": "^2.2.0",
"ethers": "^6.13.1",
"near-api-js": "^5.0.0",
"near-ca": "^0.5.2",
"viem": "^2.16.5",
"yargs": "^17.7.2"
"viem": "^2.16.5"
},
"devDependencies": {
"@types/jest": "^29.5.12",
Expand All @@ -57,6 +55,7 @@
"dotenv": "^16.4.5",
"eslint": "^9.6.0",
"eslint-plugin-import": "^2.30.0",
"ethers": "^6.13.1",
"jest": "^29.7.0",
"prettier": "^3.3.2",
"ts-jest": "^29.1.5",
Expand Down
131 changes: 98 additions & 33 deletions src/lib/bundler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
// TODO: Ethers dependency is only for Generic HTTP Provider
import { ethers } from "ethers";
import { toHex } from "viem";
import {
Address,
createPublicClient,
Hash,
http,
PublicClient,
rpcSchema,
toHex,
Transport,
RpcError,
HttpRequestError,
} from "viem";

import {
GasPrices,
Expand All @@ -15,19 +24,43 @@ function bundlerUrl(chainId: number, apikey: string): string {
return `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${apikey}`;
}

type BundlerRpcSchema = [
{
Method: "pm_sponsorUserOperation";
Parameters: [UnsignedUserOperation, Address];
ReturnType: PaymasterData;
},
{
Method: "eth_sendUserOperation";
Parameters: [UserOperation, Address];
ReturnType: Hash;
},
{
Method: "pimlico_getUserOperationGasPrice";
Parameters: [];
ReturnType: GasPrices;
},
{
Method: "eth_getUserOperationReceipt";
Parameters: [Hash];
ReturnType: UserOperationReceipt | null;
},
];

export class Erc4337Bundler {
provider: ethers.JsonRpcProvider;
entryPointAddress: string;
client: PublicClient<Transport, undefined, undefined, BundlerRpcSchema>;
entryPointAddress: Address;
apiKey: string;
chainId: number;

constructor(entryPointAddress: string, apiKey: string, chainId: number) {
constructor(entryPointAddress: Address, apiKey: string, chainId: number) {
this.entryPointAddress = entryPointAddress;
this.apiKey = apiKey;
this.chainId = chainId;
this.provider = new ethers.JsonRpcProvider(
bundlerUrl(chainId, this.apiKey)
);
this.client = createPublicClient({
transport: http(bundlerUrl(chainId, this.apiKey)),
rpcSchema: rpcSchema<BundlerRpcSchema>(),
});
}

async getPaymasterData(
Expand All @@ -38,39 +71,39 @@ export class Erc4337Bundler {
// TODO: Keep this option out of the bundler
if (usePaymaster) {
console.log("Requesting paymaster data...");
const data = this.provider.send("pm_sponsorUserOperation", [
{ ...rawUserOp, signature: PLACEHOLDER_SIG },
this.entryPointAddress,
]);
return data;
return handleRequest<PaymasterData>(() =>
this.client.request({
method: "pm_sponsorUserOperation",
params: [
{ ...rawUserOp, signature: PLACEHOLDER_SIG },
this.entryPointAddress,
],
})
);
}
return defaultPaymasterData(safeNotDeployed);
}

async sendUserOperation(userOp: UserOperation): Promise<string> {
try {
const userOpHash = await this.provider.send("eth_sendUserOperation", [
userOp,
this.entryPointAddress,
]);
return userOpHash;
} catch (err: unknown) {
const error = (err as ethers.JsonRpcError).error;
throw new Error(`Failed to send user op with: ${error.message}`);
}
async sendUserOperation(userOp: UserOperation): Promise<Hash> {
return handleRequest<Hash>(() =>
this.client.request({
method: "eth_sendUserOperation",
params: [userOp, this.entryPointAddress],
})
);
// throw new Error(`Failed to send user op with: ${error.message}`);
}

async getGasPrice(): Promise<GasPrices> {
return this.provider.send("pimlico_getUserOperationGasPrice", []);
}

async _getUserOpReceiptInner(
userOpHash: string
): Promise<UserOperationReceipt | null> {
return this.provider.send("eth_getUserOperationReceipt", [userOpHash]);
return handleRequest<GasPrices>(() =>
this.client.request({
method: "pimlico_getUserOperationGasPrice",
params: [],
})
);
}

async getUserOpReceipt(userOpHash: string): Promise<UserOperationReceipt> {
async getUserOpReceipt(userOpHash: Hash): Promise<UserOperationReceipt> {
let userOpReceipt: UserOperationReceipt | null = null;
while (!userOpReceipt) {
// Wait 2 seconds before checking the status again
Expand All @@ -79,6 +112,38 @@ export class Erc4337Bundler {
}
return userOpReceipt;
}

private async _getUserOpReceiptInner(
userOpHash: Hash
): Promise<UserOperationReceipt | null> {
return handleRequest<UserOperationReceipt | null>(() =>
this.client.request({
method: "eth_getUserOperationReceipt",
params: [userOpHash],
})
);
}
}

async function handleRequest<T>(clientMethod: () => Promise<T>): Promise<T> {
try {
return await clientMethod();
} catch (error) {
if (error instanceof HttpRequestError) {
if (error.status === 401) {
throw new Error("Unauthorized request. Please check your API key.");
} else {
console.error(
`Request failed with status ${error.status}: ${error.message}`
);
}
} else if (error instanceof RpcError) {
throw new Error(`Failed to send user op with: ${error.message}`);
}
throw new Error(
`Unexpected error ${error instanceof Error ? error.message : String(error)}`
);
}
}

// TODO(bh2smith) Should probably get reasonable estimates here:
Expand Down
4 changes: 4 additions & 0 deletions src/tx-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export class TransactionManager {
return this.nearAdapter.address;
}

get mpcContractId(): string {
return this.nearAdapter.mpcContract.contract.contractId;
}

async getBalance(chainId: number): Promise<bigint> {
return await getClient(chainId).getBalance({ address: this.address });
}
Expand Down
15 changes: 15 additions & 0 deletions tests/lib.bundler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Erc4337Bundler } from "../src/lib/bundler";
describe("Safe Pack", () => {
const entryPoint = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";

it("Unauthorized Requests Failure", async () => {
const unauthorizedBundler = new Erc4337Bundler(
entryPoint,
"invalidAPI key",
11155111
);
await expect(() => unauthorizedBundler.getGasPrice()).rejects.toThrow(
"Unauthorized request. Please check your API key."
);
});
});

0 comments on commit cf9e610

Please sign in to comment.