Skip to content

Commit

Permalink
Chain Agnostic Transaction Manager (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored Sep 16, 2024
1 parent a268613 commit 62dc76b
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 128 deletions.
23 changes: 23 additions & 0 deletions examples/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@ import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { UserOptions } from "../src";

interface ScriptEnv {
nearAccountId: string;
nearAccountPrivateKey?: string;
pimlicoKey: string;
}

export async function loadEnv(): Promise<ScriptEnv> {
const { NEAR_ACCOUNT_ID, NEAR_ACCOUNT_PRIVATE_KEY, PIMLICO_KEY } =
process.env;
if (!NEAR_ACCOUNT_ID) {
throw new Error("Must provide env var NEAR_ACCOUNT_ID");
}
if (!PIMLICO_KEY) {
throw new Error("Must provide env var PIMLICO_KEY");
}

return {
nearAccountId: NEAR_ACCOUNT_ID,
nearAccountPrivateKey: NEAR_ACCOUNT_PRIVATE_KEY,
pimlicoKey: PIMLICO_KEY,
};
}

export async function loadArgs(): Promise<UserOptions> {
return yargs(hideBin(process.argv))
.option("usePaymaster", {
Expand Down
30 changes: 0 additions & 30 deletions examples/load-manager.ts

This file was deleted.

25 changes: 15 additions & 10 deletions examples/send-tx.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import dotenv from "dotenv";
import { ethers } from "ethers";
import { loadArgs } from "./cli";
import { loadArgs, loadEnv } from "./cli";
import { TransactionManager } from "../src";
import { setupAdapter } from "near-ca";

dotenv.config();

async function main(): Promise<void> {
const { mpcContractId, recoveryAddress, usePaymaster } = await loadArgs();

const txManager = await TransactionManager.fromChainId({
chainId: 11155111,
const [
{ pimlicoKey, nearAccountId, nearAccountPrivateKey },
{ mpcContractId, recoveryAddress, usePaymaster },
] = await Promise.all([loadEnv(), loadArgs()]);
const chainId = 11155111;
const txManager = await TransactionManager.create({
ethRpc: "https://rpc2.sepolia.org",
nearAdapter: await setupAdapter({
accountId: process.env.NEAR_ACCOUNT_ID!,
accountId: nearAccountId,
// This must be set to transact. Otherwise, instance will be read-only
privateKey: process.env.NEAR_ACCOUNT_PRIVATE_KEY,
privateKey: nearAccountPrivateKey,
mpcContractId,
}),
pimlicoKey: process.env.PIMLICO_KEY!,
pimlicoKey,
});

const transactions = [
Expand All @@ -29,13 +32,14 @@ async function main(): Promise<void> {
},
];
// Add Recovery if safe not deployed & recoveryAddress was provided.
if (txManager.safeNotDeployed && recoveryAddress) {
if (!(await txManager.safeDeployed(chainId)) && recoveryAddress) {
const recoveryTx = txManager.addOwnerTx(recoveryAddress);
// This would happen (sequentially) after the userTx, but all executed in a single
transactions.push(recoveryTx);
}

const unsignedUserOp = await txManager.buildTransaction({
chainId,
transactions,
usePaymaster,
});
Expand All @@ -47,6 +51,7 @@ async function main(): Promise<void> {
const gasCost = ethers.parseEther("0.01");
// Whenever not using paymaster, or on value transfer, the Safe must be funded.
const sufficientFunded = await txManager.safeSufficientlyFunded(
chainId,
transactions,
usePaymaster ? 0n : gasCost
);
Expand All @@ -61,7 +66,7 @@ async function main(): Promise<void> {
const signature = await txManager.signTransaction(safeOpHash);

console.log("Executing UserOp...");
const userOpReceipt = await txManager.executeTransaction({
const userOpReceipt = await txManager.executeTransaction(chainId, {
...unsignedUserOp,
signature,
});
Expand Down
17 changes: 15 additions & 2 deletions src/lib/bundler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// TODO: Ethers dependency is only for Generic HTTP Provider
import { ethers } from "ethers";
import {
GasPrices,
Expand All @@ -9,13 +10,25 @@ import {
import { PLACEHOLDER_SIG } from "../util.js";
import { toHex } from "viem";

function bundlerUrl(chainId: number, apikey: string): string {
return `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${apikey}`;
}

export class Erc4337Bundler {
provider: ethers.JsonRpcProvider;
entryPointAddress: string;
apiKey: string;
chainId: number;

constructor(bundlerUrl: string, entryPointAddress: string) {
constructor(entryPointAddress: string, apiKey: string, chainId: number) {
this.entryPointAddress = entryPointAddress;
this.provider = new ethers.JsonRpcProvider(bundlerUrl);
this.apiKey = apiKey;
this.chainId = chainId;
this.provider = new ethers.JsonRpcProvider(bundlerUrl(chainId, apiKey));
}

client(chainId: number): ethers.JsonRpcProvider {
return new ethers.JsonRpcProvider(bundlerUrl(chainId, this.apiKey));
}

async getPaymasterData(
Expand Down
36 changes: 16 additions & 20 deletions src/lib/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,15 @@ export class ContractSuite {
this.entryPoint = entryPoint;
}

static async init(provider: ethers.JsonRpcProvider): Promise<ContractSuite> {
static async init(): Promise<ContractSuite> {
// TODO - this is a cheeky hack.
const provider = new ethers.JsonRpcProvider("https://rpc2.sepolia.org");
const safeDeployment = (fn: DeploymentFunction): Promise<ethers.Contract> =>
getDeployment(fn, { provider, version: "1.4.1" });
const m4337Deployment = async (
fn: DeploymentFunction
): Promise<ethers.Contract> => {
try {
return await getDeployment(fn, { provider, version: "0.3.0" });
} catch (error: unknown) {
console.warn((error as Error).message, "using v0.2.0");
return getDeployment(fn, { provider, version: "0.2.0" });
}
return getDeployment(fn, { provider, version: "0.3.0" });
};
// Need this first to get entryPoint address
const m4337 = await m4337Deployment(getSafe4337ModuleDeployment);
Expand Down Expand Up @@ -130,22 +127,21 @@ export class ContractSuite {
}

async getOpHash(unsignedUserOp: UserOperation): Promise<Hash> {
const {
factory,
factoryData,
verificationGasLimit,
callGasLimit,
maxPriorityFeePerGas,
maxFeePerGas,
} = unsignedUserOp;
return this.m4337.getOperationHash({
...unsignedUserOp,
initCode: unsignedUserOp.factory
? ethers.solidityPacked(
["address", "bytes"],
[unsignedUserOp.factory, unsignedUserOp.factoryData]
)
initCode: factory
? ethers.solidityPacked(["address", "bytes"], [factory, factoryData])
: "0x",
accountGasLimits: packGas(
unsignedUserOp.verificationGasLimit,
unsignedUserOp.callGasLimit
),
gasFees: packGas(
unsignedUserOp.maxPriorityFeePerGas,
unsignedUserOp.maxFeePerGas
),
accountGasLimits: packGas(verificationGasLimit, callGasLimit),
gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas),
paymasterAndData: packPaymasterData(unsignedUserOp),
signature: PLACEHOLDER_SIG,
});
Expand Down
Loading

0 comments on commit 62dc76b

Please sign in to comment.