A robust TypeScript/Bun backend relayer for cross-chain operations. This relayer monitors blockchain events on source chains, requests proofs, and executes transactions on destination chains.
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Source Chain β β Proof API β β Dest Chain β
β Listeners ββββββ Service ββββββ Executors β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β β β
βββββββββββββββββββββββββΌββββββββββββββββββββββββ
β
βββββββββββββββββββ
β Job Queue β
β & SQLite DB β
βββββββββββββββββββ
Under the hood, the relayer operates as a stateful, service-oriented application. Listeners are dedicated to monitoring specific smart contract events on configured source chains. When a target event is detected, a job is created and enqueued into a persistent SQLite database, ensuring no events are lost. A central Job Queue processes these jobs based on their status. If a job requires cross-chain verification, the Proof Service communicates with an external API (e.g., the Polymer testnet proof API) to fetch a cryptographic proof. Once the proof is secured, an Executor service constructs and dispatches the final transaction to the destination chain, completing the cross-chain action. This architecture ensures reliability through persistent state and a robust retry mechanism for each stage of the process.
- Configuration-Driven: Easily manage chains, contracts, and event mappings via a central JSON file.
- Multi-chain Support: Monitor multiple blockchains and relay between them simultaneously.
- Reliable & Resilient: Persistent job queue with automatic retries and state tracking in an SQLite database.
- Pluggable Proofs: Integrates with proof generation services like the Polymer API for verified cross-chain data.
- Dynamic Destination Resolvers: Sophisticated logic to determine where to send transactions based on event data.
- Type-Safe & Modern: Built with TypeScript and Bun for performance and developer experience.
- Metrics & Logging: Built-in, configurable logging and performance metrics.
- Optional Web UI: Includes a modern React-based dashboard for monitoring relayer activity.
# Install dependencies
bun install
# Start the development server with auto-reload
bun run dev
# Or build and run for production
bun run build
bun run start
The relayer includes an optional web-based dashboard for monitoring and managing the database:
# Navigate to the UI directory
cd ui
# Install UI dependencies
npm run install-all
# Start the web interface
npm run dev
The UI will be available at http://localhost:3000
and provides:
- Real-time job monitoring and filtering
- Chain state visualization
- Statistics and metrics dashboard
- Detailed job inspection
For more details, see ui/README.md.
For a consistent and portable environment, you can build and run the relayer using Docker.
-
Create a
.env
file at the root of the project. This is necessary because the Docker container will need the environment variables to configure itself. You can copy the example:cp .env.example .env
Then, edit the
.env
file with your RPC URLs and private key. -
Build the Docker image:
docker build -t polymer-relayer .
-
Run the Docker container:
docker run --rm -it --env-file .env polymer-relayer
The
--env-file
flag securely passes your environment variables to the container. The container will automatically use therelayer.config.json
from the source code.
The entire relayer is controlled by a single relayer.config.json
file. Use environment variables (e.g., ${PRIVATE_KEY}
) for sensitive data.
A fully documented version of the configuration can be found at src/config/relayer.config.documented.jsonc
.
Below is a summary of the main sections:
Define the blockchains and smart contracts the relayer will interact with.
{
"chains": {
"baseSepolia": {
"chainId": 84532,
"rpc": "${BASE_SEPOLIA_RPC_URL}",
"privateKey": "${PRIVATE_KEY}",
"pollingInterval": 2000,
"blockConfirmations": 1,
"gasMultiplier": 1.1
},
"arbitrumSepolia": {
"chainId": 421614,
"rpc": "${ARBITRUM_SEPOLIA_RPC_URL}",
"privateKey": "${PRIVATE_KEY}",
"pollingInterval": 1000,
"blockConfirmations": 1,
"gasMultiplier": 1.0
}
},
"contracts": {
"StateSyncV2": {
"baseSepolia": {
"address": "0x53489524c94f1A9197c3399435013054174b121d",
"type": "source"
},
"arbitrumSepolia": {
"address": "0x324670730736B72A3EA73067F30c54F03B23934E",
"type": "destination"
}
}
}
}
This is the core logic, linking a source event to a destination contract call.
{
"eventMappings": [
{
"name": "ValueSetCrossChain",
"sourceEvent": {
"contractName": "StateSyncV2",
"eventName": "ValueSet",
"eventSignature": "ValueSet(string key, bytes value)"
},
"destinationCall": {
"contractName": "StateSyncV2",
"methodName": "setValueFromSource",
"methodSignature": "setValueFromSource(string key, bytes value, bytes proof)"
},
"destinationResolver": "baseToArbitrum",
"proofRequired": true,
"enabled": true
}
]
}
Define strategies to determine the destination chain for a given event.
{
"destinationResolvers": {
"baseToArbitrum": {
"type": "static",
"destinations": ["arbitrumSepolia"]
}
}
}
{
"proofApi": {
"baseUrl": "https://proof.testnet.polymer.zone",
"timeout": 30000,
"retryAttempts": 3
},
"database": {
"path": "./relayer.db"
},
"logging": {
"level": "info",
"enableFileLogging": false
}
}
The relayer will load its configuration from relayer.config.json
by default.
# Development mode with file watching
bun run dev
# Production mode
bun run start
# Use a custom config file path
CONFIG_PATH=/path/to/your/config.json bun run start
The application uses SQLite for state management. A few helpful scripts are included in package.json
:
# Initialize the database schema (only needs to be run once)
bun run db:init
# View the contents of the jobs table
bun run db:view
# Clear the database for a fresh start
bun run db:clear
CREATE TABLE jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
unique_id TEXT UNIQUE NOT NULL,
source_chain TEXT NOT NULL,
source_tx_hash TEXT NOT NULL,
source_block_number INTEGER NOT NULL,
dest_chain TEXT NOT NULL,
dest_address TEXT NOT NULL,
dest_method TEXT NOT NULL,
dest_method_signature TEXT,
event_data TEXT NOT NULL,
proof_data TEXT,
status TEXT NOT NULL DEFAULT 'pending',
proof_required BOOLEAN DEFAULT 0,
dest_tx_hash TEXT,
retry_count INTEGER DEFAULT 0,
error_message TEXT,
created_at TEXT NOT NULL,
completed_at TEXT,
last_retry_at TEXT,
mapping_name TEXT
);
CREATE TABLE chain_state (
chain_name TEXT PRIMARY KEY,
last_processed_block INTEGER NOT NULL,
updated_at TEXT NOT NULL
);
- Event Detection:
ChainListener
services monitor each source chain for configured events. - Job Creation: When a valid event is found, a new job is created in the
jobs
table with apending
status. - Proof Request: The
JobQueue
sees thepending
job. IfproofRequired
is true, it calls theProofService
to fetch a proof from the Polymer API. The job status becomesproof_requested
. - Execution: Once the proof is returned, the job status changes to
proof_ready
. TheJobQueue
then passes the job to the appropriateExecutorService
, which builds and sends the transaction to the destination chain. - Completion: The job is marked as
completed
with the destination transaction hash. If any step fails, the job is markedfailed
, and theJobQueue
will retry it up to a configured limit.
pending
: Waiting for proof request (if required) or execution.proof_requested
: Proof API call is in progress.proof_ready
: Proof has been obtained, ready for execution.executing
: The transaction is being submitted to the destination chain.completed
: The transaction has been successfully confirmed on-chain.failed
: The job has failed. It will be retried automatically.
src/
βββ config/ # Relayer configuration files
βββ listeners/ # Blockchain event listeners (ChainListener)
βββ services/ # Core services (Database, Proof, Executor, DestinationResolver)
βββ queue/ # Job queue and processors (JobQueue)
βββ types/ # TypeScript type definitions
βββ utils/ # Utilities (logger, metrics, config loader)
βββ main.ts # Application entry point (RelayerApp)
ui/ # Optional web interface
βββ src/ # React frontend
βββ server/ # Express API server
βββ README.md # UI documentation
- Add the new chain and/or contract details to your
relayer.config.json
file. - Restart the relayer.
The application will automatically create the necessary listeners and executors based on your configuration. No code changes are needed for simple additions.
Logs are written to the console with clear, concise messages. The log level can be configured in relayer.config.json
.
βΉοΈ Starting Polymer Relayer { chains: [ 'baseSepolia', 'arbitrumSepolia' ], ... }
βΉοΈ Initialized chain: baseSepolia { startBlock: 27379492, ... }
βΉοΈ Created job: ValueSetCrossChain { jobId: 1, sourceTx: '0x...', ... }
βΉοΈ Requesting proof for job 1 { txHash: '0x...', chain: 'baseSepolia' }
βΉοΈ Proof obtained for job 1
βΉοΈ Executing job 1: ValueSetCrossChain { destChain: 'arbitrumSepolia', ... }
βΉοΈ Transaction submitted { txHash: '0x...', method: 'setValueFromSource' }
βΉοΈ Transaction confirmed { txHash: '0x...', blockNumber: 165808112 }
βΉοΈ Job 1 completed: ValueSetCrossChain { destTxHash: '0x...' }
# Run tests
bun test
The relayer includes robust error handling:
- Automatic Retries: Failed jobs are automatically retried with a delay.
- Persistent Job Queue: Jobs are stored in SQLite, so they are not lost if the relayer restarts.
- Graceful Shutdown: On SIGINT/SIGTERM, the relayer finishes in-flight work before exiting.
- Configuration Validation: The relayer validates your configuration file on startup to catch errors early.
- Implement faster event indexing using Eventeum architecture
- Refactor execution layer into dedicated gas and nonce manager classes
- Add multi-threaded wallet support with circuit breaker pattern for stuck transactions
- Enhanced logging system
- Metrics export for Prometheus/Grafana
- Health check endpoints
This project is licensed under the MIT License.
For questions and support:
- Create an issue on GitHub
- Join our Discord community
- Read the documentation at docs.polymerlabs.org