Channels
Overview
Channels is a plugin for OpenZeppelin Relayer that enables parallel transaction submission on Stellar using channel accounts with automatic fee bumping. Channel accounts provide unique sequence numbers for concurrent transaction processing, eliminating sequence number conflicts.
The plugin features:
- Parallel Transaction Processing: Uses a pool of channel accounts for concurrent submissions
- Automatic Fee Bumping: Dedicated fund account pays transaction fees
- Dynamic Pool Management: Channel accounts acquired and released automatically
- Transaction Simulation: Automatically simulates and builds transactions with proper resources
- Management API: Dynamic configuration of channel accounts
Using the Plugin Client
The fastest way to interact with Channels is through the plugin's TypeScript client, which provides a type-safe, unified interface for transaction submission and management operations.
Installation
npm install @openzeppelin/relayer-plugin-channels
# or
pnpm add @openzeppelin/relayer-plugin-channelsClient Setup
The client supports two connection modes that are automatically detected based on configuration:
import { ChannelsClient } from "@openzeppelin/relayer-plugin-channels";
// Direct HTTP connection to Channels service
const directClient = new ChannelsClient({
baseUrl: "https://channels.openzeppelin.com", // Channels service URL
apiKey: process.env.CHANNELS_API_KEY, // Service API key
adminSecret: process.env.CHANNELS_ADMIN, // Optional: for management
timeout: 30000, // Optional: request timeout
});
// Connection via OpenZeppelin Relayer plugin
const relayerClient = new ChannelsClient({
pluginId: "channels-plugin", // Plugin identifier (triggers relayer mode)
baseUrl: "http://localhost:8080", // Relayer URL
apiKey: process.env.RELAYER_API_KEY, // Relayer API key (sent as Bearer AND custom header)
apiKeyHeader: "x-api-key", // Optional: header for fee tracking (default: x-api-key)
adminSecret: process.env.CHANNELS_ADMIN, // Optional: for management
});Submitting Transactions
Channels provides separate methods for different transaction types:
// Submit signed transaction (XDR)
const result = await client.submitTransaction({
xdr: "AAAAAgAAAABQEp+s8xGPrF...", // Complete signed envelope
});
// Submit Soroban transaction (func + auth)
const result = await client.submitSorobanTransaction({
func: "AAAABAAAAAEAAAAGc3ltYm9s...", // Host function XDR
auth: ["AAAACAAAAAEAAAA...", "AAAACAAAAAEAAAB..."], // Detached auth entries
});
console.log("Transaction:", result.transactionId, result.hash, result.status);Managing Channel Accounts
For managing channel accounts, the SDK provides the following methods:
// List configured channel accounts
const accounts = await client.listChannelAccounts();
console.log("Channel accounts:", accounts.relayerIds);
// Update channel accounts (requires adminSecret)
const result = await client.setChannelAccounts([
"channel-001",
"channel-002",
"channel-003",
]);
console.log("Update successful:", result.ok, result.appliedRelayerIds);
// Query fee usage (requires adminSecret)
const usage = await client.getFeeUsage("client-api-key");
console.log("Fee usage:", usage);Example Scripts
For complete working examples and additional client usage details, see the Channels Plugin README.
Prerequisites
- Node.js >= 18
- pnpm >= 10
- OpenZeppelin Relayer (installed and configured)
Example Setup
For a complete working example with Docker Compose, refer to the Channels plugin example in the OpenZeppelin Relayer repository:
- Location: examples/channels-plugin-example
- Documentation: README.md
Installation
Channels can be added to any OpenZeppelin Relayer installation.
Resources:
- npm Package: @openzeppelin/relayer-plugin-channels
- GitHub Repository: https://github.com/OpenZeppelin/relayer-plugin-channels
- Example Setup: channels-plugin-example
Install from npm
# From the root of your Relayer repository
cd plugins
pnpm add @openzeppelin/relayer-plugin-channelsCreate the plugin wrapper
Inside your Relayer, create a directory for the plugin and expose its handler:
mkdir -p plugins/channelsCreate plugins/channels/index.ts:
export { handler } from "@openzeppelin/relayer-plugin-channels";Configuration
Plugin Registration
Register the plugin in your config/config.json file:
{
"plugins": [
{
"id": "channels-plugin",
"path": "channels/index.ts",
"timeout": 60
}
]
}Environment Variables
Configure the required environment variables:
# Required environment variables
export STELLAR_NETWORK="testnet" # or "mainnet"
export FUND_RELAYER_ID="channels-fund" # ID of the fund relayer
export PLUGIN_ADMIN_SECRET="your-secret-here" # Required for management API
# Optional environment variables
export LOCK_TTL_SECONDS=30 # Lock timeout (default: 30, range: 3-30)
export MAX_FEE=1000000 # Maximum fee in stroops (default: 1,000,000)Required Variables:
STELLAR_NETWORK: Either "testnet" or "mainnet"FUND_RELAYER_ID: Relayer ID for the account that pays transaction fees
Optional Variables:
PLUGIN_ADMIN_SECRET: Secret for accessing the management API (required to manage channel accounts)LOCK_TTL_SECONDS: TTL for channel account locks in seconds (default: 30, range: 3-30)MAX_FEE: Maximum transaction fee in stroops (default: 1,000,000)FEE_LIMIT: Fair use limit for fee consumption per API key in stroops. When exceeded, requests are rejected withFEE_LIMIT_EXCEEDEDerror (when not set, fee tracking is disabled)FEE_RESET_PERIOD_SECONDS: Reset fee consumption every N seconds (e.g., 86400 = 24 hours). When configured, each API key's fee consumption resets automatically after the period expiresAPI_KEY_HEADER: HTTP header name used to extract the client API key for fee tracking (default: x-api-key)LIMITED_CONTRACTS: Comma-separated list of contract addresses with restricted pool access. Comma separation will be trimmed and can handle spaces.CONTRACT_CAPACITY_RATIO: Ratio (0-1) determining what portion of the channel pool limited contracts can access (default: 0.8). The ratio is converted using Math.floor() which always rounds DOWN to the nearest integer, with a minimum guarantee of 1 channel.
Contract Capacity Limits
Channels supports per-contract capacity limits to prevent high-volume contracts from monopolizing the channel pool. This feature ensures fair resource distribution by restricting certain contracts to a subset of available channels while allowing other contracts full pool access.
How It Works:
When a transaction targets a contract listed in LIMITED_CONTRACTS, Channels restricts that transaction to a deterministic subset of the channel pool based on the configured CONTRACT_CAPACITY_RATIO. The subset is selected using hash-based partitioning, ensuring consistent channel assignment for each limited contract.
Configuration:
# Restrict high-volume contracts to 80% of the channel pool
export LIMITED_CONTRACTS="CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC,CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
export CONTRACT_CAPACITY_RATIO=0.8Behavior Notes:
- Case-insensitive matching: Contract addresses are normalized to uppercase for comparison
- Minimum guarantee: Limited contracts are always guaranteed at least 1 channel, even with low capacity ratios
- Unlisted contracts: Contracts not in
LIMITED_CONTRACTSretain full access to all channels - Deterministic selection: The same contract always gets the same subset of channels, ensuring predictable behavior
Relayer Configuration
Channels requires two types of relayers:
- Fund Account: The account that pays transaction fees (should have
concurrent_transactions: trueenabled) - Channel Accounts: At least one channel account (recommended: 2 or more for better throughput)
Configure relayers in your config/config.json:
{
"relayers": [
{
"id": "channels-fund",
"name": "Channels Fund Account",
"network": "testnet",
"paused": false,
"network_type": "stellar",
"signer_id": "channels-fund-signer",
"policies": {
"concurrent_transactions": true
}
},
{
"id": "channel-001",
"name": "Channel Account 001",
"network": "testnet",
"paused": false,
"network_type": "stellar",
"signer_id": "channel-001-signer"
},
{
"id": "channel-002",
"name": "Channel Account 002",
"network": "testnet",
"paused": false,
"network_type": "stellar",
"signer_id": "channel-002-signer"
}
],
"notifications": [],
"signers": [
{
"id": "channels-fund-signer",
"type": "local",
"config": {
"path": "config/keys/channels-fund.json",
"passphrase": {
"type": "env",
"value": "KEYSTORE_PASSPHRASE_FUND"
}
}
},
{
"id": "channel-001-signer",
"type": "local",
"config": {
"path": "config/keys/channel-001.json",
"passphrase": {
"type": "env",
"value": "KEYSTORE_PASSPHRASE_CHANNEL_001"
}
}
},
{
"id": "channel-002-signer",
"type": "local",
"config": {
"path": "config/keys/channel-002.json",
"passphrase": {
"type": "env",
"value": "KEYSTORE_PASSPHRASE_CHANNEL_002"
}
}
}
],
"networks": "./config/networks",
"plugins": [
{
"id": "channels",
"path": "channel/index.ts",
"timeout": 30,
"emit_logs": true,
"emit_traces": true
}
]
}Important Configuration Notes:
- Fund Account (
channels-fund): Must have"concurrent_transactions": truein policies to enable parallel transaction processing - Channel Accounts: Create at least 2 for better throughput (you can add more as
channel-003, etc.) - Network: Use
testnetfor testing ormainnetfor production - Signers: Each relayer references a signer by
signer_id, and signers are defined separately with keystore paths - Keystore Files: You’ll need to create keystore files for each account - see OpenZeppelin Relayer documentation for details on creating and managing keys
- Plugin Registration: The plugin
idshould match what you use in environment variables and API calls
After configuration, fund these accounts on-chain and register them with Channels (see "Initializing Channel Accounts" below).
Initializing Channel Accounts
After configuring your relayers in config.json and funding the Stellar accounts, register them with Channels via the Management API:
curl -X POST http://localhost:8080/api/v1/plugins/channels-plugin/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "setChannelAccounts",
"adminSecret": "your-secret-here",
"relayerIds": ["channel-001", "channel-002"]
}
}
}'Response:
{
"success": true,
"data": {
"ok": true,
"appliedRelayerIds": ["channel-001", "channel-002"]
},
"error": null
}This tells Channels which relayers to use as channel accounts. All relayer IDs must match your configured relayer IDs in config.json.
Channels is now ready to serve Soroban transactions.
Automated Setup
To skip the manual configuration steps, use the provided automation script. It automates the entire setup process: creating signers and relayers via the API, funding accounts on-chain, and registering them with Channels.
Prerequisites
When using the automated setup, you only need to configure and fund the fund account:
{
"relayers": [
{
"id": "channels-fund",
"chain": "stellar",
"signer": "channels-fund-signer",
"policies": {
"concurrent_transactions": true
}
}
]
}The script creates all channel account signers and relayers dynamically - no config.json entries needed for channel accounts.
Running the Script
pnpm exec tsx ./scripts/create-channel-accounts.ts \
--total 3 \
--base-url http://localhost:8080 \
--api-key <RELAYER_API_KEY> \
--funding-relayer channels-fund \
--plugin-id channels-plugin \
--plugin-admin-secret <PLUGIN_ADMIN_SECRET> \
--network testnetWhat the Script Does
- Creates channel account signers and relayers via API: Following the naming pattern
channel-0001,channel-0002, etc. - Funds channel accounts on-chain: Submits funding transactions through the fund relayer and waits for confirmation
- Registers with Channels: Automatically calls the Management API to register all channel accounts
Script Options
--total: Number of channel accounts to create (recommended: 2-3 for testing, more for production)--fix: Audit and heal partially created state (use if the script was interrupted)--dry-run: Preview actions without making changes--prefix: Customize the naming prefix (default:channel-)--starting-balance: XLM amount for each account (default: 5)
Script Location
- Example directory:
scripts/create-channel-accounts.ts
API Usage
Channels is invoked by making POST requests to the plugin endpoint:
POST /api/v1/plugins/{plugin-id}/callSubmitting Transactions
There are two ways to submit transactions to Channels:
Option 1: Complete Transaction XDR
Submit a complete, signed transaction envelope:
curl -X POST http://localhost:8080/api/v1/plugins/channels-plugin/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"xdr": "AAAAAgAAAAA..."
}
}'Option 2: Soroban Function + Auth
Submit just the Soroban function and authorization entries:
curl -X POST http://localhost:8080/api/v1/plugins/channels-plugin/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"func": "AAAABAAAAAEAAAAGc3ltYm9s...",
"auth": ["AAAACAAAAAEAAAA..."]
}
}'Parameters
xdr(string): Complete transaction envelope XDR (base64) - must be signed, not a fee-bump envelopefunc(string): Soroban host function XDR (base64)auth(array of strings): Array of Soroban authorization entry XDRs (base64)
Important Notes:
- Provide either
xdrORfunc+auth, not both - When using
xdr, the transaction must be a regular signed transaction (not a fee-bump envelope) - When using
func+auth, Channels will build and simulate the transaction automatically - Transactions are always submitted with fee bumping from the fund account
Generating XDR with Stellar SDK
Use the @stellar/stellar-sdk to generate the required XDR values:
Who Signs the Transaction?
The transaction must be signed by the user's keypair (the account invoking the smart contract), not the channel or fund accounts. Channels handles fee payment separately through fee bumping - your transaction signature authorizes the contract call, while the fund account pays the network fees.
For Soroban contract calls:
- Full XDR mode: Sign the entire transaction envelope with the invoker's key
- func + auth mode: Sign only the authorization entries with the invoker's key
Tools for Creating and Signing Transactions
While you can use the Stellar SDK programmatically, several tools make the process easier:
Stellar Laboratory - Official web-based tool for:
- Building and signing transactions visually
- Inspecting and decoding XDR data
Popular Stellar Wallets with XDR Signing Support:
- Freighter - Browser extension wallet with Soroban support and XDR signing
- Albedo - Web-based wallet supporting transaction signing and XDR export
- Lobstr - Mobile and web wallet with transaction signing capabilities
These wallets can sign transactions and export the signed XDR for submission to Channels.
Full Transaction Envelope XDR
import { Networks, TransactionBuilder, rpc } from "@stellar/stellar-sdk";
// Build your transaction
const tx = new TransactionBuilder(account, {
fee: "100",
networkPassphrase: Networks.TESTNET,
})
.addOperation(/* Operation.invokeHostFunction from Contract.call(...) */)
.setTimeout(30)
.build();
// Sign with the USER's keypair (the account invoking the contract)
// This is NOT the channel or fund account - it's your end user's key
tx.sign(userKeypair);
// Export base64 envelope XDR for submission to Channels
const envelopeXdr = tx.toXDR();Soroban Function + Auth XDRs
// Build and simulate first to obtain auth
const baseTx = /* TransactionBuilder(...).addOperation(...).build() */;
const sim = await rpcServer.simulateTransaction(baseTx);
// Apply simulation, then extract from the InvokeHostFunction operation
const assembled = rpc.assembleTransaction(baseTx, sim).build();
const op = assembled.operations[0]; // Operation.InvokeHostFunction
const funcXdr = op.func.toXDR("base64");
const authXdrs = (op.auth ?? []).map(a => a.toXDR("base64"));Management API
Channels provides a management API to dynamically configure channel accounts. This API requires authentication via the PLUGIN_ADMIN_SECRET environment variable.
List Channel Accounts
Get the current list of configured channel accounts:
curl -X POST http://localhost:8080/api/v1/plugins/channels-plugin/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "listChannelAccounts",
"adminSecret": "your-secret-here"
}
}
}'Response:
{
"success": true,
"data": {
"relayerIds": ["channel-001", "channel-002"]
},
"error": null
}Set Channel Accounts
Configure the channel accounts that Channels will use. This replaces the entire list:
curl -X POST http://localhost:8080/api/v1/plugins/channels-plugin/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "setChannelAccounts",
"adminSecret": "your-secret-here",
"relayerIds": ["channel-001", "channel-002", "channel-003"]
}
}
}'Response:
{
"success": true,
"data": {
"ok": true,
"appliedRelayerIds": ["channel-001", "channel-002", "channel-003"]
},
"error": null
}Important Notes:
- You must configure at least one channel account before Channels can process transactions
- The management API will prevent removing accounts that are currently locked (in use). On failure it returns HTTP 409 with code
LOCKED_CONFLICTanddetails.lockedlisting the blocked IDs - All relayer IDs must exist in your OpenZeppelin Relayer configuration
- The
adminSecretmust match thePLUGIN_ADMIN_SECRETenvironment variable
Get Fee Usage
Query fee consumption for a specific API key (requires FEE_LIMIT to be configured):
curl -X POST http://localhost:8080/api/v1/plugins/channels-plugin/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "getFeeUsage",
"adminSecret": "your-secret-here",
"apiKey": "client-api-key-to-check"
}
}
}'Response:
{
"success": true,
"data": {
"consumed": 500000,
"limit": 1000000,
"remaining": 500000,
"periodStartAt": "2024-01-15T00:00:00.000Z",
"periodEndsAt": "2024-01-16T00:00:00.000Z"
},
"error": null
}Response Fields:
consumed: Fee used in stroopslimit: Effective fee limit (custom if set, otherwise default) - undefined if unlimitedremaining: Remaining fee budget - undefined if unlimitedperiodStartAt: ISO 8601 timestamp when current period started (if reset period configured)periodEndsAt: ISO 8601 timestamp when period will reset (if reset period configured)
Get Fee Limit
Query fee limit configuration for an API key:
curl -X POST http://localhost:8080/api/v1/plugins/channels-plugin/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "getFeeLimit",
"adminSecret": "your-secret-here",
"apiKey": "client-api-key-to-query"
}
}
}'Response:
{
"success": true,
"data": {
"limit": 500000
},
"error": null
}Response Fields:
limit: Fee limit (custom if set, otherwise default), undefined if unlimited
Set Fee Limit
Set a custom fee limit for an API key:
curl -X POST http://localhost:8080/api/v1/plugins/channels-plugin/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "setFeeLimit",
"adminSecret": "your-secret-here",
"apiKey": "client-api-key",
"limit": 500000
}
}
}'Response:
{
"success": true,
"data": {
"ok": true,
"limit": 500000
},
"error": null
}Delete Fee Limit
Remove custom limit for an API key (reverts to default):
curl -X POST http://localhost:8080/api/v1/plugins/channels-plugin/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "deleteFeeLimit",
"adminSecret": "your-secret-here",
"apiKey": "client-api-key"
}
}
}'Response:
{
"success": true,
"data": {
"ok": true
},
"error": null
}Responses
All API responses use the standard Relayer envelope format: success, data, error, metadata.
Success Response (HTTP 200)
{
"success": true,
"data": {
"transactionId": "tx_123456",
"status": "confirmed",
"hash": "1234567890abcdef..."
},
"error": null
}Response Fields:
success:truewhen the plugin executed successfullydata: Contains the transaction resulttransactionId: The OpenZeppelin Relayer transaction IDstatus: Transaction status (e.g., "confirmed")hash: The Stellar transaction hasherror:nullon success
Error Response (HTTP 4xx)
{
"success": false,
"data": {
"code": "POOL_CAPACITY",
"details": {}
},
"error": "Too many transactions queued. Please try again later",
"metadata": {
"logs": [{ "level": "error", "message": "All channel accounts in use" }]
}
}Error Response Fields:
success:falsewhen the plugin encountered an errordata: Contains error detailscode: Error code (e.g., "POOL_CAPACITY", "LOCKED_CONFLICT")details: Additional context about the errorerror: Human-readable error messagemetadata.logs: Plugin execution logs (ifemit_logsis enabled)
Common Error Codes
CONFIG_MISSING: Missing required environment variableUNSUPPORTED_NETWORK: Invalid network typeINVALID_PARAMS: Invalid request parametersINVALID_XDR: Failed to parse XDRINVALID_ENVELOPE_TYPE: Not a regular transaction envelope (e.g., fee bump)INVALID_TIME_BOUNDS: TimeBounds too far in the futureNO_CHANNELS_CONFIGURED: No channel accounts have been configured via management APIPOOL_CAPACITY: All channel accounts in useRELAYER_UNAVAILABLE: Relayer not foundSIMULATION_FAILED: Transaction simulation failedONCHAIN_FAILED: Transaction failed on-chainWAIT_TIMEOUT: Transaction wait timeoutMANAGEMENT_DISABLED: Management API not enabledUNAUTHORIZED: Invalid admin secretLOCKED_CONFLICT: Cannot remove locked channel accountsFEE_LIMIT_EXCEEDED: API key has exceeded its fair use fee limitAPI_KEY_REQUIRED: Fee tracking is enabled but request is missing the API key header
Additional Resources
- Stellar SDK Documentation: https://stellar.github.io/js-stellar-sdk/
- Soroban Documentation: https://soroban.stellar.org/docs
- OpenZeppelin Relayer Documentation: https://docs.openzeppelin.com/relayer