Bridge-and-Call
Advanced atomic operations that combine asset transfers with contract execution using Lxly.js
Overview
Bridge-and-call is the most advanced cross-chain operation in Lxly.js, enabling atomic transactions that both transfer assets and execute smart contract functions on the destination chain. This powerful feature allows for complex cross-chain workflows in a single transaction.
Key Features:
- Atomic Operations: Asset transfer and contract execution succeed or fail together
- No Interface Required: Target contracts don't need
IBridgeMessageReceiver - JumpPoint Deployment: Temporary contracts handle execution
- Fallback Support: Handles failed executions gracefully
Basic Bridge-and-Call
Simple Bridge-and-Call
const { LxLyClient, use } = require('@maticnetwork/lxlyjs');
const { Web3ClientPlugin } = require('@maticnetwork/maticjs-web3');
// Initialize client (see Client Initialization guide)
const client = await initializeClient();
async function basicBridgeAndCall() {
try {
// Get bridge extension for source network
const bridgeExtension = client.bridgeExtensions[0]; // Ethereum
// Encode function call for destination contract
const targetContract = client.contract(targetABI, targetAddress, 1);
const callData = await targetContract.encodeAbi('receiveTokens', userAddress, amount);
// Execute bridge-and-call
const result = await bridgeExtension.bridgeAndCall(
'0x3fd0A53F4Bf853985a95F4Eb3F9C9FDE1F8e2b53', // Token to bridge
'1000000000000000000', // 1 token
1, // Destination network (Polygon)
'0xTargetContractAddress', // Contract to call
userAddress, // Fallback address
callData, // Encoded function call
true // Force update global exit root
);
const txHash = await result.getTransactionHash();
console.log('Bridge-and-call transaction:', txHash);
return txHash;
} catch (error) {
console.error('Bridge-and-call failed:', error);
throw error;
}
}Bridge ETH and Call
async function bridgeETHAndCall() {
try {
const bridgeExtension = client.bridgeExtensions[0];
// Encode function call
const callData = await targetContract.encodeAbi('processPayment', paymentId, amount);
// Bridge ETH and call contract
const result = await bridgeExtension.bridgeAndCall(
'0x0000000000000000000000000000000000000000', // ETH token (zero address)
'100000000000000000', // 0.1 ETH
1, // Destination network
'0xPaymentContractAddress',
userAddress, // Fallback address
callData,
true,
undefined, // No permit data for ETH
{
value: '100000000000000000', // ETH value to send
gasLimit: 400000
}
);
const txHash = await result.getTransactionHash();
console.log('ETH bridge-and-call transaction:', txHash);
return txHash;
} catch (error) {
console.error('ETH bridge-and-call failed:', error);
throw error;
}
}Bridge-and-Call with Permit
async function bridgeAndCallWithPermit() {
const amount = '2000000000000000000'; // 2 tokens
try {
const sourceToken = client.erc20('0x3fd0A53F4Bf853985a95F4Eb3F9C9FDE1F8e2b53', 0);
const bridgeExtension = client.bridgeExtensions[0];
// Get permit data for bridge extension
const bridgeExtensionAddress = bridgeExtension.contractAddress;
const permitData = await sourceToken.getPermitData(amount, {
spenderAddress: bridgeExtensionAddress
});
// Encode target function call
const callData = await targetContract.encodeAbi('processTokens', amount, userAddress);
// Execute bridge-and-call with permit (no separate approval needed)
const result = await bridgeExtension.bridgeAndCall(
'0x3fd0A53F4Bf853985a95F4Eb3F9C9FDE1F8e2b53',
amount,
1, // Destination network
'0xTargetContractAddress',
userAddress,
callData,
true,
permitData, // Permit data for gasless approval
{ gasLimit: 450000 }
);
const txHash = await result.getTransactionHash();
console.log('Gasless bridge-and-call transaction:', txHash);
return txHash;
} catch (error) {
console.error('Gasless bridge-and-call failed:', error);
throw error;
}
}Target Contract
Contract Requirements
Bridge-and-call target contracts do NOT need to implement IBridgeMessageReceiver. The JumpPoint contract handles the bridge reception and calls your contract directly.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract BridgeAndCallTarget {
event TokensReceived(address token, uint256 amount, address from);
mapping(address => uint256) public tokenBalances;
// This function can be called directly by JumpPoint
function receiveTokens(address token, uint256 amount, address from) external {
// JumpPoint will transfer tokens to this contract before calling
require(IERC20(token).balanceOf(address(this)) >= amount, "Tokens not received");
tokenBalances[token] += amount;
emit TokensReceived(token, amount, from);
}
// Process payment and execute business logic
function processPayment(
address paymentToken,
uint256 paymentAmount,
bytes calldata orderData
) external {
require(IERC20(paymentToken).balanceOf(address(this)) >= paymentAmount, "Payment not received");
// Decode and process order
(uint256 productId, uint256 quantity, address recipient) = abi.decode(orderData, (uint256, uint256, address));
// Execute business logic
// ...
}
}Claiming Bridge-and-Call
Two-Phase Claiming
Bridge-and-call operations create two separate bridges that must be claimed in sequence:
async function claimBridgeAndCall(bridgeTransactionHash, sourceNetwork) {
const destinationNetwork = 1;
try {
console.log('🎯 Starting bridge-and-call claim process...');
// Phase 1: Claim asset bridge (deposit_count = 0)
console.log('📦 Claiming asset bridge...');
const destinationToken = client.erc20('0xDestinationTokenAddress', destinationNetwork);
const assetClaimResult = await destinationToken.claimAsset(
bridgeTransactionHash,
sourceNetwork,
{
bridgeIndex: 0, // Asset bridge is always index 0
gasLimit: 400000
}
);
const assetClaimTxHash = await assetClaimResult.getTransactionHash();
console.log('✅ Asset claim transaction:', assetClaimTxHash);
// Wait for asset claim confirmation
await assetClaimResult.getReceipt();
console.log('✅ Asset claim confirmed');
// Phase 2: Claim message bridge (deposit_count = 1)
console.log('📨 Claiming message bridge...');
const bridge = client.bridges[destinationNetwork];
// Build payload for message claim
const messagePayload = await client.bridgeUtil.buildPayloadForClaim(
bridgeTransactionHash,
sourceNetwork,
1 // Message bridge is always index 1
);
const messageClaimResult = await bridge.claimMessage(
messagePayload.smtProof,
messagePayload.smtProofRollup,
messagePayload.globalIndex,
messagePayload.mainnetExitRoot,
messagePayload.rollupExitRoot,
messagePayload.originNetwork,
messagePayload.originTokenAddress,
messagePayload.destinationNetwork,
messagePayload.destinationAddress,
messagePayload.amount,
messagePayload.metadata,
{ gasLimit: 500000 }
);
const messageClaimTxHash = await messageClaimResult.getTransactionHash();
console.log('✅ Message claim transaction:', messageClaimTxHash);
console.log('🎉 Bridge-and-call completed successfully!');
return {
assetClaimTxHash,
messageClaimTxHash
};
} catch (error) {
console.error('❌ Bridge-and-call claim failed:', error);
throw error;
}
}Verify Contract Execution
async function verifyContractExecution(targetContractAddress, expectedChanges) {
try {
const targetContract = client.contract(targetABI, targetContractAddress, 1);
// Check if contract state changed as expected
const results = await Promise.all([
targetContract.read('tokenBalances', tokenAddress),
targetContract.read('lastOperationTimestamp'),
targetContract.read('operationCount')
]);
const [tokenBalance, lastOperation, operationCount] = results;
console.log('Contract verification:');
console.log(' Token balance:', tokenBalance);
console.log(' Last operation:', new Date(lastOperation * 1000));
console.log(' Operation count:', operationCount);
// Verify expected changes
const verified = parseInt(tokenBalance) >= parseInt(expectedChanges.minTokenBalance);
console.log(verified ? '✅ Verification passed' : '❌ Verification failed');
return {
tokenBalance,
lastOperation,
operationCount,
verified
};
} catch (error) {
console.error('Contract verification failed:', error);
throw error;
}
}Edit on GitHub
Last updated on