@openzeppelin/relayer-plugin-channels
v0.14.0
Published
OpenZeppelin Relayer Plugin for Stellar Channel Accounts
Keywords
Readme
Channels Plugin
A plugin for OpenZeppelin Relayer that enables parallel transaction submission on Stellar using channel accounts with fee bumping. Channel accounts provide unique sequence numbers for parallel transaction submission, preventing sequence number conflicts.
Table of Contents
- Quick Start
- Prerequisites
- Installation & Setup
- Development
- Overview
- Architecture
- Contract Capacity Limits
- Management API
- Plugin Client
- API Usage
- How It Works
- Validation Rules
- KV Schema
- Error Codes
- Smoke Test Contract
- License
Quick Start
Want to get started quickly? Check out the Channels Plugin Example which includes a pre-configured relayer setup, Docker Compose configuration, and step-by-step instructions. This is the fastest way to get the Channels plugin up and running.
For manual installation and configuration details, continue reading below.
Prerequisites
- Node.js >= 18
- pnpm >= 10
- OpenZeppelin Relayer
Installation & Setup
The Channels plugin can be added to any OpenZeppelin Relayer in two ways:
1. Install from npm (recommended)
# From the root of your Relayer repository
cd plugins
mkdir channels
cd channels
pnpm add @openzeppelin/relayer-plugin-channels2. Use a local build (for development / debugging)
# Clone and build the plugin
git clone https://github.com/openzeppelin/relayer-plugin-channels.git
cd relayer-plugin-channels
pnpm install
pnpm buildNow reference the local build from your Relayer's plugins/package.json:
{
"dependencies": {
"@openzeppelin/relayer-plugin-channels": "file:../../relayer-plugin-channels",
},
}Install dependencies:
pnpm installCreate the plugin wrapper
Inside the Relayer create a directory for the plugin and expose its handler:
mkdir -p plugins/channelsplugins/channels/index.ts
export { handler } from '@openzeppelin/relayer-plugin-channels';Configure the Relayer
Before setting environment variables, you need to configure your Relayer's config.json with the fund account and channel accounts. Create or update 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
pathpoints to your plugin wrapper file relative to the plugins directory
For more details on Relayer configuration, see the OpenZeppelin Relayer documentation.
Configure Environment Variables
Set the required environment variables for the plugin:
# Required environment variables
export STELLAR_NETWORK="testnet" # or "mainnet"
export FUND_RELAYER_ID="channels-fund"
export PLUGIN_ADMIN_SECRET="your-secret-here" # Required for management API
# Optional environment variables
export LOCK_TTL_SECONDS=10 # default: 30, min: 3, max: 30
# Fee tracking (optional)
export FEE_LIMIT=1000000 # Default max fee per API key in stroops (disabled if not set)
export FEE_RESET_PERIOD_SECONDS=86400 # Reset fee consumption every N seconds (e.g., 86400 = 24 hours)
export API_KEY_HEADER="x-api-key" # Header name to extract API key (default: x-api-key)
# Contract capacity limits (optional)
export LIMITED_CONTRACTS="CDL74RF5BLYR2YBLCCI7F5FB6TPSCLKEJUBSD2RSVWZ4YHF3VMFAIGWA" # Comma-separated contract addresses
export CONTRACT_CAPACITY_RATIO=0.8 # Max ratio of pool for limited contracts (default: 0.8 = 80%)
# Inclusion fee overrides (optional)
export INCLUSION_FEE_DEFAULT=203 # Inclusion fee in stroops for regular contracts (default: BASE_FEE * 2 + 3 = 203)
export INCLUSION_FEE_LIMITED=201 # Inclusion fee in stroops for limited contracts (default: BASE_FEE * 2 + 1 = 201)
# Sequence number cache (optional)
export SEQUENCE_NUMBER_CACHE_MAX_AGE_MS=120000 # Max age of cached sequence numbers in ms (default: 120000)
# Auth expiry validation (optional)
export MIN_SIGNATURE_EXPIRATION_LEDGER_BUFFER=2 # Minimum ledger margin for auth entry signatureExpirationLedger (default: 2)Your Relayer should now contain:
relayer/
└─ plugins/
└─ channels/
├─ package.json # lists the dependency
└─ index.tsInitialize Channel Accounts
Before using the Channels plugin, you must configure channel accounts using the management API:
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "setChannelAccounts",
"adminSecret": "your-secret-here",
"relayerIds": ["channel-0001", "channel-0002", "channel-0003"]
}
}
}'The Channels plugin is now ready to serve Soroban transactions 🚀
Development
Building from Source
# Install dependencies
pnpm install
# Build the plugin
pnpm build
# Run tests
pnpm test
# Lint and format
pnpm lint
pnpm formatOverview
The Channels plugin accepts Soroban operations and handles all the complexity of getting them on-chain:
- Automatic fee bumping using a dedicated fund account
- Parallel transaction execution with a pool of channel accounts
- Transaction simulation and resource management
- Error handling and confirmation waiting
Architecture
- Fund Account: Holds funds and pays for fee bumps
- Channel Accounts: Provide unique sequence numbers for parallel transaction submission
- The channel account is the transaction source and signer; the fund account wraps it in a fee bump
Contract Capacity Limits
High-volume contracts can monopolize the channel pool, starving other traffic. Contract capacity limits allow you to reserve a portion of the pool for non-limited contracts.
Configuration
# Comma-separated list of contract addresses to limit (case-insensitive)
export LIMITED_CONTRACTS="CDL74RF5BLYR2YBLCCI7F5FB6TPSCLKEJUBSD2RSVWZ4YHF3VMFAIGWA,CABC123..."
# Maximum ratio of pool that limited contracts can use (default: 0.8)
export CONTRACT_CAPACITY_RATIO=0.8How It Works
- Limited contracts can only acquire from a deterministic subset of channels (e.g., 80% of pool)
- Unlisted contracts have full access to all channels
- The subset is selected deterministically using a hash-based partition, ensuring stable channel assignment
- Minimum 1 channel is always guaranteed, even at very low ratios
Example
With 10 channel accounts and CONTRACT_CAPACITY_RATIO=0.8:
- Limited contracts can use up to 8 channels (
floor(10 * 0.8)) - 2 channels are always reserved for non-limited traffic
- If all 8 limited slots are in use, limited contracts get
POOL_CAPACITYerror while non-limited contracts can still acquire
Notes
- Contract IDs are matched case-insensitively (normalized to uppercase internally)
- If contract ID cannot be extracted from the request (non-
invokeContractoperations), no limit is applied - Zero runtime overhead for unlimited contracts
Management API
The Channels plugin 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/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "listChannelAccounts",
"adminSecret": "your-secret-here"
}
}
}'Response:
{
"relayerIds": ["channel-0001", "channel-0002", "channel-0003"]
}Set Channel Accounts
Configure the channel accounts that the plugin will use. This replaces the entire list:
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "setChannelAccounts",
"adminSecret": "your-secret-here",
"relayerIds": ["channel-0001", "channel-0002", "channel-0003"]
}
}
}'Response:
{
"ok": true,
"appliedRelayerIds": ["channel-0001", "channel-0002", "channel-0003"]
}Get Fee Usage
Query fee consumption for a specific API key:
curl -X POST http://localhost:8080/api/v1/plugins/channels/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-query"
}
}
}'Response:
{
"consumed": 500000,
"limit": 1000000,
"remaining": 500000,
"periodStartAt": "2024-01-15T00:00:00.000Z",
"periodEndsAt": "2024-01-16T00:00:00.000Z"
}limit: Effective fee limit (custom if set, otherwise default)remaining: Remaining fee budget in stroopsperiodStartAt: Datetime string when current period started (if reset period configured)periodEndsAt: Datetime string when period will reset (if reset period configured)
Get Fee Limit
Query fee limit configuration for a specific API key:
curl -X POST http://localhost:8080/api/v1/plugins/channels/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:
{
"limit": 500000
}Set Fee Limit
Set a custom fee limit for a specific API key:
curl -X POST http://localhost:8080/api/v1/plugins/channels/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:
{
"ok": true,
"limit": 500000
}Note: If custom limit is set to 0 it will block all transactions
Delete Fee Limit
Remove a custom fee limit for a specific API key (reverts to default limit):
curl -X POST http://localhost:8080/api/v1/plugins/channels/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:
{
"ok": true
}Get Pool Stats
Returns pool health metrics, configuration, and fee info.
curl -X POST http://localhost:8080/... \
-H "Content-Type: application/json" \
-d '{
"params": {
"management": {
"action": "stats",
"adminSecret": "your-secret-here"
}
}
}'Response:
{
"pool": {
"size": 5,
"locked": 2,
"available": 3
},
"config": {
"network": "testnet",
"lockTtlSeconds": 30,
"feeLimit": 10000,
"feeResetPeriodSeconds": 3600,
"contractCapacityRatio": 0.8,
"limitedContracts": []
},
"fees": {
"inclusionFeeDefault": 203,
"inclusionFeeLimited": 201
}
}Note: If lock checks fail,
lockedandavailablewill beundefinedrather than causing the request to fail. Thesizeand config info are always returned.
Important Notes:
- You must configure at least one channel account before the plugin can process transactions
- The management API will prevent removing accounts that are currently locked (in use). On failure it throws a plugin error with status 409, code
LOCKED_CONFLICT, anddetails.lockedlisting blocked IDs. - All relayer IDs must exist in your OpenZeppelin Relayer configuration
- The
adminSecretmust match thePLUGIN_ADMIN_SECRETenvironment variable
Plugin Client
The Channels plugin provides a TypeScript client for easy integration into your applications. The client automatically handles request/response formatting, error handling, and supports both relayer mode and direct HTTP mode.
Installation
npm install @openzeppelin/relayer-plugin-channels
# or
pnpm add @openzeppelin/relayer-plugin-channelsQuick Start
import { ChannelsClient } from '@openzeppelin/relayer-plugin-channels';
// Connecting to OpenZeppelin's managed Channels service
const client = new ChannelsClient({
baseUrl: 'https://channels.openzeppelin.com',
apiKey: 'your-api-key',
});
// Connecting to your own Relayer with Channels plugin
const relayerClient = new ChannelsClient({
baseUrl: 'http://localhost:8080',
pluginId: 'channels',
apiKey: 'your-relayer-api-key',
adminSecret: 'your-admin-secret', // Optional: Required for management operations
});Configuration
Managed Service
When connecting to OpenZeppelin's managed Channels service (which runs behind Cloudflare and a load balancer), provide just the baseUrl and apiKey:
// Mainnet
const client = new ChannelsClient({
baseUrl: 'https://channels.openzeppelin.com',
apiKey: 'your-api-key',
});
// Testnet
const testnetClient = new ChannelsClient({
baseUrl: 'https://channels.openzeppelin.com/testnet',
apiKey: 'your-api-key',
});Generate API Keys:
- Testnet: https://channels.openzeppelin.com/testnet/gen
- Mainnet: https://channels.openzeppelin.com/gen
Self-Hosted Relayer
When connecting directly to your own OpenZeppelin Relayer instance, include the pluginId:
const client = new ChannelsClient({
baseUrl: 'http://localhost:8080',
pluginId: 'channels',
apiKey: 'your-relayer-api-key',
adminSecret: 'your-admin-secret', // Optional: Required for management operations
});The client automatically routes requests appropriately based on whether pluginId is provided
Usage Examples
Submit Signed XDR Transaction
// Submit a complete, signed transaction
const result = await client.submitTransaction({
xdr: 'AAAAAgAAAAC...', // Complete transaction envelope XDR
});
console.log(result.hash); // Transaction hash
console.log(result.status); // Transaction status
console.log(result.transactionId); // Relayer transaction IDSubmit Soroban Function with Auth
// Submit func+auth (uses channel accounts and simulation)
const result = await client.submitSorobanTransaction({
func: 'AAAABAAAAAEAAAAGc3ltYm9s...', // Host function XDR (base64)
auth: ['AAAACAAAAAEAAAA...'], // Auth entry XDRs (base64)
});
console.log(result.hash);List Channel Accounts (Management)
// Initialize client with admin secret
const adminClient = new ChannelsClient({
baseUrl: 'http://localhost:8080',
apiKey: 'your-api-key',
pluginId: 'channels',
adminSecret: 'your-admin-secret', // Required for management operations
});
// List configured channel accounts
const accounts = await adminClient.listChannelAccounts();
console.log(accounts.relayerIds); // ['channel-001', 'channel-002', ...]Set Channel Accounts (Management)
// Configure channel accounts (requires adminSecret)
const result = await adminClient.setChannelAccounts(['channel-001', 'channel-002', 'channel-003']);
console.log(result.ok); // true
console.log(result.appliedRelayerIds); // ['channel-001', 'channel-002', 'channel-003']Get Fee Usage (Management)
// Query fee consumption for an API key (requires adminSecret)
const usage = await adminClient.getFeeUsage('client-api-key');
console.log(usage.consumed); // 500000 (stroops)
console.log(usage.limit); // 1000000 (effective limit)
console.log(usage.remaining); // 500000 (remaining budget)
console.log(usage.periodStartAt); // '2024-01-15T00:00:00.000Z'
console.log(usage.periodEndsAt); // '2024-01-16T00:00:00.000Z'Get Fee Limit (Management)
// Query fee limit configuration for an API key (requires adminSecret)
const limitInfo = await adminClient.getFeeLimit('client-api-key');
console.log(limitInfo.limit); // 500000 (custom limit if set, otherwise default)Set Fee Limit (Management)
// Set a custom fee limit for an API key (requires adminSecret)
const result = await adminClient.setFeeLimit('client-api-key', 500000);
console.log(result.ok); // true
console.log(result.limit); // 500000Delete Fee Limit (Management)
// Remove custom fee limit, revert to default (requires adminSecret)
const result = await adminClient.deleteFeeLimit('client-api-key');
console.log(result.ok); // trueGet Pool Stats (Management)
// Get pool health metrics (requires adminSecret)
const stats = await adminClient.getStats();
console.log(stats.pool.size); // total channels
console.log(stats.pool.locked); // currently in-use
console.log(stats.pool.available); // size - locked
console.log(stats.config.network); // 'testnet' or 'mainnet'
console.log(stats.fees); // inclusion fee valuesError Handling
The client provides three types of errors:
import {
PluginTransportError,
PluginExecutionError,
PluginUnexpectedError,
} from '@openzeppelin/relayer-plugin-channels';
try {
const result = await client.submitTransaction({ xdr: '...' });
} catch (error) {
if (error instanceof PluginTransportError) {
// Network/HTTP failures (connection refused, timeout, 500/502/503)
console.error('Transport error:', error.message);
console.error('Status code:', error.statusCode);
} else if (error instanceof PluginExecutionError) {
// Plugin rejected the request (validation, business logic, on-chain failure)
console.error('Execution error:', error.message);
console.error('Details:', error.errorDetails);
} else if (error instanceof PluginUnexpectedError) {
// Client-side parsing/validation errors
console.error('Unexpected error:', error.message);
}
}Metadata and Debugging
Responses include optional metadata (logs and traces) when the plugin is configured with emit_logs and emit_traces:
const result = await client.submitTransaction({ xdr: '...' });
// Access metadata if available
if (result.metadata) {
console.log('Logs:', result.metadata.logs);
console.log('Traces:', result.metadata.traces);
}TypeScript Types
All request and response types are fully typed:
import type {
ChannelsXdrRequest,
ChannelsFuncAuthRequest,
ChannelsTransactionResponse,
ListChannelAccountsResponse,
SetChannelAccountsResponse,
GetFeeUsageResponse,
GetFeeLimitResponse,
SetFeeLimitResponse,
DeleteFeeLimitResponse,
} from '@openzeppelin/relayer-plugin-channels';Configuration Options
interface ChannelsClientConfig {
// Required
baseUrl: string; // Service URL
apiKey: string; // API key for authentication
// Optional
pluginId?: string; // Include when connecting to a Relayer directly
adminSecret?: string; // Required for management operations
timeout?: number; // Request timeout in ms (default: 30000)
apiKeyHeader?: string; // Header name for API key (default: 'x-api-key')
}API Usage
Submit with Transaction XDR
Submit a complete, signed transaction:
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"xdr": "AAAAAgAAAAB..."
}
}'Submit with Function and Auth
Submit just the Soroban function and auth entries:
curl -X POST http://localhost:8080/api/v1/plugins/channels/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 (signed, not fee-bump)func(string): Soroban host function XDR (base64)auth(array): Array of Soroban authorization entry XDRs (base64)skipWait(boolean, optional): Whentrue, returns immediately after submitting the transaction without waiting for confirmation. Defaults tofalse. Must be a boolean — non-boolean values (e.g.,"false",1) are rejected.getTransaction(object, optional): Poll for a transaction's status by ID. Cannot be combined with other parameters.
Note: Provide either xdr OR func+auth OR getTransaction, not a combination.
Fire-and-Forget with skipWait
When skipWait: true is passed with an xdr or func+auth request, the plugin submits the transaction and returns immediately with status "pending" instead of waiting for on-chain confirmation. This is useful for high-throughput scenarios where the caller handles confirmation separately.
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"xdr": "AAAAAgAAAAB...",
"skipWait": true
}
}'Response:
{
"success": true,
"data": {
"transactionId": "tx_123456",
"status": "pending",
"hash": null
}
}Use the returned transactionId with getTransaction to poll for the final status.
Get Transaction by ID
Poll for the status of a previously submitted transaction using its transactionId. This is typically used after a skipWait submission to check whether the transaction has been confirmed.
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"params": {
"getTransaction": {
"transactionId": "tx_123456"
}
}
}'Response:
{
"success": true,
"data": {
"transactionId": "tx_123456",
"status": "confirmed",
"hash": "1234567890abcdef..."
}
}The status field reflects the transaction's current state (e.g., "pending", "sent", "submitted", "confirmed", "failed", "expired"). The hash field is null until the transaction is submitted.
Response
Responses follow the Relayer envelope { success, data, error }.
Success example:
{
"success": true,
"data": {
"transactionId": "tx_123456",
"status": "confirmed",
"hash": "1234567890abcdef..."
},
"error": null
}Plugin error example:
{
"success": false,
"data": {
"code": "POOL_CAPACITY",
"details": {}
},
"error": "Too many transactions queued. Please try again later"
}How It Works
- Request Validation: Validates input parameters (xdr OR func+auth)
- Channel Account Pool: Acquires an available channel account from the pool
- Transaction Building: For func+auth, builds transaction with channel as source
- Simulation: Simulates transaction to obtain sorobanData and resource fee
- Signing: Channel account signs the transaction
- Fee Calculation: Calculates dynamic max_fee based on resource fee
- Fee Bumping: Fund account wraps transaction with fee bump
- Submission: Sends to Stellar network and waits for confirmation
- Pool Release: Returns channel account to the pool
Validation Rules
Input Validation
- Must provide
xdrORfunc+auth(not both) - XDR must not be a fee-bump envelope
- All parameters must be valid base64 XDR
Transaction Validation (XDR mode)
- Envelope type must be
envelopeTypeTx(not fee bump) - TimeBounds maxTime must be within 30 seconds from now
KV Schema
Membership List
- Key:
<network>:channel:relayer-ids - Value:
{ relayerIds: string[] }
Channel Locks
- Key:
<network>:channel:in-use:<relayerId> - Value:
{ token: string, lockedAt: ISOString } - TTL: Configured by
LOCK_TTL_SECONDS.
Fee Tracking
- Key:
<network>:api-key-fees:<apiKey> - Value:
{ consumed: number, periodStart?: number }(fees in stroops, period start timestamp in ms)
Custom Fee Limits
- Key:
<network>:api-key-limit:<apiKey> - Value:
{ limit: number }(custom fee limit in stroops)
Sequence Number Cache
- Key:
<network>:channel:seq:<address> - Value:
{ sequence: string, storedAt: number }(sequence number and cache timestamp in ms)
Error Codes
Configuration
CONFIG_MISSING: Missing required environment variableCONFIG_INVALID: Invalid configuration value (e.g., invalid contract address inLIMITED_CONTRACTS)UNSUPPORTED_NETWORK: Invalid network type
Request Validation
INVALID_PARAMS: Invalid request parametersINVALID_XDR: Failed to parse XDRINVALID_ENVELOPE_TYPE: Not a regular transaction envelopeINVALID_UNSIGNED_XDR: Unsigned XDR must contain exactly oneinvokeHostFunctionoperationINVALID_TIME_BOUNDS: TimeBounds too far in the futureTIMEBOUNDS_EXPIRED: Transaction timebounds have expiredTIMEBOUNDS_TOO_FAR: Transaction timebounds maxTime too far in the futureFEE_MISMATCH: Fee mismatch in signed transactionINVALID_OPERATION_SOURCE: Operation source does not match transaction source
Pool & Channel
NO_CHANNELS_CONFIGURED: No channel accounts have been configured via management APIPOOL_CAPACITY: All channel accounts in useRELAYER_UNAVAILABLE: Relayer not foundFAILED_TO_GET_SEQUENCE: Failed to fetch channel account sequence numberACCOUNT_NOT_FOUND: Channel account not found on the ledger
Simulation & Assembly
SIMULATION_FAILED: Transaction simulation failedSIMULATION_NETWORK_ERROR: Simulation network request failed (HTTP 502)SIMULATION_RPC_FAILURE: Simulation RPC provider error (HTTP 502)SIMULATION_SIGNED_AUTH_VALIDATION_FAILED: Signed auth entry validation failed in enforce-mode simulationAUTH_EXPIRY_TOO_SHORT: Auth entrysignatureExpirationLedgertoo close to current ledgerASSEMBLY_FAILED: Transaction assembly failed
Submission
INVALID_SIGNATURE: Invalid channel signature responseONCHAIN_FAILED: Transaction failed on-chainWAIT_TIMEOUT: Transaction wait timeout
Fee Tracking
API_KEY_REQUIRED: API key header missing whenFEE_LIMITis configuredFEE_LIMIT_EXCEEDED: API key has exceeded its fee limit (HTTP 429)
Management
MANAGEMENT_DISABLED: Management API not enabledUNAUTHORIZED: Invalid admin secretINVALID_ACTION: Invalid management actionINVALID_PAYLOAD: Invalid management request payloadLOCKED_CONFLICT: Cannot remove locked channel accountsKV_ERROR: KV store operation failed
License
MIT
