zksync-ethers
Version:
A Web3 library for interacting with the ZkSync Layer 2 scaling solution.
245 lines • 12.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InteropClient = exports.resolveGateway = exports.GATEWAY_PRESETS = void 0;
// src/interop-client.ts
const ethers_1 = require("ethers");
const interop_utils_1 = require("./interop-utils");
const utils_1 = require("./utils");
const utils = __importStar(require("./utils"));
/**
* Gateway presets for mainnet and testnet.
* @public
*/
exports.GATEWAY_PRESETS = {
testnet: {
chainId: 32657n,
rpcUrl: 'https://rpc.era-gateway-testnet.zksync.dev/',
},
mainnet: {
chainId: 9075n,
rpcUrl: 'https://rpc.era-gateway-mainnet.zksync.dev/',
},
};
/**
* Resolve a Gateway provider + chainId from a flexible config.
*
* Precedence:
* 1) explicit `gwProvider` + `gwChainId`
* 2) `env` ('testnet' | 'mainnet') with optional overrides
* 3) `env: 'local'` requires `gwRpcUrl` and `gwChainId` (or explicit provider + chainId)
*
* @public
* @param config - Flexible Gateway configuration. Defaults to the **testnet** preset.
* @returns `{ gwProvider, gwChainId }` ready to use.
* @throws If `env: 'local'` is selected without enough information to construct a provider.
* @example
* ```ts
* const { gwProvider, gwChainId } = resolveGateway(); // testnet by default
* const onMainnet = resolveGateway({ env: 'mainnet' });
* const local = resolveGateway({ env: 'local', gwRpcUrl: 'http://localhost:3250', gwChainId: 506n });
* ```
*/
function resolveGateway(config = {}) {
if (config.gwProvider &&
config.gwChainId !== null &&
config.gwChainId !== undefined) {
return { gwProvider: config.gwProvider, gwChainId: config.gwChainId };
}
const env = config.env ?? 'testnet';
// Presets
if (env === 'testnet' || env === 'mainnet') {
const preset = exports.GATEWAY_PRESETS[env];
const gwChainId = config.gwChainId ?? preset.chainId;
const rpcUrl = config.gwRpcUrl ?? preset.rpcUrl;
const gwProvider = config.gwProvider ?? new ethers_1.JsonRpcProvider(rpcUrl);
return { gwProvider, gwChainId };
}
// Local
if (env === 'local') {
const gwProvider = config.gwProvider ??
(config.gwRpcUrl ? new ethers_1.JsonRpcProvider(config.gwRpcUrl) : undefined);
if (!gwProvider ||
config.gwChainId === null ||
config.gwChainId === undefined) {
throw new Error('Gateway config for env="local" requires gwRpcUrl and gwChainId (or explicit gwProvider + gwChainId).');
}
return { gwProvider, gwChainId: config.gwChainId };
}
throw new Error('Invalid gateway configuration');
}
exports.resolveGateway = resolveGateway;
class InteropClient {
constructor(opts = {}) {
const { gwProvider, gwChainId } = resolveGateway(opts.gateway);
this.gwProvider = gwProvider;
this.gwChainId = gwChainId;
}
/**
* Send a message via the L1Messenger on the source chain and return the `Sent` bundle.
*
* @param srcWallet - Wallet connected to the source L2.
* @param message - Bytes or string; strings are UTF-8 encoded.
*
* @returns txHash — The transaction hash.
*/
async sendMessage(srcWallet, message) {
const messenger = new ethers_1.ethers.Contract(utils.L1_MESSENGER_ADDRESS, utils.L1_MESSENGER, srcWallet);
const bytes = typeof message === 'string' ? ethers_1.ethers.toUtf8Bytes(message) : message;
const tx = await messenger.sendToL1(bytes);
await (await srcWallet.provider.getTransaction(tx.hash)).wait();
return { txHash: tx.hash };
}
/**
* Verify inclusion of a previously sent message on a target chain.
* This is a read-only check against the target's L2MessageVerification contract.
*
* @param params.txHash - Returned txHash from `sendMessage`.
* @param params.srcProvider - Provider for the source chain (to fetch proof nodes + batch details).
* @param params.targetChain - Provider for the target chain (to read interop roots + call verifier). This can be any chain that imports the Gateway roots.
* @param params.includeProofInputs - If true, include raw proof positioning info in the result (for debugging).
* @param params.timeoutMs - Max time to wait for the interop root on the target chain (ms). Default: 120_000.
* @returns InteropResult — compact verification outcome (plus optional proof inputs).
*/
async verifyMessage(params) {
const { txHash, srcProvider, targetChain, includeProofInputs, timeoutMs } = params;
const { srcChainId, l1BatchNumber, l2MessageIndex, msgData, gatewayProof, gwBlock, l2ToL1LogIndex, l1BatchTxIndex, interopRoot, } = await this.getVerificationArgs({
txHash,
srcProvider,
targetChain,
includeProofInputs: true,
timeoutMs,
});
const verifier = new ethers_1.ethers.Contract(utils_1.L2_MESSAGE_VERIFICATION_ADDRESS, utils_1.L2_MESSAGE_VERIFICATION_ABI, targetChain);
const included = await verifier.proveL2MessageInclusionShared(srcChainId, l1BatchNumber, l2MessageIndex, msgData, gatewayProof);
if (!included)
throw new Error('Verification failed.');
const result = {
source: {
chainId: srcChainId,
txHash,
sender: msgData.sender,
messageHash: ethers_1.ethers.keccak256(msgData.data),
},
interopRoot: interopRoot,
verified: included,
};
if (includeProofInputs) {
result.proof = {
l1BatchNumber,
l2MessageIndex,
l1BatchTxIndex: l1BatchTxIndex,
l2ToL1LogIndex: l2ToL1LogIndex,
gwBlockNumber: gwBlock,
};
}
return result;
}
/**
* Get the input arguments for proveL2MessageInclusionShared to verify a previously sent message on a target chain.
*
* @param params.txHash - Returned txHash from `sendMessage`.
* @param params.srcProvider - Provider for the source chain (to fetch proof nodes + batch details).
* @param params.targetChain - Provider for the target chain (to read interop roots + call verifier). This can be any chain that imports the Gateway roots.
* @param params.includeProofInputs - If true, include raw proof positioning info in the result (for debugging).
* @param params.timeoutMs - Max time to wait for the interop root on the target chain (ms). Default: 120_000.
* @returns ProveL2MessageInclusionSharedArgs & { interopRoot: string; gwBlock?: bigint; l2ToL1LogIndex?: number; } - An object with all the required input arguments to verify a previously sent message using the proveL2MessageInclusionShared method on the target's L2MessageVerification contract.
*/
async getVerificationArgs(params) {
const { txHash, srcProvider, targetChain, includeProofInputs, timeoutMs = 120000, } = params;
const phase = await this.getMessageStatus(srcProvider, txHash);
if (phase !== 'EXECUTED') {
switch (phase) {
case 'QUEUED':
throw new Error('Status: Pending → Transaction is included on L2 but the batch has not yet been committed. Not ready for verification.');
case 'SENDING':
throw new Error('Status: Included → Batch has been committed and is being sent to Gateway. Not ready for verification.');
case 'PROVING':
throw new Error('Status: Verified → Batch proof is being generated and submitted. Not ready for verification.');
case 'FAILED':
throw new Error('Status: Failed → Transaction did not verify successfully.');
case 'REJECTED':
throw new Error('Status: Failed → Transaction was rejected by the sequencer.');
default:
throw new Error('Status: Unknown → Transaction status could not be determined.');
}
}
const tx = await srcProvider.getTransaction(txHash);
const finalizedRcpt = await tx.wait();
const sender = tx.from;
const { l2ToL1LogIndex, messageSentInContract } = (0, interop_utils_1.findInteropLogIndex)(finalizedRcpt, sender);
if (l2ToL1LogIndex < 0) {
throw new Error('Interop log not found in source receipt for L1Messenger');
}
const log = finalizedRcpt.logs.filter(log => (0, utils_1.isAddressEq)(log.address, utils.L1_MESSENGER_ADDRESS) &&
log.topics[0] === ethers_1.ethers.id('L1MessageSent(address,bytes32,bytes)'))[l2ToL1LogIndex];
const messageHex = ethers_1.ethers.AbiCoder.defaultAbiCoder().decode(['bytes'], log.data)[0];
if (!messageHex) {
throw new Error('Missing message value on matched L1Messenger log');
}
const l1BatchNumber = finalizedRcpt.l1BatchNumber;
const l1BatchTxIndex = finalizedRcpt.l1BatchTxIndex;
const { nodes, proofId } = await (0, interop_utils_1.getGatewayProof)(srcProvider, txHash, l2ToL1LogIndex);
const gwBlock = await (0, interop_utils_1.getGwBlockForBatch)(BigInt(l1BatchNumber), srcProvider, this.gwProvider);
const interopRoot = await (0, interop_utils_1.waitForGatewayInteropRoot)(this.gwChainId, targetChain, gwBlock, { timeoutMs });
const srcChainId = Number((await srcProvider.getNetwork()).chainId);
const result = {
srcChainId,
l1BatchNumber,
l2MessageIndex: proofId,
msgData: {
txNumberInBatch: l1BatchTxIndex,
sender: messageSentInContract ? tx.to : sender,
data: messageHex,
},
gatewayProof: nodes,
};
if (includeProofInputs) {
return { ...result, gwBlock, l2ToL1LogIndex, l1BatchTxIndex, interopRoot };
}
return result;
}
/**
* Check the current lifecycle phase of a sent message on the source chain.
*
* @param srcProvider - Source chain provider (supports `getTransactionDetails`).
* @param txHash - Transaction hash returned by {@link sendMessage}.
* @returns Phase classification:
* 'QUEUED' | 'SENDING' | 'PROVING' | 'EXECUTED' | 'FAILED' | 'REJECTED' | 'UNKNOWN'
*/
async getMessageStatus(srcProvider, txHash) {
const d = await srcProvider.getTransactionDetails(txHash);
if (!d)
return { phase: 'UNKNOWN', message: 'No details available' };
return (0, interop_utils_1.classifyPhase)({
status: d.status,
ethCommitTxHash: d.ethCommitTxHash ?? null,
ethProveTxHash: d.ethProveTxHash ?? null,
ethExecuteTxHash: d.ethExecuteTxHash ?? null,
});
}
}
exports.InteropClient = InteropClient;
//# sourceMappingURL=interop-client.js.map