Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e90a976
feat: add interop to ethers adapter
vasyl-ivanchuk Feb 5, 2026
b89e586
refactor: interop implementation
vasyl-ivanchuk Feb 6, 2026
c27f90c
refactor: interop finalization service
vasyl-ivanchuk Feb 6, 2026
9abbd1d
refactor: improve client ensureAddresses
vasyl-ivanchuk Feb 6, 2026
1a21536
refactor: interop
vasyl-ivanchuk Feb 6, 2026
5ca72f1
feat: add preconfigured interop chains
vasyl-ivanchuk Feb 6, 2026
f819b47
test: unit tests for interop
vasyl-ivanchuk Feb 6, 2026
7baf1ca
fix: ensure and approve tokens
vasyl-ivanchuk Feb 9, 2026
dff3768
fix: improve errors handling
vasyl-ivanchuk Feb 9, 2026
7c82fe9
fix: improve processing
vasyl-ivanchuk Feb 9, 2026
985008a
Merge branch 'main' into vi/ethers-adapter-interop
vasyl-ivanchuk Feb 9, 2026
09e6435
fix: make InteropRouteStrategy preflight required
vasyl-ivanchuk Feb 9, 2026
3097a0e
fix: chains registration
vasyl-ivanchuk Feb 9, 2026
ef6ab1d
Merge branch 'main' into vi/ethers-adapter-interop
vasyl-ivanchuk Feb 10, 2026
95a0d8d
fix: add finalization service
vasyl-ivanchuk Feb 10, 2026
e970512
refactor: optimize attributes generation
vasyl-ivanchuk Feb 10, 2026
e181612
refactor: addresses and topics
vasyl-ivanchuk Feb 10, 2026
a883c42
fix: logs fetching approach
vasyl-ivanchuk Feb 10, 2026
b97b86f
refactor: extract logic to indirect interop helpers
vasyl-ivanchuk Feb 10, 2026
49f9ff8
fix: deposit example
vasyl-ivanchuk Feb 10, 2026
d032f34
fix: switch to working zk os version
vasyl-ivanchuk Feb 10, 2026
1cdfdfd
fix: remove rich wallets file
vasyl-ivanchuk Feb 10, 2026
098f10d
fix: remove chains registry
vasyl-ivanchuk Feb 12, 2026
cbffbed
fix: get logs batching
vasyl-ivanchuk Feb 12, 2026
c6a39ce
Merge branch 'main' of github.com:matter-labs/zksync-js into vi/ether…
dutterbutter Feb 12, 2026
6b6545f
chore: resolve conflicts
dutterbutter Feb 12, 2026
5a32be6
chore: use v30.2 zksync-server version
dutterbutter Feb 12, 2026
23803fe
fix: improve naming
vasyl-ivanchuk Feb 13, 2026
f0b4ba8
Merge branch 'main' into vi/ethers-adapter-interop
dutterbutter Feb 13, 2026
a88d0d6
fix: process txOverrides nonce in interop
vasyl-ivanchuk Feb 13, 2026
0fa73ea
feat: check for protocol version for interop
vasyl-ivanchuk Feb 13, 2026
3a2251d
fix: tests
vasyl-ivanchuk Feb 13, 2026
d1cc38f
fix: move types and resolvers to separate files
vasyl-ivanchuk Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions examples/ethers/interop/contracts/Greeting.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Greeting {
string public message;
bytes public lastSender;

constructor() {
message = "initialized";
lastSender = "";
}

// ERC-7930 receiveMessage function
// Receive messages coming from other chains.
function receiveMessage(
bytes32, // Unique identifier
bytes calldata sender, // ERC-7930 address
bytes calldata payload
) external payable returns (bytes4) {
// Check that it is coming from a trusted caller - interop handler.
require(
msg.sender == address(0x000000000000000000000000000000000001000E),
"message must come from interop handler"
);

// Decode the payload to extract the greeting message

string memory newMessage = abi.decode(payload, (string));
message = newMessage;
lastSender = sender;

// Return the function selector to acknowledge receipt
return this.receiveMessage.selector;
}
}
106 changes: 106 additions & 0 deletions examples/ethers/interop/erc20-transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Contract, JsonRpcProvider, Wallet, parseUnits, formatUnits } from 'ethers';
import { createEthersClient, createEthersSdk } from '../../../src/adapters/ethers';
import { type Address, type Hex } from '../../../src/core';
import { FORMAL_ETH_ADDRESS, L2_NATIVE_TOKEN_VAULT_ADDRESS } from '../../../src/core/constants';
import { IERC20ABI, L2NativeTokenVaultABI } from '../../../src/core/abi';
import { getErc20TokenAddress } from './utils';

const L1_RPC = process.env.L1_RPC ?? 'http://127.0.0.1:8545';
const SRC_L2_RPC = process.env.SRC_L2_RPC ?? 'http://127.0.0.1:3050';
const DST_L2_RPC = process.env.DST_L2_RPC ?? 'http://127.0.0.1:3051';
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const AMOUNT_RAW = process.env.AMOUNT ?? '100';

async function main() {
if (!PRIVATE_KEY) throw new Error('Set PRIVATE_KEY in env');

const l1 = new JsonRpcProvider(L1_RPC);
const l2Source = new JsonRpcProvider(SRC_L2_RPC);
const l2Destination = new JsonRpcProvider(DST_L2_RPC);

const client = await createEthersClient({
l1,
l2: l2Source,
signer: new Wallet(PRIVATE_KEY),
});
const sdk = createEthersSdk(client);

const walletA = new Wallet(PRIVATE_KEY, l2Source);
const walletB = new Wallet(PRIVATE_KEY, l2Destination);
const me = (await walletA.getAddress()) as Address;
const recipientOnDst = walletB.address as Address;

const amountToSend = parseUnits(AMOUNT_RAW, 18);

console.log('Sender address:', me);

// ---- Deploy ERC20 token on source chain ----
console.log('=== DEPLOYING ERC20 TOKEN ===');
const tokenAAddress = await getErc20TokenAddress({ signer: walletA });
console.log('Token deployed at:', tokenAAddress);
console.log('Token registration will be handled by the SDK');

const tokenA = new Contract(tokenAAddress, IERC20ABI, l2Source);
const balanceA = await tokenA.balanceOf(walletA.address);
console.log('WalletA token balance:', formatUnits(balanceA, 18), 'TEST');

const params = {
dstChain: l2Destination,
actions: [
{
type: 'sendErc20' as const,
token: tokenAAddress,
to: recipientOnDst,
amount: amountToSend,
},
],
unbundling: { by: recipientOnDst },
};

// QUOTE: Build and return the summary.
const quote = await sdk.interop.quote(params);
console.log('QUOTE:', quote);

// PREPARE: Build plan without executing.
const prepared = await sdk.interop.prepare(params);
console.log('PREPARE:', prepared);

// CREATE: Execute the source-chain step(s), wait for each tx receipt to confirm (status != 0).
const created = await sdk.interop.create(params);
console.log('CREATE:', created);

// WAIT: Waits until the L2->L1 proof is available on source and the interop root
// becomes available on the destination chain. It returns the proof payload needed
// to execute the bundle later.
const finalizationInfo = await sdk.interop.wait(created, {
pollMs: 5_000,
timeoutMs: 30 * 60 * 1_000,
});
console.log('Bundle is finalized on source; root available on destination.');

// FINALIZE: Execute on destination and block until done.
// finalize() calls executeBundle(...) on the destination chain,
// waits for the tx to mine, then returns { bundleHash, dstExecTxHash }.
const finalizationResult = await sdk.interop.finalize(finalizationInfo);
console.log('FINALIZE RESULT:', finalizationResult);

const assetId = (await sdk.tokens.assetIdOfL2(tokenAAddress)) as Hex;
console.log('Asset ID:', assetId);

const ntvDst = new Contract(L2_NATIVE_TOKEN_VAULT_ADDRESS, L2NativeTokenVaultABI, l2Destination);
const tokenBAddress = (await ntvDst.tokenAddress(assetId)) as Address;

if (tokenBAddress === FORMAL_ETH_ADDRESS) {
console.log('Token is not registered on destination yet.');
} else {
const tokenB = new Contract(tokenBAddress, IERC20ABI, l2Destination);
const balanceB = await tokenB.balanceOf(recipientOnDst);
console.log('Destination token address:', tokenBAddress);
console.log('Destination balance:', balanceB.toString());
}
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
98 changes: 98 additions & 0 deletions examples/ethers/interop/remote-call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { AbiCoder, Contract, JsonRpcProvider, Wallet } from 'ethers';
import { createEthersClient, createEthersSdk } from '../../../src/adapters/ethers';
import { getGreetingTokenAddress } from './utils';

const L1_RPC = process.env.L1_RPC ?? 'http://127.0.0.1:8545';
const SRC_L2_RPC = process.env.SRC_L2_RPC ?? 'http://127.0.0.1:3050';
const DST_L2_RPC = process.env.DST_L2_RPC ?? 'http://127.0.0.1:3051';
const PRIVATE_KEY = process.env.PRIVATE_KEY;

const GREETING_ABI = ['function message() view returns (string)'] as const;

async function main() {
if (!PRIVATE_KEY) throw new Error('Set your PRIVATE_KEY in env');

const l1 = new JsonRpcProvider(L1_RPC);
const l2Source = new JsonRpcProvider(SRC_L2_RPC);
const l2Destination = new JsonRpcProvider(DST_L2_RPC);

const signer = new Wallet(PRIVATE_KEY, l2Source);
const client = await createEthersClient({
l1,
l2: l2Source,
signer,
});
const sdk = createEthersSdk(client);
const dstSigner = new Wallet(PRIVATE_KEY, l2Destination);

// ---- Deploy Greeter on destination ----
console.log('=== DEPLOYING GREETER ON DESTINATION ===');
const initialGreeting = 'hello from destination';
const greeterAddress = await getGreetingTokenAddress({
signer: dstSigner,
greeting: initialGreeting,
});
console.log('Greeter deployed at:', greeterAddress);

const greeter = new Contract(greeterAddress, GREETING_ABI, l2Destination);
const greetingBefore = (await greeter.message()) as string;
console.log('Greeting before:', greetingBefore);
const newGreeting = 'hello from example!';
const data = AbiCoder.defaultAbiCoder().encode(['string'], [newGreeting]) as `0x${string}`;

const params = {
dstChain: l2Destination,
actions: [
{
type: 'call' as const,
to: greeterAddress,
data: data,
},
],
// Optional bundle-level execution constraints:
// execution: { only: someExecAddress },
// unbundling: { by: someUnbundlerAddress },
};

// QUOTE: Build and return the summary.
const quote = await sdk.interop.quote(params);
console.log('QUOTE:', quote);

// PREPARE: Build plan without executing.
const prepared = await sdk.interop.prepare(params);
console.log('PREPARE:', prepared);

// CREATE: Execute the source-chain step(s), wait for each tx receipt to confirm (status != 0).
const created = await sdk.interop.create(params);
console.log('CREATE:', created);

// STATUS: Non-blocking lifecycle inspection.
const st0 = await sdk.interop.status(created);
console.log('STATUS after create:', st0);

// WAIT: waits until the L2->L1 proof is available on source and the interop root
// becomes available on the destination chain. It returns the proof payload needed
// to execute the bundle later.
const finalizationInfo = await sdk.interop.wait(created, {
pollMs: 5_000,
timeoutMs: 30 * 60 * 1_000,
});
console.log('Bundle is finalized on source; root available on destination.');
// FINALIZE: Execute on destination and block until done.
// finalize() calls executeBundle(...) on the destination chain,
// waits for the tx to mine, then returns { bundleHash, dstExecTxHash }.
const finalizationResult = await sdk.interop.finalize(finalizationInfo);
console.log('FINALIZE RESULT:', finalizationResult);

// STATUS: Terminal status (EXECUTED).
const st1 = await sdk.interop.status(created);
console.log('STATUS after finalize:', st1);

const greetingAfter = (await greeter.message()) as string;
console.log('Greeting after:', greetingAfter);
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
38 changes: 38 additions & 0 deletions examples/ethers/interop/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { AbiCoder, Wallet, parseUnits } from 'ethers';
import type { Address } from '../../../src/core';
import { ERC20_BYTECODE, GREETING_BYTECODE } from '../../interop/constants';

export async function getGreetingTokenAddress(args: {
signer: Wallet;
greeting?: string;
}): Promise<Address> {
const greeting = args.greeting ?? 'hello from destination';
const constructorArgs = AbiCoder.defaultAbiCoder().encode(['string'], [greeting]);
const deployData = GREETING_BYTECODE + constructorArgs.substring(2);

const deployTx = await args.signer.sendTransaction({ data: deployData });
const deployReceipt = await deployTx.wait();
if (!deployReceipt?.contractAddress) {
throw new Error('Greeting contract deployment failed: missing contract address.');
}

return deployReceipt.contractAddress as Address;
}

export async function getErc20TokenAddress(args: {
signer: Wallet;
initialSupply?: bigint;
}): Promise<Address> {
const initialSupply = args.initialSupply ?? parseUnits('1000000', 18);
const constructorArgs = AbiCoder.defaultAbiCoder().encode(['uint256'], [initialSupply]);
const deployData = ERC20_BYTECODE + constructorArgs.substring(2);

const deployTx = await args.signer.sendTransaction({ data: deployData });
const deployReceipt = await deployTx.wait();
if (!deployReceipt?.contractAddress) {
throw new Error('ERC20 deployment failed: missing contract address.');
}
const tokenAddress = deployReceipt.contractAddress as Address;

return tokenAddress;
}
Loading