DEV Community

Cover image for Blockchain Interoperability: Connecting Multiple Blockchain Networks for Payments
Drew Harris for Rapyd

Posted on • Originally published at community.rapyd.net

Blockchain Interoperability: Connecting Multiple Blockchain Networks for Payments

Writer Name: 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:

Project architecture, courtesy of Vivek Kumar Maskara

  • CrossChainPayment is a simple smart contract that initiates a cross-chain transaction and emits the PaymentSent event.
  • PaymentVerifier is another smart contract that simulates a cross-chain transaction verification and emits the PaymentVerified 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 the verifyPayment method of the destination chain.

Prerequisites

Before you get started, you need to do the following:

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
Enter fullscreen mode Exit fullscreen mode

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 sample Lock.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);
    }
}
Enter fullscreen mode Exit fullscreen mode

The smart contract defines a sendPayment function that does the following:

  • It takes the to address and amount for sending the payment, and it asserts that the amount matches the value set by the sender in the msg 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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,
    },
  },
};

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

Obtain RPC URLs from Alchemy

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
Enter fullscreen mode Exit fullscreen mode

It might take a few seconds for the contract to deploy, and it outputs the contract address like this:

Contract deployed to: 0x09292e7C53697DFcdBA3c51425bb7e36d7F6Ef2a
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then, deploy the CrossChainPayment to the Sepolia chain:

npx hardhat run scripts/deploy_crosschain.js --network sepolia
Enter fullscreen mode Exit fullscreen mode

Finally, deploy the PaymentVerifier to the Sepolia chain:

npx hardhat run scripts/deploy_paymentverifier.js --network sepolia
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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 the CrossChainPayment. Once the transaction is complete, you will receive a ContractTransactionResponse confirming the successful completion of the transaction.
  • Within a few seconds of completion, the relay script should receive a PaymentSent event from the CrossChainPayment contract deployed on the Sepolia chain. The script should invoke the verifyPayment method defined in the PaymentVerifier 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
umang_suthar_9bad6f345a8a profile image
Umang Suthar

Interoperability is exactly where the future of blockchain + payments is heading.

At Haveto (haveto.com), we’re pushing this vision further by enabling AI to run directly on-chain with auto-scaling and true cross-chain support. That means faster, cheaper, and more secure interoperability for real-world use cases like fintech and global payments.

Excited to see more developers building in this space.