Jan 9, 2024

A complete example for multi EVM chain voting service easily and securely with Chainsight

by Hideyoshi

Hey Fellow Devs đź‘‹

I've been working on a project called ChainSight for the past few months. It's a composable-oracle platform that allows you to create custom oracles for your EVM smart contracts. Using ChainSight, you can create oracles that provide data from any source, including other smart contracts, APIs, and even other blockchains.

Anyone can create an oracle and publish it on the platform. The oracle can be used by anyone who needs the data it provides.

From some points of view, Chainsight can be considered as a SDK for deploying smart contracts on the Internet Computer that interact with other smart contracts on other chains. These contracts are called "canisters" and they are deployed on the Internet Computer's blockchain. Using https-outcalls and threshold ECDSA, important features of the Internet Computer, Chainsight provides a set of tools for creating canisters and deploying them on the Internet Computer easily and securely.. For more details, check out the documentation.

Recently, I have added a new feature to the platform that allows you to execute not only an oracle operation but also any other smart contract execution. This feature allows you to create a little bit more complex use cases. The following are some examples of what you can do with it:

  • An arbitrage bot: execute a swap on Uniswap when the price of a token is lower than the price on another exchange.
  • A liquidation bot: execute a liquidation on Compound when the collateralization ratio of a loan is lower than a certain threshold.
  • A multi-chain voting: submit a proposal on one chain and vote on it on another chain.

These are just a few examples of what you can do with this feature. I'm sure you can come up with many more. In this post, I will show you how to create a multi-chain voting in 30 minutes using ChainSight.

The complete code for this post can be found here.

On this post, I will show you how to create a multi-chain voting in 30 minutes using ChainSight. And the next post, I will deploy the canisters on the Internet Computer and show you how to use them.

What is a multi-chain voting I am talking about?

A multi-chain voting is a voting that takes place on multiple chains. The idea is to submit a proposal on one chain and vote on it on another chain. This is useful when you want to create a DAO that spans across multiple chains. For example, you can create a DAO on Ethereum and vote on proposals on Polygon. In this example, I will show you

How to create a multi-chain voting?

To create a multi-chain voting, you need to create two functions: one for submitting a proposal and one for voting on it. The first contract will be used to synchronize the proposal between the chains. The second one will be used to vote on the proposal.

And how can we synchronize proposals and votes between chains is the issue we are going to solve in this post. We will use ChainSight to do that. ChainSight provides a set of tools for creating canisters and deploying them on the Internet Computer.

Prerequisites

To try out this example, you may want to know a little bit about the Internet Computer and Chainsight. If you are not familiar with them, you can check out the following links:

I'll write about overview of Chainsight and hello world example in future posts. So, stay tuned.

Project Overview

The project consists of two parts: the solidity smart contracts and ICP smart contracts(aka canisters). The solidity smart contracts are used to submit proposals and vote on them. The ICP smart contracts are used to synchronize proposals and votes between chains.

├── components
├── interfaces
├── scripts
│── solidity
│   ├── contracts
|── src

FolderDescription
componentsChainsight components I will deploy on the Internet Computer
interfacesInterfaces Chainight components will use to interact with the Internet Computer
scriptsScripts for deploying Chainsight components on the Internet Computer local env
soliditySolidity smart contracts we will deploy on Sepolia and Scroll Sepolia
srcUser defined logics for Chainsight components

Following contracts are deployed on each EVM chain.

ContractDescription
contracts/proposal/ProposalFactory.solContract for creating a new proposal
contracts/proposal/ProposalManager.solContract for managing proposals and its votes
contracts/proposal/ProposalSynchronizer.solContract for synchronizing proposals from other chains via Chainsight
contracts/voting/VotingSynchonizer.solContract for synchronizing votes from other chains via Chainsight

And following canisters are deployed on the Internet Computer as Chainsight components.

ComponentDescription
components/proposal_factory_event_indexer_sepolia.yamlEvent indexer for ProposalFactory contract events ProposalCreated on Scroll Sepolia
components/proposal_factory_event_indexer_scroll_sepolia.yamlEvent indexer for ProposalFactory contract events ProposalCreated on Scroll Sepolia
components/proposal_lens.yamlLens for ProposalManager contract to convert proposal event data into another format to be used by proposal relayers
components/proposal_relayer_sepolia.yamlRelayer for ProposalManager contract to relay proposal data from Scroll Sepolia to Sepolia
components/proposal_relayer_scroll_sepolia.yamlRelayer for ProposalManager contract to relay proposal data from Sepolia to Scroll Sepolia
components/voting_event_indexer_sepolia.yamlEvent indexer for Voting contract events Voted on Sepolia
components/voting_event_indexer_scroll_sepolia.yamlEvent indexer for Voting contract events Voted on Scroll Sepolia
components/voting_lens.yamlLens for Voting contract to convert vote event data into another format to be used by voting relayers
components/voting_relayer_sepolia.yamlRelayer for Voting contract to relay vote data from Scroll Sepolia to Sepolia
components/voting_relayer_scroll_sepolia.yamlRelayer for Voting contract to relay vote data from Sepolia to Scroll Sepolia

Overview of Chainsight components and EVM smart contracts are as follows.

clknt3qxiuhnlmmamnba.png
  1. User submits a proposal on a chain.
    1. The proposal is indexed by the event indexer on the chain.
  2. The proposal is relayed to another chain by the proposal relayer by calling batchSynchronize function on the ProposalSynchonizer contract.
    1. Voting on the proposal is started on each chain.
  3. User votes on the proposal on a chain.
    1. The vote is indexed by the event indexer on the chain.
    2. The vote is relayed to another chain by the voting relayer by calling batchSynchronize function on the VotingSynchonizer contract.

Details of Chainsight components

Let's take a look at the details of Chainsight components with its code.

ProposalFactoryEventIndexerSepolia

This component indexes ProposalCreated events emitted by ProposalFactory contract on Scroll Sepolia. The indexer is deployed on Sepolia.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/main/resources/schema/event_indexer.json
version: v1
metadata:
  label: Proposal Factory Indexer Sepolia
  type: event_indexer
  description: ""
  tags:
    - TODO
datasource:
  id: 0xAEB9FBEE52B7272b5c75Bc64E4f82D24Ee2Aaa33 // ProposalFactory contract address on Sepolia
  event:
    identifier: ProposalCreated // Event name
    interface: IProposalFactory.json // Interface of ProposalFactory contract. You can find it in interfaces/ folder.
  network:
    rpc_url: https://ethereum-sepolia.blockpi.network/v1/rpc/public
    chain_id: 11155111
  from: 2739538
  contract_type: ERC-20
interval: ${INTERVAL} // Interval to check new events
cycles: null

When I need to index smart contract events, what I only have to do is to write a yaml file like this. I don't need to write any code.

ProposalFactoryEventIndexerSepolia

This is the same as ProposalFactoryEventIndexerSepolia but it indexes events emitted by ProposalFactory contract on Scroll Sepolia.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/main/resources/schema/event_indexer.json

version: v1
metadata:
label: Proposal Factory Indexer Scroll
type: event_indexer
description: ""
tags: - TODO
datasource:
id: 0xaA00080dd203701d94e6F262bb7101E19A0C8107
event:
identifier: ProposalCreated
interface: IProposalFactory.json
network:
rpc_url: https://sepolia-rpc.scroll.io
chain_id: 534351
from: 0
contract_type: ERC-20
interval: ${INTERVAL}
cycles: null

ProposalLens

This component converts ProposalCreated event indexed by ProposalFactoryEventIndexer into another format to be used by proposal relayers.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/main/resources/schema/algorithm_lens.json
version: v1
metadata:
  label: Proposal Lens
  type: algorithm_lens
  description: "TODO"
  tags:
    - TODO
datasource:
  methods:
    - id: proposal_factory_event_indexer_sepolia // This is the id of EventIndexerSepolia. But this canister can be used for Scroll Sepolia as well since the format of the event data is the same.
      identifier: "events_from_to : (nat64, nat64) -> (vec record { nat64; vec ProposalCreated })" // This identifier is defined in artifacts/proposal_factory_event_indexer_sepolia.did
      candid_file_path: null
with_args: true
cycles: null

We can write some conversion logic in src/logics/proposal_lens/lib.rs file.

Now, to use event data to synchronize proposals, we need to convert it into what ProposalSynchonizer contract expects. The format of the event data is as follows.

solidity/contracts/interfaces/IProposalSynchronizer.sol

    /**
     * @dev Batch synchronize proposals from another chain
     * @param ids The ids of the proposals
     * @param proposers proposers of the proposals
     * @param chainIds The ids of the chain where the proposals are created
     * @param startTimestamps The start timestamps of voting
     * @param endTimestamps The end timestamps of voting
     * @param proposedBlocks The block numbers of the proposals
     * @notice The length of the arrays should be the same
     **/
    function batchSynchronize(
        uint256[] calldata ids,
        address[] calldata proposers,
        uint256[] calldata chainIds,
        uint256[] calldata startTimestamps,
        uint256[] calldata endTimestamps,
        uint256[] calldata proposedBlocks
    ) external;

So, we need to collect events and convert them into the format above. The following is the code for that.

src/logics/proposal_lens/lib.rs

pub type LensValue = ( // This is the format of the event data to be used by proposal relayers.
    Vec<u128>,
    Vec<String>,
    Vec<u128>,
    Vec<u128>,
    Vec<u128>,
    Vec<u128>,
);
pub type CalculateArgs = (u64, u64); // block number from, block number to. This is provided by the caller (in this case, a proposal relayer).

pub async fn calculate(targets: Vec<String>, args: CalculateArgs) -> LensValue {
    let results = get_events_from_to_in_proposal_factory_event_indexer_sepolia(
        targets.get(0usize).unwrap().clone(), // Request argments provided by the caller (in this case, a proposal relayer).
        args,
    )
    .await
    .unwrap();
    results
        .iter()
        .map(|r| {
            let block = r.0;
            let events = &r.1;
            let e: Vec<(u128, String, u128, u128, u128, u64)> = events
                .iter()
                .map(|e| {
                    (
                        e.id.value.parse().unwrap(),
                        e.proposer.clone(),
                        e.startTimestamp.value.parse().unwrap(),
                        e.endTimestamp.value.parse().unwrap(),
                        e.chainId.value.parse().unwrap(),
                        block,
                    )
                })
                .collect();
            e
        })
        .flatten()
        .map(|e| (e.0, e.1, e.2, e.3, e.4, e.5))
        .fold(
            (vec![], vec![], vec![], vec![], vec![], vec![]),
            |mut acc, e| {
                acc.0.push(e.0);
                acc.1.push(e.1);
                acc.2.push(e.2);
                acc.3.push(e.3);
                acc.4.push(e.4);
                acc.5.push(e.5.into());
                acc
            },
        )
}



ProposalRelayerSepolia

This component relays proposals from Scroll Sepolia to Sepolia.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/feature/anyrelayer/resources/schema/relayer.json
version: v1
metadata:
  label: Voting Relayer Sepolia
  type: relayer
  description: "TODO"
  tags:
    - TODO
datasource:
  location:
    id: voting_lens
  method:
    identifier: "get_result :  (LensArgs) -> (record { vec nat; vec text; vec nat; vec nat; vec nat; vec nat })" // This identifier is defined in artifacts/voting_lens.did
    interface: null
    args: []
destination:
  network_id: 11155111
  type: custom
  oracle_address: 0xa9FAf4c08147f4Cbb022f3c5e666B51Fd7244c44 // ProposalSynchonizer contract address on Sepolia
  rpc_url: https://ethereum-sepolia.blockpi.network/v1/rpc/public
  interface: IVotingSynchronizer.json
  method_name: batchSynchronize
interval: ${INTERVAL}
lens_targets:
  identifiers:
    - voting_event_indexer_scroll_sepolia // Event source. This canister relayes events from Scroll Sepolia to Sepolia.
cycles: null


One of the important things for relayers is to make sure that the data is not duplicated. To do that, we need to keep track of the last block number we have relayed. The following is the code for that.

src/logics/proposal_relayer_sepolia/lib.rs

use std::str::FromStr;

use ic_web3_rs::{ethabi::Address, types::U256};

mod types;
pub type CallCanisterResponse = types::ResponseType;

pub type LensArgs = proposal_relayer_sepolia_bindings::LensArgs;

thread_local! {
    // TODO: handle edge case when tx is not relayed
    static LAST_RELAYED: std::cell::RefCell<u64> = std::cell::RefCell::new(0);
}

// This is call arguments for the lens canister. In this case, we need to provide block number from and block number to we want to get.
// `LAST_RELAYED` is the last block number we have relayed. So, we need to get events from `LAST_RELAYED + 1` to `u64::MAX` to make sure that we don't relay duplicated events.
pub fn call_args() -> (u64, u64) {
    LAST_RELAYED.with_borrow(|r| {
        let last_relayed = r.clone();
        (last_relayed + 1, u64::MAX)
    })
}

// This is a filter function for the lens canister. We need to filter out empty results. If filtered, the canister doesn't call the contract method. In this case, we don't have to call the contract method if there is no event. So we filter out empty results.
pub fn filter(res: &CallCanisterResponse) -> bool {
    if res.0.len() == 0 {
        return false;
    }
    let last_relayed = res.4.iter().max().unwrap().clone();
    LAST_RELAYED.with_borrow_mut(|r| {
        *r = (last_relayed as u64).clone();
    });
    true
}

pub struct ContractCallArgs { // This is automatically generated from interfaces/IVotingSynchronizer.json. So what I only have to do is to implement `call_args`, `filter` and `convert` functions.
    pub ids: Vec<U256>,
    pub proposers: Vec<Address>,
    pub chainIds: Vec<U256>,
    pub startTimestamps: Vec<U256>,
    pub endTimestamps: Vec<U256>,
    pub proposedBlocks: Vec<U256>,
}

// Convert the result of the lens canister into the arguments for the contract method. In this case, we need to convert the result into the format the contract method expects.
pub fn convert(res: &CallCanisterResponse) -> ContractCallArgs {
    let ids = res
        .0
        .clone()
        .iter()
        .map(|x| U256::from(x.clone()))
        .collect::<Vec<U256>>();
    let proposers = res
        .1
        .clone()
        .iter()
        .map(|x| Address::from_str(x).unwrap())
        .collect::<Vec<Address>>();
    let chain_id = res
        .2
        .clone()
        .iter()
        .map(|x| U256::from(x.clone()))
        .collect::<Vec<U256>>();
    let start_timestamps = res
        .3
        .clone()
        .iter()
        .map(|x| U256::from(x.clone()))
        .collect::<Vec<U256>>();
    let end_timestamps = res
        .4
        .clone()
        .iter()
        .map(|x| U256::from(x.clone()))
        .collect::<Vec<U256>>();
    let proposed_blocks = res
        .5
        .clone()
        .iter()
        .map(|x| U256::from(x.clone()))
        .collect::<Vec<U256>>();
    ContractCallArgs {
        ids,
        proposers,
        chainIds: chain_id,
        startTimestamps: start_timestamps,
        endTimestamps: end_timestamps,
        proposedBlocks: proposed_blocks,
    }
}



ProposalRelayerScrollSepolia

This component relays proposals from Sepolia to Scroll Sepolia. This is the same as ProposalRelayerSepolia but it relays proposals from Sepolia to Scroll Sepolia.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/feature/anyrelayer/resources/schema/relayer.json
version: v1
metadata:
  label: Proposal Relayer Scroll Sepolia
  type: relayer
  description: "TODO"
  tags:
    - TODO
datasource:
  location:
    id: proposal_lens
  method: We've created Chainsight components for synchronizing proposals between chains. Now, let's take a look at how to create Chainsight components for synchronizing votes between chains.
    identifier: "get_result : (LensArgs) -> (record { vec nat; vec text; vec nat; vec nat; vec nat; vec nat })"
    interface: null
    args: []
destination:
  network_id: 534351
  type: custom
  oracle_address: 0x400b306fF82EEBac84946F73826B68241421EA6F
  rpc_url: https://sepolia-rpc.scroll.io
  interface: IProposalSynchronizer.json
  method_name: batchSynchronize
interval: ${INTERVAL}
lens_targets:
  identifiers:
    - proposal_factory_event_indexer_sepolia
cycles: null

We've need to write some logics in src/logics/proposal_relayer_sepolia/lib.rs file. But its' completely the same as what we have to write in ProposalRelayerSepolia. And we don't have to write them once again. We can reuse the code by using ProposalRelayerSepolia as a template. The following is the code for that.

src/logics/proposal_relayer_scroll_sepolia/lib.rs

mod types;
pub type CallCanisterResponse = types::ResponseType;
pub type LensArgs = proposal_relayer_sepolia_bindings::LensArgs;
pub fn call_args() -> (u64, u64) {
    proposal_relayer_scroll_sepolia::call_args()
}
pub type ContractCallArgs = proposal_relayer_scroll_sepolia::ContractCallArgs;
pub fn convert(res: &CallCanisterResponse) -> ContractCallArgs {
    proposal_relayer_scroll_sepolia::convert(
        &proposal_relayer_scroll_sepolia::CallCanisterResponse {
            0: res.0.clone(),
            1: res.1.clone(),
            2: res.2.clone(),
            3: res.3.clone(),
            4: res.4.clone(),
            5: res.5.clone(),
        },
    )
}
pub fn filter(res: &CallCanisterResponse) -> bool {
    proposal_relayer_scroll_sepolia::filter(
        &proposal_relayer_scroll_sepolia::CallCanisterResponse {
            0: res.0.clone(),
            1: res.1.clone(),
            2: res.2.clone(),
            3: res.3.clone(),
            4: res.4.clone(),
            5: res.5.clone(),
        },
    )
}


That's it.

VotingEventIndexerSepolia

This component indexes Voted events emitted by Voting contract on Scroll Sepolia. The indexer is deployed on Sepolia. The difference between VotingEventIndexerSepolia and ProposalFactoryEventIndexerSepolia is kind of event they index and the contract address they use.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/main/resources/schema/event_indexer.json
version: v1
metadata:
  label: Voting Event Indexer Scroll
  type: event_indexer
  description: ""
  tags:
    - TODO
datasource:
  id: 0x1A0ceb79B5B2bD56c68C3E23A8a41639a7c12ac6
  event:
    identifier: Voted
    interface: IProposalManager.json
  network:
    rpc_url: https://sepolia-rpc.scroll.io
    chain_id: 534351
  from: 0
  contract_type: ERC-20
interval: ${INTERVAL}
cycles: null

No code is required to write.

VotingEventIndexerScrollSepolia

This is the same as VotingEventIndexerSepolia but it indexes events emitted by Voting contract on Scroll Sepolia.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/main/resources/schema/event_indexer.json
version: v1
metadata:
  label: Voting Event Indexer Scroll Sepolia
  type: event_indexer
  description: ""
  tags:
    - TODO
datasource:
  id: 0x1A0ceb79B5B2bD56c68C3E23A8a41639a7c12ac6
  event:
    identifier: Voted
    interface: IProposalManager.json
  network:
    rpc_url: https://sepolia-rpc.scroll.io
    chain_id: 534351
  from: 0
  contract_type: ERC-20
interval: ${INTERVAL}
cycles: null

VotingLens

This component converts Voted event indexed by VotingEventIndexer into another format to be used by voting relayers.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/main/resources/schema/algorithm_lens.json
version: v1
metadata:
  label: Voting Lens
  type: algorithm_lens
  description: "TODO"
  tags:
    - TODO
datasource:
  methods:
    - id: voting_event_indexer_sepolia
      identifier: "events_from_to : (nat64, nat64) -> (vec record { nat64; vec Voted })"
      candid_file_path: null
with_args: true
cycles: null

We can write some conversion logic in src/logics/voting_lens/lib.rs file. The concept is the same as ProposalLens. But the format of the event data is different. The following is the code for that.

use voting_lens_accessors::*;

pub type LensValue = (
    Vec<u128>,
    Vec<String>,
    Vec<bool>,
    Vec<u128>,
    Vec<u128>,
    Vec<u64>,
);
pub type CalculateArgs = (u64, u64);

pub async fn calculate(targets: Vec<String>, args: CalculateArgs) -> LensValue {
    let r = get_events_from_to_in_voting_event_indexer_sepolia(
        targets.get(0usize).unwrap().clone(),
        args,
    )
    .await;
    if r.is_err() {
        ic_cdk::println!("error: {:?}", r);
    }
    let results = r.unwrap();
    results
        .iter()
        .map(|r| {
            let block = r.0;
            let events = &r.1;
            let e: Vec<(u128, String, bool, u128, u128, u64)> = events
                .iter()
                .map(|e| {
                    (
                        e.id.value.parse().unwrap(),
                        e.voter.clone(),
                        e.support,
                        e.power.value.parse().unwrap(),
                        e.chainId.value.parse().unwrap(),
                        block,
                    )
                })
                .collect();
            e
        })
        .flatten()
        .map(|e| (e.0, e.1, e.2, e.3, e.4, e.5))
        .fold(
            (vec![], vec![], vec![], vec![], vec![], vec![]),
            |mut acc, e| {
                acc.0.push(e.0);
                acc.1.push(e.1);
                acc.2.push(e.2);
                acc.3.push(e.3);
                acc.4.push(e.4);
                acc.5.push(e.5.into());
                acc
            },
        )
}


VotingRelayerSepolia

This component relays votes from Scroll Sepolia to Sepolia.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/feature/anyrelayer/resources/schema/relayer.json
version: v1
metadata:
  label: Voting Relayer Sepolia
  type: relayer
  description: "TODO"
  tags:
    - TODO
datasource:
  location:
    id: voting_lens
  method:
    identifier: "get_result :  (LensArgs) -> (record { vec nat; vec text; vec nat; vec nat; vec nat; vec nat })"
    interface: null
    args: []
destination:
  network_id: 11155111
  type: custom
  oracle_address: 0xa9FAf4c08147f4Cbb022f3c5e666B51Fd7244c44
  rpc_url: https://ethereum-sepolia.blockpi.network/v1/rpc/public
  interface: IVotingSynchronizer.json
  method_name: batchSynchronize
interval: ${INTERVAL}
lens_targets:
  identifiers:
    - voting_event_indexer_scroll_sepolia
cycles: null

We've need to write some logics in src/logics/voting_relayer_sepolia/lib.rs file by the same reason as ProposalRelayerSepolia. The following is the code for that.

mod types;
use std::str::FromStr;

use ic_web3_rs::{ethabi::Address, types::U256};
pub type CallCanisterResponse = types::ResponseType;
pub type LensArgs = voting_relayer_sepolia_bindings::LensArgs;

thread_local! {
    // TODO: handle edge case when tx is not relayed
    static LAST_RELAYED: std::cell::RefCell<u64> = std::cell::RefCell::new(0);
}

pub fn call_args() -> (u64, u64) {
    LAST_RELAYED.with_borrow(|r| {
        let last_relayed = r.clone();
        (last_relayed + 1, u64::MAX)
    })
}
#[derive(Clone, Debug)]
pub struct ContractCallArgs {
    pub ids: Vec<U256>,
    pub voters: Vec<Address>,
    pub _supports: Vec<bool>,
    pub votingPowers: Vec<U256>,
    pub chainIds: Vec<U256>,
}

pub fn convert(res: &CallCanisterResponse) -> ContractCallArgs {
    let ids = res
        .0
        .clone()
        .iter()
        .map(|x| U256::from(x.clone()))
        .collect::<Vec<U256>>();
    let voters = res
        .1
        .clone()
        .iter()
        .map(|x| Address::from_str(x).unwrap())
        .collect::<Vec<Address>>();
    let _supports = res.2.clone().iter().map(|x| x > &0).collect::<Vec<bool>>();
    let voting_powers = res
        .3
        .clone()
        .iter()
        .map(|x| U256::from(x.clone()))
        .collect::<Vec<U256>>();
    let chain_ids = res
        .4
        .clone()
        .iter()
        .map(|x| U256::from(x.clone()))
        .collect::<Vec<U256>>();
    ContractCallArgs {
        ids,
        voters,
        _supports,
        votingPowers: voting_powers,
        chainIds: chain_ids,
    }
}

pub fn filter(res: &CallCanisterResponse) -> bool {
    if res.0.len() == 0 {
        return false;
    }
    let last_relayed = res.5.iter().max().unwrap().clone();
    LAST_RELAYED.with_borrow_mut(|r| {
        *r = (last_relayed as u64).clone();
    });
    true
}


VotingRelayerScrollSepolia

This component relays votes from Sepolia to Scroll Sepolia. This is the same as VotingRelayerSepolia but it relays votes from Sepolia to Scroll Sepolia.

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/feature/anyrelayer/resources/schema/relayer.json
version: v1
metadata:
  label: Voting Relayer Scroll Sepolia
  type: relayer
  description: "TODO"
  tags:
    - TODO
datasource:
  location:
    id: voting_lens
  method:
    identifier: "get_result :  (LensArgs) -> (record { vec nat; vec text; vec nat; vec nat; vec nat; vec nat })"
    interface: null
    args: []
destination:
  network_id: 534351
  type: custom
  oracle_address: 0xE13E832e673dF3588d9a71F67C69826ECC76Cad5
  rpc_url: https://sepolia-rpc.scroll.io
  interface: IVotingSynchronizer.json
  method_name: batchSynchronize
interval: ${INTERVAL}
lens_targets:
  identifiers:
    - voting_event_indexer_scroll_sepolia
cycles: null

We've need to write some logics in src/logics/voting_relayer_scroll_sepolia/lib.rs file by the same reason as ProposalRelayerSepolia. The following is the code for that.

mod types;
pub type CallCanisterResponse = types::ResponseType;
pub type LensArgs = voting_relayer_scroll_sepolia_bindings::LensArgs;
pub fn call_args() -> (u64, u64) {
    voting_relayer_sepolia::call_args()
}

pub type ContractCallArgs = voting_relayer_sepolia::ContractCallArgs;
pub fn convert(res: &CallCanisterResponse) -> ContractCallArgs {
    voting_relayer_sepolia::convert(&voting_relayer_sepolia::CallCanisterResponse {
        0: res.0.clone(),
        1: res.1.clone(),
        2: res.2.clone(),
        3: res.3.clone(),
        4: res.4.clone(),
        5: res.5.clone(),
    })
}
pub fn filter(res: &CallCanisterResponse) -> bool {
    voting_relayer_sepolia::filter(&voting_relayer_sepolia::CallCanisterResponse {
        0: res.0.clone(),
        1: res.1.clone(),
        2: res.2.clone(),
        3: res.3.clone(),
        4: res.4.clone(),
        5: res.5.clone(),
    })
}

That's it.

The part I'd like to emphasize is that we don't have to write any code for transparency, security and scalability. We can focus on writing business logics. Usually we have to write a lot of code for that, especially specific cases like multi-chain voting. But using Chainsight and deploying canisters on the Internet Computer, we can focus on writing business logics.

In this post, I've written about how to create Chainsight components for synchronizing proposals and votes between chains. In the next post, I will deploy the canisters on the Internet Computer and show you how to use them.