Skip to content

Commit

Permalink
feat: merkle-io integration
Browse files Browse the repository at this point in the history
  • Loading branch information
0xSulpiride committed Feb 20, 2024
1 parent fb203bb commit fb897eb
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 5 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,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
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
)
);

// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!conf.whitelistedEntities) {
conf.whitelistedEntities = bundlerDefaultConfigs.whitelistedEntities;
Expand Down Expand Up @@ -391,6 +399,7 @@ const bundlerDefaultConfigs: BundlerConfig = {
relayingMode: "classic",
pvgMarkup: 0,
gasFeeInSimulation: false,
merkleApiURL: "https://pool.merkle.io",
};

const NETWORKS_ENV = (): string[] | undefined => {
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;
}

export type BundlerConfig = Omit<
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();
});
}
}
18 changes: 15 additions & 3 deletions packages/executor/src/services/BundlingService/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { mergeStorageMap } from "../../utils/mergeStorageMap";
import { getAddr, wait } from "../../utils";
import { MempoolEntry } from "../../entities/MempoolEntry";
import { IRelayingMode } from "./interfaces";
import { ClassicRelayer, FlashbotsRelayer } from "./relayers";
import { ClassicRelayer, FlashbotsRelayer, MerkleRelayer } from "./relayers";

export class BundlingService {
private mutex: Mutex;
Expand Down Expand Up @@ -64,6 +64,18 @@ export class BundlingService {
this.reputationService,
this.metrics
);
} else if (relayingMode === "merkle") {
this.relayer = new MerkleRelayer(
this.logger,
this.chainId,
this.network,
this.provider,
this.config,
this.networkConfig,
this.mempoolService,
this.reputationService,
this.metrics
);
} else {
this.relayer = new ClassicRelayer(
this.logger,
Expand Down Expand Up @@ -341,7 +353,7 @@ export class BundlingService {
if (!entries.length) {
this.logger.debug("No new entries");
return;
};
}
// remove entries from mempool if submitAttempts are greater than maxAttemps
const invalidEntries = entries.filter(
(entry) => entry.submitAttempts >= this.maxSubmitAttempts
Expand All @@ -358,7 +370,7 @@ export class BundlingService {
if (!entries.length) {
this.logger.debug("No entries left");
return;
};
}
const gasFee = await getGasFee(
this.chainId,
this.provider,
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/executor/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type RelayingMode = "flashbots" | "classic";
export type RelayingMode = "merkle" | "flashbots" | "classic";
export interface SendBundleReturn {
transactionHash: string;
userOpHashes: string[];
Expand Down

0 comments on commit fb897eb

Please sign in to comment.