Mirrors

Syndicate’s mirrors allow you to send data from blockchains or HTTP resources to target blockchains. This becomes useful when creating cross-chain applications that could benefit from data that lives outside of a single blockchain.

Dependencies

Our data mirror service is a simple HTTP server that listens to events on desired blockchains or polls HTTP resources and allows you to broadcast transactions to target blockchains using Syndicate’s API.

Before getting started, ensure you have created an account with Syndicate. Additionally make sure you have bun installed on your system.

Next, clone the mirror repository and add your Syndicate project ID and API key to your environment

$touch .env
.env
$SYNDICATE_API_KEY=
>SYNDICATE_PROJECT_ID=

Install dependencies

$bun i

Run the server

$bun dev

Upon starting the server two listeners will be initialized, a ChainListener and an HttpListener.

These two listeners implement the logic necessary for retreiving data from their sources

  • ChainListener: queries events emitted on a blockchain
  • HttpListener: polls HTTP resources

ChainListener

On start, chain listeners will immediately query all events emitted from the fromBlock to the most recent block and will continue listening to all future events. An example is shown below for listening to all purchases of a CryptoPunk and minting an NFT for the purchaser on Base.

The following parameters can be passed to define a ChainListener:

  • rpcUrl: the RPC URL of the target chain you are mirroring data from
  • fromBlock: the block from which you want to start mirroring
  • event: the ABI of the event emitted from contractAddress
  • contractAddress: the source contract address that emits an event
  • onData: callback that receives all data emitted in the event
  • pollingInterval: (optional) value in seconds to poll for event
1import { waitForHash } from '@syndicateio/syndicate-node/utils'
2import { v5 } from 'uuid'
3import { formatEther, parseAbiItem } from 'viem'
4import syndicate from '~/clients/syndicate'
5import { env } from '~/env'
6import { ChainListener } from '~/listeners/chain'
7
8const ETH_MAINNET_RPC_URL = 'https://eth.drpc.org'
9const PUNK_LISTEN_FROM_BLOCK = BigInt(19963995)
10const PUNK_ADDRESS = '0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB'
11const PUNK_BOUGHT_EVENT = parseAbiItem(
12 'event PunkBought(uint indexed punkIndex, uint value, address indexed fromAddress, address indexed toAddress)'
13)
14
15const TARGET_CONTRACT_ADDRESS = '0x1194b2a8fff08f6518af7d66b072938e35c71706' // NFT contract address to be minted to purchaser
16const TARGET_CHAIN_ID = 8_453 // Base chain ID
17
18const chainListener = new ChainListener({
19 rpcUrl: ETH_MAINNET_RPC_URL,
20 fromBlock: PUNK_LISTEN_FROM_BLOCK,
21 event: PUNK_BOUGHT_EVENT,
22 contractAddress: PUNK_ADDRESS,
23 onData: async ({ logs }) => {
24 const promises = logs.map(
25 async ({
26 args: { punkIndex, fromAddress, toAddress },
27 transactionHash
28 }) => {
29 // We generate a unique uuid to prevent duplicate transactions
30 const requestId = v5(
31 `${transactionHash}-${punkIndex}-${fromAddress}-${toAddress}`,
32 v5.URL
33 )
34
35 // Mint an NFT on base to the purchaser of a cryptopunk
36 const { transactionId } = await syndicate.transact
37 .sendTransaction({
38 requestId,
39 chainId: TARGET_CHAIN_ID,
40 contractAddress: TARGET_CONTRACT_ADDRESS,
41 projectId: env.SYNDICATE_PROJECT_ID,
42 functionSignature: 'mintTo(address to)',
43 args: {
44 to: args.toAddress
45 }
46 })
47 .catch((_) => {
48 throw new Error(
49 `request ID: ${requestId} has already been processed`
50 )
51 })
52 const hash = await waitForHash(syndicate, {
53 transactionId,
54 projectId: env.SYNDICATE_PROJECT_ID
55 })
56 console.log(`got hash: ${hash} for transaction ID: ${transactionId}`)
57 }
58 )
59
60 const settled = await Promise.allSettled(promises)
61 const rejected = settled.filter((p) => p.status === 'rejected')
62 if (rejected.length > 0) {
63 console.error(`rejected: ${rejected.length}`)
64 for (const promise of rejected) {
65 console.error(promise)
66 }
67 }
68 }
69})
70
71chainListener.init()

HttpListener

An HttpListener allows you to query multiple HTTP resources and broadcast data to a target blockchain.

The following parameters can be passed to define an HttpListener

  • getters: array of HttpGetters used to query HTTP resources and normalize data utilizing their onJson callback
  • onData: callback that receives data from the HTTP resources
  • pollingInterval: (optional) value in seconds to poll the HTTP resources
1import { waitForHash } from '@syndicateio/syndicate-node/utils'
2import syndicate from '~/clients/syndicate'
3import { env } from '~/env'
4import { HttpGetter } from '~/getters/httpGetter'
5import { HttpListener } from '../../http'
6
7const TARGET_CONTRACT_ADDRESS = '0x2a3B6469F084Fe592BC50EeAbC148631AdAcB92e' // Contract address on Degen
8const DEGEN_CHAIN_ID = 666_666_666 // Degen chain ID
9
10type CoinGeckoResponse = {
11 'degen-base': { usd: number }
12}
13
14const httpListener = new HttpListener<number>({
15 getters: [
16 new HttpGetter<CoinGeckoResponse, number>({
17 url: 'https://api.coingecko.com/api/v3/simple/price?ids=degen-base&vs_currencies=usd',
18 onJson: (json) => {
19 return json['degen-base'].usd
20 }
21 })
22 ],
23 onData: async ({ fulfilled: prices, logger }) => {
24 const { transactionId } = await syndicate.transact
25 .sendTransaction({
26 chainId: DEGEN_CHAIN_ID,
27 contractAddress: TARGET_CONTRACT_ADDRESS,
28 projectId: env.SYNDICATE_PROJECT_ID,
29 functionSignature: 'updatePrice(uint256 price)',
30 args: { price: prices[0] }
31 })
32 .catch((_) => {
33 throw new Error('Could not get price')
34 })
35 const hash = await waitForHash(syndicate, {
36 transactionId,
37 projectId: env.SYNDICATE_PROJECT_ID
38 })
39 console.log(`got hash: ${hash} for transaction ID: ${transactionId}`)
40 }
41})
42
43httpListener.init()

Hosting

Our data mirror service is open source and should be self-hosted. It does not use external dependencies such as a database or memory-cache and it can be deployed to any cloud provider such as Render, fly.io, Heroku. When hosting you will want to ensure the instance is always on so the service is able to continually poll resources. A Dockerfile can be found in the repository here if you are running your service in a container.