npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@openzeppelin/relayer-plugin-channels

v0.14.0

Published

OpenZeppelin Relayer Plugin for Stellar Channel Accounts

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

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-channels

2. 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 build

Now reference the local build from your Relayer's plugins/package.json:

{
  "dependencies": {
    "@openzeppelin/relayer-plugin-channels": "file:../../relayer-plugin-channels",
  },
}

Install dependencies:

pnpm install

Create the plugin wrapper

Inside the Relayer create a directory for the plugin and expose its handler:

mkdir -p plugins/channels

plugins/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": true in 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 testnet for testing or mainnet for 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 path points 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.ts

Initialize 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 format

Overview

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.8

How 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_CAPACITY error 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-invokeContract operations), 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 stroops
  • periodStartAt: 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, locked and available will be undefined rather than causing the request to fail. The size and 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, and details.locked listing blocked IDs.
  • All relayer IDs must exist in your OpenZeppelin Relayer configuration
  • The adminSecret must match the PLUGIN_ADMIN_SECRET environment 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-channels

Quick 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 ID

Submit 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); // 500000

Delete Fee Limit (Management)

// Remove custom fee limit, revert to default (requires adminSecret)
const result = await adminClient.deleteFeeLimit('client-api-key');

console.log(result.ok); // true

Get 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 values

Error 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): When true, returns immediately after submitting the transaction without waiting for confirmation. Defaults to false. 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

  1. Request Validation: Validates input parameters (xdr OR func+auth)
  2. Channel Account Pool: Acquires an available channel account from the pool
  3. Transaction Building: For func+auth, builds transaction with channel as source
  4. Simulation: Simulates transaction to obtain sorobanData and resource fee
  5. Signing: Channel account signs the transaction
  6. Fee Calculation: Calculates dynamic max_fee based on resource fee
  7. Fee Bumping: Fund account wraps transaction with fee bump
  8. Submission: Sends to Stellar network and waits for confirmation
  9. Pool Release: Returns channel account to the pool

Validation Rules

Input Validation

  • Must provide xdr OR func+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 variable
  • CONFIG_INVALID: Invalid configuration value (e.g., invalid contract address in LIMITED_CONTRACTS)
  • UNSUPPORTED_NETWORK: Invalid network type

Request Validation

  • INVALID_PARAMS: Invalid request parameters
  • INVALID_XDR: Failed to parse XDR
  • INVALID_ENVELOPE_TYPE: Not a regular transaction envelope
  • INVALID_UNSIGNED_XDR: Unsigned XDR must contain exactly one invokeHostFunction operation
  • INVALID_TIME_BOUNDS: TimeBounds too far in the future
  • TIMEBOUNDS_EXPIRED: Transaction timebounds have expired
  • TIMEBOUNDS_TOO_FAR: Transaction timebounds maxTime too far in the future
  • FEE_MISMATCH: Fee mismatch in signed transaction
  • INVALID_OPERATION_SOURCE: Operation source does not match transaction source

Pool & Channel

  • NO_CHANNELS_CONFIGURED: No channel accounts have been configured via management API
  • POOL_CAPACITY: All channel accounts in use
  • RELAYER_UNAVAILABLE: Relayer not found
  • FAILED_TO_GET_SEQUENCE: Failed to fetch channel account sequence number
  • ACCOUNT_NOT_FOUND: Channel account not found on the ledger

Simulation & Assembly

  • SIMULATION_FAILED: Transaction simulation failed
  • SIMULATION_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 simulation
  • AUTH_EXPIRY_TOO_SHORT: Auth entry signatureExpirationLedger too close to current ledger
  • ASSEMBLY_FAILED: Transaction assembly failed

Submission

  • INVALID_SIGNATURE: Invalid channel signature response
  • ONCHAIN_FAILED: Transaction failed on-chain
  • WAIT_TIMEOUT: Transaction wait timeout

Fee Tracking

  • API_KEY_REQUIRED: API key header missing when FEE_LIMIT is configured
  • FEE_LIMIT_EXCEEDED: API key has exceeded its fee limit (HTTP 429)

Management

  • MANAGEMENT_DISABLED: Management API not enabled
  • UNAUTHORIZED: Invalid admin secret
  • INVALID_ACTION: Invalid management action
  • INVALID_PAYLOAD: Invalid management request payload
  • LOCKED_CONFLICT: Cannot remove locked channel accounts
  • KV_ERROR: KV store operation failed

License

MIT