By: Vivek Kumar Maskara
Blockchain is a decentralized and immutable digital database or ledger that’s distributed across a network of computers in a transparent and tamperproof manner.
Blockchain interoperability refers to the ability of blockchains to communicate and share data with other blockchains. This enables developers to build cross-chain solutions that combine the strengths of each blockchain. For example, financial applications need blockchain interoperability to enable smooth cross-chain transactions and to improve liquidity, accessibility, and expand financial inclusion.
In this article, you’ll learn how to implement blockchain interoperability using Solidity smart contracts for payment verification.
Why Fintech Applications Need Blockchain Interoperability
If you’re building a decentralized application (dApps), you need blockchain interoperability. This allows your app to interact with different blockchain networks, expanding the app’s access to assets, usability, and scalability.
Here are a few benefits of blockchain interoperability:
- Seamless cross-chain transactions: Blockchain interoperability enables users to transfer assets across different blockchain networks without relying on centralized exchanges or manually converting them to fiat currency.
- Lower transaction costs and faster settlement: It reduces the need for intermediaries to carry out a transaction, optimizes payment processing costs, and enables faster settlement across blockchain networks.
- Improved liquidity and accessibility: It connects fragmented payment ecosystems, allowing users to access a wider liquidity pool to exchange their tokens. For example, a user can transfer USDT from Ethereum (ERC-20) to the Tron network to take advantage of lower fees and faster transaction speeds. This enhances financial accessibility for global users as they can transfer their assets to a different chain based on their needs.
- Expanded financial inclusion: Supporting multichain compatibility enables fintech applications to reach more users and businesses, even in regions where traditional financial services are limited.
Simplifying Payment Verification with Rapyd
Blockchain interoperability sounds great in theory: move assets and data across networks seamlessly. But building a complete payment verification system requires significant developer effort. Smart contracts struggle to handle real-world requirements, like fiat conversions, regulatory compliance, fraud prevention, and off-chain settlement on their own, especially when these systems must remain reliable and responsive at scale.
Thankfully, Rapyd offers a robust financial infrastructure with easy-to-integrate REST APIs that offload the hard parts for you. It acts as an off-chain settlement layer and cross-chain data exchange, enabling use cases like automated compliance checks, escrow, and transaction verification, without requiring developers to build and maintain that infrastructure themselves.
From real-time fraud prevention and automated global KYC/AML to fiat on/off-ramps in over 100 countries, Rapyd removes the traditional finance complexity from blockchain payments. The Rapyd API platform simplifies the end-to-end financial workflow:
- The Collect Payments API receives funds from various payment methods and deposits them into Rapyd Wallets.
- The Rapyd Verify APIs perform hosted Know Your Business (KYB) and Know Your Customer (KYC) verifications to meet compliance requirements.
- The Rapyd Protect APIs detect fraud and suspicious activity through built-in risk scoring, enhancing compliance and eliminating the need to build custom fraud logic.
- Webhooks receive real-time notifications for key events, such as payment status changes or verification updates.
As an off-chain infrastructure layer, Rapyd also aligns with one of the core principles of blockchain systems: availability. Even during peak congestion or transaction delays on the blockchain, Rapyd continues to operate reliably, ensuring high availability for compliance, verification, and settlement workflows.
Implementing Blockchain Interoperability
In this tutorial, you’ll build, deploy, and test smart contracts on Ethereum testnets like Sepolia and Holesky. The tutorial uses the Hardhat developer environment to easily deploy your Solidity smart contracts and test them using the Hardhat network, which is a local Ethereum network designed for development.
Here are the primary components of the project:
CrossChainPayment
is a simple smart contract that initiates a cross-chain transaction and emits thePaymentSent
event.PaymentVerifier
is another smart contract that simulates a cross-chain transaction verification and emits thePaymentVerified
event. For simplicity, it doesn’t perform any complex checks, but it can be extended based on the use case.relay.js
script watches for events on the source chain (ie Sepolia) with the destination chain as Holesky and invokes theverifyPayment
method of the destination chain.
Prerequisites
Before you get started, you need to do the following:
- Install a Node.js development environment.
- Sign up for an Alchemy account and create an API key.
- Set up Metamask and create a wallet.
- Obtain a Metamask account private key.
- Fund your Metamask wallet using Google Cloud’s Ethereum Holešky and Sepolia Faucet. You can use any faucet of your choice, but some faucets may require a minimum wallet balance to receive funds.
Cloning the Starter Project
This tutorial uses this GitHub starter code that has a barebones Hardhat app set up. To follow along, clone the GitHub repo and switch to the starter
branch:
git clone https://github.com/Rapyd-Samples/rapyd-starter-blockhain.git
cd rapyd-starter-blockhain
git checkout starter
Let’s go over the structure of the codebase:
- The
package.json
file defines the Hardhat project dependencies. It uses the Hardhat dependency to get an Ethereum development environment and uses @nomicfoundation/hardhat-toolbox to get all the common Hardhat plugins. - The
contracts
directory contains a sampleLock.sol
solidity smart contract. You won’t need it in the tutorial, but you can use it to understand the basic structure of a Solidity smart contract.
Defining Smart Contracts for Cross-Chain Transaction Verification
In this section, you’ll define two smart contracts to enable cross-chain transaction verification.
CrossChainPayment contract
Initially, you’ll define the CrossChainPayment
that initiates a cross-chain payment and emits the PaymentSent
event. Here, you’ll define a simple smart contract that checks for amount mismatch issues before emitting the PaymentSent
event.
Create a contracts/CrossChainPayment.sol
file and add the following code snippet to it:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract CrossChainPayment {
event PaymentSent(address indexed from, address indexed to, uint256 amount, uint256 chainId);
function sendPayment(address to, uint256 amount, uint256 destChainId) external payable {
require(msg.value == amount, "Amount mismatch");
emit PaymentSent(msg.sender, to, amount, destChainId);
}
}
The smart contract defines a sendPayment
function that does the following:
- It takes the
to
address andamount
for sending the payment, and it asserts that theamount
matches the value set by the sender in themsg
payload. - It emits a
PaymentSent
event with the sender’s address (msg.sender
), receiver’s address (to
), amount, and destination chain ID (destChainId
).
PaymentVerified contract
Now it’s time to define the PaymentVerifier
that will be invoked by the relayer (more on this later). The PaymentVerifier
smart contract contains the verifyPayment
method, where on-chain verification can be performed before emitting the PaymentVerified
event.
Create a contracts/PaymentVerifier.sol
file and add the following code snippet to it:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract PaymentVerifier {
event PaymentVerified(address indexed to, uint256 amount, uint256 srcChainId, bytes32 txHash);
function verifyPayment(address to, uint256 amount, uint256 srcChainId, bytes32 txHash) external {
// perform verification steps
emit PaymentVerified(to, amount, srcChainId, txHash);
}
}
This contract doesn’t perform a full cross-chain verification on its own. It acts as a receiver for cross-chain data, and usually, a relayer or a middleware service invokes the verifyPayment
method of the contract. This method expects the transaction receiver’s address (to
), amount, source chain ID (srcChainId
), and the transaction hash (txHash
). These fields will be supplied by the relayer while invoking the contract.
The txHash
refers to the on-chain transaction hash and can be used to perform additional transaction checks before emitting the PaymentVerified
event. For this tutorial, the smart contract emits the event without any checks.
Note that in a real-world system, the PaymentVerifier
contract can be extended to include more extensive verification checks, such as validating signatures and cryptographic proofs.
Defining Scripts to Deploy the Smart Contracts
Now that you’ve defined the smart contracts, you need to deploy them to test blockchain networks (testnets) before you can use them.
To deploy the smart contracts, you can use a node
script that uses the hardhat
SDK to deploy the smart contract and print its address. To deploy the CrossChainPayment
contract, create a scripts/deploy_crosschain.js
script and add the following contents to it:
const hre = require("hardhat");
async function main() {
const Contract = await hre.ethers.getContractFactory("CrossChainPayment");
const contract = await Contract.deploy();
await contract.waitForDeployment();
console.log("Contract deployed to:", await contract.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The Hardhat SDK creates an instance of the Ethers ContractFactory to convert the CrossChainPayment
solidity contract into bytecode and deploys it on the blockchain network.
Before using the script, you need to make sure that the network is configured under hardhat.config.js
. You also need to specify the network name as a CLI parameter while using the script. More on this in a later section.
Similarly, to deploy the PaymentVerifier
contract, create a scripts/deploy_paymentverifier.js
file and add the following contents to it:
const hre = require("hardhat");
async function main() {
const Contract = await hre.ethers.getContractFactory("PaymentVerifier");
const contract = await Contract.deploy();
await contract.waitForDeployment();
console.log("Contract deployed to:", await contract.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
This script converts the PaymentVerifier
contract into bytecode and deploys it. Notice that both the contract deployment scripts are quite similar, and you could also parameterize the script and pass the contract name as a CLI argument.
Deploying the Smart Contracts to testnets
This tutorial uses the Sepolia and Holesky testnets. Because deploying a smart contract to a network requires gas, make sure to fund your wallet with tokens using a faucet like Google Cloud’s Ethereum Holešky or Sepolia Faucet.
Before deploying the smart contracts, you need to configure the desired testnets in the hardhat.config.js
file. Update the contents of the hardhat.config.js
file with the following code snippet:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC,
accounts: [process.env.PRIVATE_KEY],
chainId: 11155111,
},
holesky: {
url: process.env.HOLESKY_RPC,
accounts: [process.env.PRIVATE_KEY],
chainId: 17000,
},
},
};
This snippet updates the network
configuration to add sepolia
and holesky
testnets. Notice that the config file uses SEPOLIA_RPC
, HOLESKY_RPC
, and PRIVATE_KEY
environment variables. To configure environment variables, create a .env
file in the project’s root and add the following contents to it:
PRIVATE_KEY=<YOUR_META_MASK_PRIVATE_KEY>
SEPOLIA_RPC=<YOUR_ALCHEMY_SEPOLOIA_TEST_NET_HTTPS_RPC_URL>
SEPOLIA_WSS=<YOUR_ALCHEMY_SEPOLOIA_TEST_NET_WSS_RPC_URL>
HOLESKY_RPC=<YOUR_ALCHEMY_HOLESKY_TEST_NET_HTTPS_RPC_URL>
Replace <YOUR_META_MASK_PRIVATE_KEY>
with the Metamask wallet’s private key obtained earlier. Replace <YOUR_ALCHEMY_SEPOLOIA_TEST_NET_HTTPS_RPC_URL>
and <YOUR_ALCHEMY_HOLESKY_TEST_NET_HTTPS_RPC_URL>
with your HTTPS RPC URL from the Alchemy dashboard:
Additionally, make sure you replace <YOUR_ALCHEMY_SEPOLOIA_TEST_NET_WSS_RPC_URL>
with the WebSocket (WSS) RPC URL obtained from the Alchemy dashboard.
Now that your environment variables and Hardhat configuration are set up, you can deploy the contracts. Either of these chains can be used as a source or destination for a transaction, so you need to deploy PaymentVerifier
and CrossChainPayment
contracts to both Sepolia and Holesky chains.
Execute the following command to deploy the CrossChainPayment
to the Holesky chain:
npx hardhat run scripts/deploy_crosschain.js --network holesky
It might take a few seconds for the contract to deploy, and it outputs the contract address like this:
Contract deployed to: 0x09292e7C53697DFcdBA3c51425bb7e36d7F6Ef2a
Note the contract address and deploy the PaymentVerifier
contract to the Holesky chain by executing the following:
npx hardhat run scripts/deploy_paymentverifier.js --network holesky
Then, deploy the CrossChainPayment
to the Sepolia chain:
npx hardhat run scripts/deploy_crosschain.js --network sepolia
Finally, deploy the PaymentVerifier
to the Sepolia chain:
npx hardhat run scripts/deploy_paymentverifier.js --network sepolia
Make sure to save all the contract addresses as you will need them while defining the relay script.
Defining the Relay Script
Now that the smart contracts are defined, you need to define a relay script that will listen for PaymentSent
events on the source chain. When it finds a PaymentSent
event, it matches the destination chain ID and triggers the verifyPayment
smart contract method on the destination chain.
Create a scripts/relay.js
file and add the following code snippet to it:
const { WebSocketProvider, JsonRpcProvider, Contract, Wallet } = require("ethers");
require("dotenv").config();
// Use WebSocket for Sepolia (where we're listening for events)
const SEPOLIA_WSS = process.env.SEPOLIA_WSS;
const HOLESKY_RPC = process.env.HOLESKY_RPC;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const SEPOLIA_CONTRACT = "<YOUR_CROSS_PAYMENT_SEPOLIA_ADDRESS>";
const HOLESKY_VERIFIER = "<YOUR_PAYMENT_VERIFIER_HOLESKY_ADDRESS>";
const eventAbi = [
"event PaymentSent(address indexed from, address indexed to, uint256 amount, uint256 chainId)"
];
const verifierAbi = [
"function verifyPayment(address to, uint256 amount, uint256 srcChainId, bytes32 txHash)"
];
async function startRelayer() {
// Use WebSocketProvider for event monitoring
const sepoliaProvider = new WebSocketProvider(SEPOLIA_WSS);
const holeskyProvider = new JsonRpcProvider(HOLESKY_RPC);
const wallet = new Wallet(PRIVATE_KEY, holeskyProvider);
const sourceContract = new Contract(SEPOLIA_CONTRACT, eventAbi, sepoliaProvider);
const verifier = new Contract(HOLESKY_VERIFIER, verifierAbi, wallet);
console.log("Relayer is watching for events on Sepolia via WebSocket...");
// Set up reconnection logic
sepoliaProvider.websocket.on('close', (code) => {
console.log(`WebSocket connection closed with code ${code}. Reconnecting...`);
setTimeout(startRelayer, 3000);
});
// Listen for events using WebSocket
sourceContract.on("PaymentSent", async (from, to, amount, chainId, event) => {
console.log("PaymentSent Detected:");
console.log({ from, to, amount: amount.toString(), chainId });
if (chainId.toString() !== "17000") {
console.log("Skipping non-Holesky destination.");
return;
}
try {
// Get transaction hash from event and ensure it's in bytes32 format
const txHash = event.log.transactionHash;
const tx = await verifier.verifyPayment(
to,
BigInt(amount.toString()),
11155111,
txHash
);
console.log("Verification TX sent:", tx.hash);
} catch (err) {
console.error("Error verifying payment:", err.message);
console.log("Error details:", err);
}
});
// Handle process termination
process.on('SIGINT', async () => {
console.log('Closing WebSocket connection...');
await sepoliaProvider.destroy();
process.exit();
});
}
// In case of connection errors, restart the relayer
try {
startRelayer();
} catch (error) {
console.error("Error starting relayer:", error);
setTimeout(startRelayer, 3000);
}
To interact with the Sepolia and Holesky chains, this code creates an instance of the JsonRpcProvider. The relay script assumes that the transaction will be initiated on the Sepolia chain and starts listening for the PaymentSent
events on this chain. It sets the Holesky chain as the destination chain, creates an instance of the PaymentVerifier
contract deployed on it, and invokes the verifyPayment
method using the payload received in the PaymentSent
event.
This setup enables blockchain interoperability by bridging event data between two separate chains. It captures transactions on the source chain (Sepolia) and triggers verification logic on the destination chain (Holesky), without requiring a built-in bridge or shared state between them.
Testing Cross-chain Interactions
With smart contracts deployed to both Sepolia and Holesky chains and the relay scripts in place, you can test blockchain interoperability. The goal of testing is to verify the following:
- Initiate a cross-chain transaction on the Sepolia chain using the
sendPayment
method defined in theCrossChainPayment
. Once the transaction is complete, you will receive aContractTransactionResponse
confirming the successful completion of the transaction. - Within a few seconds of completion, the relay script should receive a
PaymentSent
event from theCrossChainPayment
contract deployed on the Sepolia chain. The script should invoke theverifyPayment
method defined in thePaymentVerifier
contract deployed on the Holesky chain. - On invocation, the
PaymentVerifier
contract deployed on the Holesky chain should verify the transaction and return a successful response.
Before you begin testing, start the relay script in a new terminal window:
# Execute this command in the project's root directory
node scripts/relay.js
This code starts the relay script, which listens for events on the Sepolia chain.
To initiate a transaction on the Sepolia chain, start the hardhat
console for the sepolia
network by executing the following command in the project’s root in a separate terminal window:
# Execute this in the project's root
npx hardhat console --network sepolia
This command starts the hardhat
console, where you can execute smart contracts and perform transactions. To initiate a cross-chain payment, paste the following script in the hardhat
console:
// Get ethers from Hardhat runtime
const { ethers } = hre;
// Load your deployed contract
const contract = await ethers.getContractAt("CrossChainPayment", "0x45d88f6DD0f0eDB69C563233Be73458c9980b519");
// Send payment
await contract.sendPayment(
"0x11ddd4b07B095802B537267358fB8Eb954B29d99", // Recipient
ethers.parseEther("0.001"), // Amount
17000, // Destination Chain ID (Holešky)
{ value: ethers.parseEther("0.001") } // Payment value
);
Note that if the terminal prompts you to confirm pasting multiple lines of code, click Paste to confirm the action. Once you paste the code and press Enter, you will receive a ContractTransactionResponse
confirming that the transaction was successful:
ContractTransactionResponse {
provider: HardhatEthersProvider {
_hardhatProvider: LazyInitializationProviderAdapter {
_providerFactory: [AsyncFunction (anonymous)],
_emitter: [EventEmitter],
_initializingPromise: [Promise],
provider: [BackwardsCompatibilityProviderAdapter]
},
_networkName: 'sepolia',
_blockListeners: [],
_transactionHashListeners: Map(0) {},
_eventListeners: []
},
blockNumber: null,
blockHash: null,
index: undefined,
hash: '0xa2bd160eb0bfde7b1eadbde6c3a1c3a19f876ed5e8731e0d32d582628ba5e361',
type: 2,
to: '0x45d88f6DD0f0eDB69C563233Be73458c9980b519',
from: '0xcD0AAcf118B43C0878D90886f0e1D54D043CF726',
nonce: 7,
gasLimit: 25215n,
gasPrice: 55068367n,
maxPriorityFeePerGas: 50000000n,
maxFeePerGas: 55068367n,
maxFeePerBlobGas: null,
data: '0x8d82e72f00000000000000000000000011ddd4b07b095802b537267358fb8eb954b29d9900000000000000000000000000000000000000000000000000038d7ea4c680000000000000000000000000000000000000000000000000000000000000004268',
value: 1000000000000000n,
chainId: 11155111n,
signature: Signature { r: "0x9c0bc7dd319671a3bf13c09e3d0b7398529fe0805055a86ecb41fc7a0a2c76f8", s: "0x047fcab2b6594f02d3fa8b3d0a00e92364b8c1a41713126cd2974bc15d00dd52", yParity: 0, networkV: null },
accessList: [],
blobVersionedHashes: null
}
The response contains details about the executed transaction, including the transaction hash (hash
), the sender’s address (from
), the receiver’s address (to
), and the hashed transaction payload (data
).
Head back to the relay script’s terminal window, and within a few seconds, you should see a PaymentSent
event log printed in the console:
PaymentSent Detected:
{
from: '0xcD0AAcf118B43C0878D90886f0e1D54D043CF726',
to: '0x11ddd4b07B095802B537267358fB8Eb954B29d99',
amount: '1000000000000000',
chainId: 17000n
}
Notice that the from
and to
addresses in the event match the addresses received in the ContractTransactionResponse
. The relay script will invoke the verifyPayment
contract on the Holesky chain, and within a few seconds, a transaction verification log will be printed in the console:
Verification TX sent: 0xb034b9bcd67eb3b58615fcdcd3704df70f75608bda16c1f2a454dc0f200a14dd
Conclusion
In this tutorial, you learned how to achieve blockchain interoperability by building smart contracts using Solidity and Hardhat. Smart contracts can be used to initiate cross-chain transactions and verify them based on preset rules. This lets developers connect and share data between isolated blockchains, helping them build innovative applications.
However, developing extensive verification checks to handle different scenarios relating to fraud, settlement, and compliance can be challenging for developers. With Rapyd, developers can focus on their core application logic, whether on-chain or off-chain, while relying on Rapyd to handle the complexity of compliance, identity verification, and secure settlement. More than just a complementary tool, Rapyd serves as your fiat bridge and compliance co-pilot by simplifying off-chain infrastructure, allowing teams to focus on delivering innovative, seamless, and scalable payment experiences faster.
Experiment with the Rapyd API and the code used in this tutorial to discover how Rapyd can offer you the best in building an interoperable payment system.