Skip to content

Commit

Permalink
feat: merkle-io integration (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xSulpiride committed Feb 29, 2024
1 parent 43af1d3 commit c358f7c
Show file tree
Hide file tree
Showing 19 changed files with 273 additions and 41 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Or follow the steps below:
"account": []
},
"bundleGasLimitMarkup": 25000, # optional, adds some amount of additional gas to a bundle tx
"relayingMode": "classic"; # optional, allows to switch to Flashbots Builder api if set to "flashbots", see packages/executor/src/interfaces.ts for more
"relayingMode": "classic"; # optional, "flashbots" for Flashbots Builder API, "merkle" for Merkle.io
"bundleInterval": 10000, # bundle creation interval
"bundleSize": 4, # optional, max size of a bundle, 4 userops by default
"pvgMarkup": 0 # optional, adds some gas on top of estimated PVG
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "1.0.39-alpha",
"version": "1.0.40-alpha",
"stream": "true",
"command": {
"version": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "root",
"private": true,
"version": "1.0.39-alpha",
"version": "1.0.40-alpha",
"engines": {
"node": ">=18.0.0"
},
Expand Down
8 changes: 4 additions & 4 deletions packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "api",
"version": "1.0.39-alpha",
"version": "1.0.40-alpha",
"description": "The API module of Etherspot bundler client",
"author": "Etherspot",
"homepage": "https://https://github.com/etherspot/skandha#readme",
Expand Down Expand Up @@ -35,13 +35,13 @@
"class-transformer": "0.5.1",
"class-validator": "0.14.0",
"ethers": "5.7.2",
"executor": "^1.0.39-alpha",
"executor": "^1.0.40-alpha",
"fastify": "4.14.1",
"monitoring": "^1.0.39-alpha",
"monitoring": "^1.0.40-alpha",
"pino": "8.11.0",
"pino-pretty": "10.0.0",
"reflect-metadata": "0.1.13",
"types": "^1.0.39-alpha"
"types": "^1.0.40-alpha"
},
"devDependencies": {
"@types/connect": "3.4.35"
Expand Down
14 changes: 7 additions & 7 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cli",
"version": "1.0.39-alpha",
"version": "1.0.40-alpha",
"description": "> TODO: description",
"author": "zincoshine <psramanuj@gmail.com>",
"homepage": "https://https://github.com/etherspot/skandha#readme",
Expand Down Expand Up @@ -38,15 +38,15 @@
"@libp2p/peer-id-factory": "2.0.1",
"@libp2p/prometheus-metrics": "1.1.3",
"@multiformats/multiaddr": "12.1.3",
"api": "^1.0.39-alpha",
"db": "^1.0.39-alpha",
"executor": "^1.0.39-alpha",
"api": "^1.0.40-alpha",
"db": "^1.0.40-alpha",
"executor": "^1.0.40-alpha",
"find-up": "5.0.0",
"got": "12.5.3",
"js-yaml": "4.1.0",
"monitoring": "^1.0.39-alpha",
"node": "^1.0.39-alpha",
"types": "^1.0.39-alpha",
"monitoring": "^1.0.40-alpha",
"node": "^1.0.40-alpha",
"types": "^1.0.40-alpha",
"yargs": "17.6.2"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "db",
"version": "1.0.39-alpha",
"version": "1.0.40-alpha",
"description": "The DB module of Etherspot bundler client",
"author": "Etherspot",
"homepage": "https://github.com/etherspot/etherspot-bundler#readme",
Expand Down Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"@chainsafe/ssz": "0.10.1",
"@farcaster/rocksdb": "5.5.0",
"types": "^1.0.39-alpha"
"types": "^1.0.40-alpha"
},
"devDependencies": {
"@types/rocksdb": "3.0.1",
Expand Down
8 changes: 4 additions & 4 deletions packages/executor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "executor",
"version": "1.0.39-alpha",
"version": "1.0.40-alpha",
"description": "The Relayer module of Etherspot bundler client",
"author": "Etherspot",
"homepage": "https://https://github.com/etherspot/skandha#readme",
Expand Down Expand Up @@ -34,8 +34,8 @@
"@flashbots/ethers-provider-bundle": "0.6.2",
"async-mutex": "0.4.0",
"ethers": "5.7.2",
"monitoring": "^1.0.39-alpha",
"params": "^1.0.39-alpha",
"types": "^1.0.39-alpha"
"monitoring": "^1.0.40-alpha",
"params": "^1.0.40-alpha",
"types": "^1.0.40-alpha"
}
}
9 changes: 9 additions & 0 deletions packages/executor/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@ export class Config {
)
);

conf.merkleApiURL = String(
fromEnvVar(
network,
"MERKLE_API_URL",
conf.merkleApiURL || bundlerDefaultConfigs.merkleApiURL
)
);

conf.skipBundleValidation = Boolean(
fromEnvVar(
network,
Expand Down Expand Up @@ -415,6 +423,7 @@ const bundlerDefaultConfigs: BundlerConfig = {
relayingMode: "classic",
pvgMarkup: 0,
gasFeeInSimulation: false,
merkleApiURL: "https://pool.merkle.io",
skipBundleValidation: false,
userOpGasLimit: 25000000,
bundleGasLimit: 25000000,
Expand Down
10 changes: 10 additions & 0 deletions packages/executor/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ export class Executor {
);
this.logger.info(`${this.networkName}: [X] FLASHBOTS BUIDLER API`);
}
if (this.networkConfig.relayingMode === "merkle") {
if (
!this.networkConfig.rpcEndpointSubmit ||
!this.networkConfig.merkleApiURL
)
throw Error(
"If you want to use Merkle API, please set RPC url in 'rpcEndpointSubmit' and API url in `merkleApiURL` in config file"
);
this.logger.info(`${this.networkName}: [X] Merkle API`);
}

if (this.networkConfig.conditionalTransactions) {
this.logger.info(`${this.networkName}: [x] CONDITIONAL TRANSACTIONS`);
Expand Down
2 changes: 2 additions & 0 deletions packages/executor/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ export interface NetworkConfig {
pvgMarkup: number;
// add gas fee in simulated transactions (may be required for some rpc providers)
gasFeeInSimulation: boolean;
// api url of Merkle.io (by default https://pool.merkle.io)
merkleApiURL: string;
// skips bundle validation
skipBundleValidation: boolean;
userOpGasLimit: number; // 25kk by default
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./classic";
export * from "./flashbots";
export * from "./merkle";
194 changes: 194 additions & 0 deletions packages/executor/src/services/BundlingService/relayers/merkle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import path from "node:path";
import { providers } from "ethers";
import { PerChainMetrics } from "monitoring/lib";
import { Logger, NetworkName } from "types/lib";
import { IEntryPoint__factory } from "types/lib/executor/contracts";
import { AccessList, fetchJson } from "ethers/lib/utils";
import { MempoolEntryStatus } from "types/lib/executor";
import { Config } from "../../../config";
import { Bundle, NetworkConfig } from "../../../interfaces";
import { MempoolService } from "../../MempoolService";
import { ReputationService } from "../../ReputationService";
import { estimateBundleGasLimit } from "../utils";
import { now } from "../../../utils";
import { BaseRelayer } from "./base";

export class MerkleRelayer extends BaseRelayer {
private submitTimeout = 2 * 60 * 1000; // 2 minutes

constructor(
logger: Logger,
chainId: number,
network: NetworkName,
provider: providers.JsonRpcProvider,
config: Config,
networkConfig: NetworkConfig,
mempoolService: MempoolService,
reputationService: ReputationService,
metrics: PerChainMetrics | null
) {
super(
logger,
chainId,
network,
provider,
config,
networkConfig,
mempoolService,
reputationService,
metrics
);
}

async sendBundle(bundle: Bundle): Promise<void> {
const availableIndex = this.getAvailableRelayerIndex();
if (availableIndex == null) return;

const relayer = this.relayers[availableIndex];
const mutex = this.mutexes[availableIndex];

const { entries, storageMap } = bundle;
if (!bundle.entries.length) return;

await mutex.runExclusive(async () => {
const beneficiary = await this.selectBeneficiary(relayer);
const entryPoint = entries[0]!.entryPoint;
const entryPointContract = IEntryPoint__factory.connect(
entryPoint,
this.provider
);

const txRequest = entryPointContract.interface.encodeFunctionData(
"handleOps",
[entries.map((entry) => entry.userOp), beneficiary]
);

const transactionRequest: providers.TransactionRequest = {
to: entryPoint,
data: txRequest,
type: 2,
maxPriorityFeePerGas: bundle.maxPriorityFeePerGas,
maxFeePerGas: bundle.maxFeePerGas,
gasLimit: estimateBundleGasLimit(
this.networkConfig.bundleGasLimitMarkup,
bundle.entries
),
chainId: this.provider._network.chainId,
nonce: await relayer.getTransactionCount(),
};

if (this.networkConfig.eip2930) {
const { storageMap } = bundle;
const addresses = Object.keys(storageMap);
if (addresses.length) {
const accessList: AccessList = [];
for (const address of addresses) {
const storageKeys = storageMap[address];
if (typeof storageKeys == "object") {
accessList.push({
address,
storageKeys: Object.keys(storageKeys),
});
}
}
transactionRequest.accessList = accessList;
}
}

try {
// checking for tx revert
await relayer.estimateGas(transactionRequest);
} catch (err) {
this.logger.error(err);
await this.mempoolService.removeAll(entries);
return;
}

this.logger.debug(transactionRequest, "Merkle: Submitting");
const merkleProvider = new providers.JsonRpcProvider(
this.networkConfig.rpcEndpointSubmit
);
const signedRawTx = await relayer.signTransaction(transactionRequest);
const params = !this.networkConfig.conditionalTransactions
? [signedRawTx]
: [signedRawTx, { knownAccounts: storageMap }];
try {
const hash = await merkleProvider.send(
"eth_sendRawTransaction",
params
);
this.logger.debug(`Bundle submitted: ${hash}`);
this.logger.debug(
`User op hashes ${entries.map((entry) => entry.userOpHash)}`
);
await this.mempoolService.setStatus(
entries,
MempoolEntryStatus.Submitted,
hash
);
await this.waitForTransaction(hash);
} catch (err) {
await this.mempoolService.setStatus(entries, MempoolEntryStatus.New);
await this.handleUserOpFail(entries, err);
}
});
}

async waitForTransaction(hash: string): Promise<boolean> {
const txStatusUrl = new URL(
path.join("transaction", hash),
this.networkConfig.merkleApiURL
).toString();
const submitStart = now();
return new Promise<boolean>((resolve, reject) => {
let lock = false;
const handler = async (): Promise<void> => {
this.logger.debug("Merkle: Fetching tx status");
if (now() - submitStart > this.submitTimeout) return reject("timeout");
if (lock) return;
lock = true;
try {
// https://docs.merkle.io/private-pool/wallets/transaction-status
const status = await fetchJson(txStatusUrl);
this.logger.debug(status, `Merkle: ${hash}`);
switch (status.status) {
case "nonce_too_low":
case "not_enough_funds":
case "base_fee_low":
case "low_priority_fee":
case "not_enough_gas":
case "sanctioned":
case "gas_limit_too_high":
case "invalid_signature":
case "nonce_gapped":
reject("rebundle"); // the bundle can be submitted again, no need to delete userops
break;
default: {
const response = await this.provider.getTransaction(hash);
if (response == null) {
this.logger.debug(
"Transaction not found yet. Trying again in 2 seconds"
);
setTimeout(() => handler(), 2000); // fetch status again in 2 seconds
lock = false;
return;
}
this.logger.debug("Transaction is found");
resolve(true); // transaction is found
}
}
} catch (err: any) {
this.logger.debug(err, "Could not fetch transaction status");
// transaction is not found, but not necessarily failed
if (err.status === 400) {
setTimeout(() => handler(), 2000); // fetch status again in 2 seconds
lock = false;
return;
}
reject(err);
}
};
void handler();
});
}
}
Loading

0 comments on commit c358f7c

Please sign in to comment.