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)
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": "channels/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:
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 the transaction
tx.sign(keypair);
// Export base64 envelope XDR
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