Route funds directly to Soroban smart accounts (C-addresses) without requiring a traditional G-address. The onboarding layer for all Soroban dApps.
The shift to C-addresses (Soroban smart accounts) is critical for next-generation dApps on Stellar, but two major adoption blockers persist:
- Funding friction: Users cannot easily fund a C-address without first using a traditional G-address
- Lack of tooling: No modern onboarding flow exists around the Smart Account standard by OpenZeppelin
In plain terms: new users can't interact with Soroban dApps directly. They need an old-style Stellar account first, which kills UX for mainstream adoption.
A protocol and backend infrastructure that lets anyone fund a Soroban smart account (C-address) directly from a CEX withdrawal, a credit card, or an existing G-address — without the user understanding the underlying account model.
| Concept | Ethereum | Stellar (this project) |
|---|---|---|
| Smart accounts | EIP-4337 Account Abstraction | Soroban C-addresses |
| Funding layer | Paymasters | Onboarding Bridge contract |
| Wallet integration | ERC-4337 SDK | TypeScript SDK |
| Fiat on-ramp | Moonpay/Transak integration | Same (via off-ramp module) |
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Wallet/ │────▶│ SDK Client │────▶│ API Server │
│ dApp │ │ (TypeScript)│ │ (Express) │
└─────────────┘ └──────────────┘ └────────┬────────┘
│
┌──────────────────────────────┼──────────────────┐
│ ┌───────────────▼────────┐ │
│ │ Onboarding Bridge │ │
│ │ Soroban Contract │ │
│ └───────────┬────────────┘ │
│ │ │
│ ┌───────────▼────────────┐ │
│ │ C-Address Target │ │
│ └────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Moonpay │ │ Transak │ │ CEX Router │ │
│ └──────────┘ └──────────┘ └─────────────┘ │
└────────────────────────────────────────────────┘
G-Address ──▶ Bridge Contract ──▶ C-Address
CEX ──▶ Bridge Contract ──▶ C-Address
Credit Card ──▶ Moonpay/Transak ──▶ Bridge Contract ──▶ C-Address
The repository runs automated dependency vulnerability scanning through GitHub Actions and scheduled audits. The workflow currently:
- runs a weekly dependency audit on the default branch and for pull requests,
- publishes a machine-readable audit report plus a human-readable summary artifact,
- tracks severity-based patch SLAs of critical (24 hours), high (7 days), moderate (30 days), and low (next release),
- sends Slack notifications when the workflow is configured with a webhook secret.
Accepted-risk overrides must be documented in the issue tracker with the dependency name, severity, owner, and expiry date, and they should be reviewed on the next scheduled scan.
├── contracts/onboarding-bridge/ # Soroban smart contract (Rust)
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # Core contract logic
│ └── test.rs # Contract unit tests
│
├── api/ # Express API server
│ ├── package.json
│ └── src/
│ ├── index.ts # Server entry point
│ ├── config.ts # Environment configuration
│ ├── routes/ # REST API routes
│ │ ├── quote.ts # GET /api/v1/quote
│ │ ├── funding.ts # POST /api/v1/fund
│ │ ├── status.ts # GET /api/v1/status/:txHash
│ │ ├── offramp.ts # POST /api/v1/offramp/{moonpay,transak}
│ │ └── cex.ts # POST /api/v1/cex/route
│ ├── services/ # Business logic
│ │ ├── soroban.ts # Soroban RPC client
│ │ ├── moonpay.ts # Moonpay integration
│ │ ├── transak.ts # Transak integration
│ │ └── cex.ts # CEX routing
│ └── middleware/
│ └── error.ts # Error handling
│
├── sdk/ # TypeScript SDK
│ ├── package.json
│ └── src/
│ ├── index.ts # Public API exports
│ ├── bridge.ts # BridgeClient class
│ ├── types.ts # TypeScript type definitions
│ └── utils.ts # Address validation, fee calculation
│
├── offramp/ # Standalone off-ramp modules
│ ├── moonpay.ts # Moonpay widget + webhook verification
│ └── transak.ts # Transak widget generation
│
├── cex/ # CEX withdrawal routing
│ ├── withdrawal-router.ts # Pluggable routing engine
│ └── README.md # Exchange integration guide
│
├── .env.example # Environment variables template
├── tsconfig.base.json # Shared TypeScript config
└── package.json # Workspace root
onboarding-bridge — deployed on Stellar testnet/mainnet.
| Function | Auth | Description |
|---|---|---|
initialize(admin, fee_bps) |
Admin | One-time initialization |
version() |
None | Returns contract version |
admin() |
None | Returns admin address |
fee_bps() |
None | Returns fee in basis points |
accumulated_fees() |
None | Returns total fees collected |
set_fee(new_fee_bps) |
Admin | Update fee rate |
fund_c_address(source, target, token, amount, memo) |
Source | Route funds from G → C address |
withdraw_fees(to, token, amount) |
Admin | Withdraw accumulated fees |
route_from_exchange(exchange, target, token, amount, memo) |
Exchange | CEX → C-address routing |
Fees are calculated in basis points (1 bps = 0.01%):
- Configurable by admin (max 10000 bps = 100%)
- Deducted from the transfer amount
- Accumulated in the contract for admin withdrawal
- Emitted in
fundedevent for transparency
// On initialization
(event: "initialize", admin: Address, fee_bps: u32)
// On funding
(event: "funded", source: Address, target: Address, amount: i128, fee: i128, memo: String)
// On fee update
(event: "set_fee", new_fee_bps: u32)
// On fee withdrawal
(event: "withdrawn", to: Address, amount: i128)The API now supports URL-based versioning and Accept header versioning. Version negotiation is handled by the middleware and returns standard deprecation headers on v1 responses.
v1: current stable endpoint family with deprecation headers and a sunset datev2: compatible successor endpoints that can coexist withv1alpha -> beta -> stable -> deprecated -> sunset: lifecycle is documented in the changelog below
| Version | Status | Notes |
|---|---|---|
v1 |
deprecated | Legacy endpoints with deprecation and sunset headers |
v2 |
beta | Newer routing surface with version negotiation support |
Get a funding quote including fee estimates.
Query Parameters:
| Param | Type | Description |
|---|---|---|
sourceAsset |
string |
Asset code (e.g. XLM, USDC) |
amount |
string |
Amount in stroops (integer string) |
targetAddress |
string |
Destination C-address |
Response:
{
"estimatedFee": "3",
"expectedReceive": "997",
"feeBps": 30,
"rate": "1.0"
}Submit a funding transaction to the Soroban bridge contract.
Request Body:
{
"sourceAddress": "G...",
"targetAddress": "C...",
"tokenAddress": "CC...",
"amount": "1000",
"memo": "onboarding",
"sourceSecretKey": "S..."
}Response:
{
"status": "success",
"hash": "a1b2c3d4..."
}Check the status of a funding transaction.
Response:
{
"status": "success",
"hash": "a1b2c3d4..."
}Generate a Moonpay widget URL for credit card → C-address funding.
Request Body:
{
"currencyCode": "xlm",
"walletAddress": "C...",
"walletNetwork": "stellar",
"baseCurrencyAmount": 100,
"baseCurrencyCode": "USD",
"email": "user@example.com"
}Response:
{
"url": "https://buy.moonpay.com?apiKey=..."
}Generate a Transak widget URL.
Request Body:
{
"walletAddress": "C...",
"network": "stellar",
"fiatCurrency": "USD",
"cryptoCurrency": "XLM",
"fiatAmount": 100
}Response:
{
"url": "https://global-stg.transak.com?apiKey=..."
}Route a CEX withdrawal to a C-address.
Request Body:
{
"exchange": "binance",
"sourceAsset": "XLM",
"amount": "10000000",
"targetCAddress": "C...",
"targetNetwork": "stellar",
"memo": "bridge:binance:ABCD1234"
}Response:
{
"status": "pending",
"withdrawalId": "bin-1712345678-a1b2c3",
"estimatedArrival": "5-30 minutes",
"fee": "0.0001"
}npm install @c-address-bridge/sdkPython, Rust, Go, and Java examples live in examples/. Each includes a thin HTTP client, Docker support, and CI verification:
node examples/mock-server/server.mjs # terminal 1
export BRIDGE_BASE_URL=http://localhost:3099
cd examples/python && python main.py # or rust / go / javaSee examples/README.md and video walkthroughs.
import { BridgeClient, utils } from '@c-address-bridge/sdk';
const client = new BridgeClient({
baseUrl: 'https://api.bridge.example.com',
apiKey: 'your-api-key',
});
// Get a quote
const quote = await client.getQuote({
sourceAsset: 'XLM',
amount: '10000000', // 1 XLM in stroops
targetAddress: 'C...',
});
console.log(`Fee: ${quote.estimatedFee} stroops`);
console.log(`You receive: ${quote.expectedReceive} stroops`);
// Fund a C-address (two-step: prepare → sign → submit)
const prepared = await client.prepareFundingTransaction({
sourceAddress: 'G...',
targetAddress: 'C...',
tokenAddress: 'CC...',
amount: '10000000',
});
// Sign the prepared transaction with your wallet (off-SDK), then submit
const result = await client.submitSignedXdr({ signedXdr: '' });
console.log(`Transaction hash: ${result.hash}`);
// Check status
const status = await client.getStatus(result.hash);
console.log(`Status: ${status.status}`);
// Validate addresses
console.log(utils.isValidStellarAddress('C...')); // true
console.log(utils.isCAddress('C...')); // true
console.log(utils.isGAddress('G...')); // true
console.log(utils.isGAddress('C...')); // false
// Calculate fees
const fee = utils.calculateFee(1000n, 30); // 30 bps fee
console.log(`Fee: ${fee} stroops`);// Moonpay — credit card to C-address
const moonpay = await client.createMoonpayUrl({
walletAddress: 'C...',
currencyCode: 'xlm',
walletNetwork: 'stellar',
baseCurrencyAmount: 100,
baseCurrencyCode: 'USD',
});
// Transak — credit card to C-address
const transak = await client.createTransakUrl({
walletAddress: 'C...',
network: 'stellar',
fiatCurrency: 'USD',
cryptoCurrency: 'XLM',
fiatAmount: 100,
});const result = await client.routeCexWithdrawal({
exchange: 'coinbase',
sourceAsset: 'USDC',
amount: '5000000',
targetCAddress: 'C...',
targetNetwork: 'stellar',
});Exchanges can integrate the bridge by implementing the WithdrawalRouter:
import { WithdrawalRouter, defaultCexHandlers } from './cex/withdrawal-router';
const router = new WithdrawalRouter();
router.registerExchange('my-exchange', {
name: 'my-exchange',
apiBaseUrl: 'https://api.my-exchange.com',
}, async (req, config) => {
// Implement withdrawal API call
// Route through bridge contract
return {
success: true,
withdrawalId: 'tx-...',
status: 'pending',
estimatedCompletion: '5-30 minutes',
};
});Memo format for tracking: bridge:{exchange_name}:{c_address_suffix}
defaultCexHandlersare non-functional placeholders. Thebinance,coinbase, andkrakenhandlers exported fromcex/withdrawal-router.tsnever call an exchange API — they immediately return a fakesuccess: true/status: 'pending'result. They exist only to illustrate the handler shape used in the snippet above. Registering them directly instead of supplying your own handler will silently "succeed" without ever calling the exchange or moving funds.
# Clone
git clone https://github.com/C-Address-Onboarding-Bridge/C-Address-Onboarding-Bridge-Backend.git
cd C-Address-Onboarding-Bridge-Backend
# Install JS dependencies
npm install
# Copy environment config
cp .env.example .env# Build Soroban contract
cd contracts/onboarding-bridge
cargo build
# Build TypeScript packages
cd ../..
npm run build# Run Soroban contract tests
cargo test
# Run JS/TS tests
npm run test --workspacesnpm run dev -w apiServer starts at http://localhost:3001. Health check: GET /health.
Smart contract deployments are fully automated through GitHub Actions and version-tagged git pushes. No manual CLI commands are needed.
| Tag format | Network | Approval |
|---|---|---|
contract/v1.2.0-dev.3 |
testnet | None (auto) |
contract/v1.2.0 |
mainnet | Required reviewers |
Push a tag to trigger the pipeline:
# Deploy to testnet (automated)
git tag contract/v1.2.0-dev.1
git push origin contract/v1.2.0-dev.1
# Deploy to mainnet (requires approval in GitHub → Environments → mainnet)
git tag contract/v1.2.0
git push origin contract/v1.2.0You can also trigger it manually from Actions → Deploy Contract.
Tag push → CI gate → Build WASM → (approval for mainnet) → Deploy
↓
Initialize → Verify → Save artifact
↓
Generate report → Notify Slack
↓
Publish GitHub Release (tag pushes)
- CI gate — all existing tests must pass before deploy starts.
- Build WASM —
stellar contract buildwith thereleaseprofile; SHA-256 is computed and stored. - Mainnet approval — the
mainnetGitHub Environment enforces required reviewers. No code runs until approved. - Deploy —
stellar contract deployuploads the WASM; contract ID is captured. - Initialize —
initialize()is called with admin addresses, threshold, and fee parameters. - Verify —
version()andfee_bps()are invoked on-chain to confirm the contract is live and correctly configured. - Save artifact —
deployments/deployment-<network>.jsonis written with all deployment metadata. - Report — a Markdown deployment report is generated at
reports/deployment-report-<network>-<timestamp>.md. - GitHub Release — the WASM binary, artifact JSON, and report are attached to the release.
- Slack notification — team is notified (if
SLACK_WEBHOOK_URLis configured).
Configure these under Settings → Secrets and variables → Actions:
| Secret | Description |
|---|---|
SOROBAN_SOURCE_ACCOUNT |
Testnet deployer secret key (S...) |
SOROBAN_SOURCE_ACCOUNT_MAINNET |
Mainnet deployer secret key (S...) |
CONTRACT_ADMIN_ADDRESSES |
Comma-separated admin addresses for testnet |
CONTRACT_ADMIN_ADDRESSES_MAINNET |
Comma-separated admin addresses for mainnet |
SOROBAN_NETWORK_PASSPHRASE_MAINNET |
Mainnet network passphrase |
SLACK_WEBHOOK_URL |
Optional — Slack incoming webhook URL |
| Variable | Default | Description |
|---|---|---|
SOROBAN_RPC_URL_TESTNET |
https://soroban-rpc.testnet.stellar.org |
Testnet RPC |
SOROBAN_RPC_URL_MAINNET |
https://mainnet.sorobanrpc.com |
Mainnet RPC |
CONTRACT_THRESHOLD |
1 |
Testnet multi-sig threshold |
CONTRACT_THRESHOLD_MAINNET |
2 |
Mainnet multi-sig threshold |
CONTRACT_FEE_BPS |
30 |
Fee in basis points |
CONTRACT_MAX_FEE_BPS |
1000 |
Fee cap in basis points |
CONTRACT_MIN_AMOUNT |
100 |
Minimum fund amount (stroops) |
CONTRACT_MAX_AMOUNT |
1000000000000 |
Maximum fund amount (stroops) |
Every successful run saves a JSON artifact in deployments/:
For one-off deployments outside of CI:
# Build and deploy to testnet
SOURCE_ACCOUNT="S..." \
ADMIN_ADDRESSES="G...addr1,G...addr2" \
THRESHOLD=1 \
BRIDGE_FEE_BPS=30 \
bash scripts/deploy-contract.sh --network testnet
# Dry run (build + validate, no on-chain operations)
SOURCE_ACCOUNT="S..." ADMIN_ADDRESSES="G..." \
bash scripts/deploy-contract.sh --network testnet --dry-run
# Force re-deploy (even if an existing live contract is found)
SOURCE_ACCOUNT="S..." ADMIN_ADDRESSES="G..." \
bash scripts/deploy-contract.sh --network testnet --reinstall
# Skip build (reuse last compiled WASM)
SOURCE_ACCOUNT="S..." ADMIN_ADDRESSES="G..." \
bash scripts/deploy-contract.sh --network testnet --skip-buildSoroban contracts are immutable — rolling back means routing your API server to a previously-deployed contract ID. The pipeline preserves the previous artifact as deployments/deployment-<network>.prev.json.
# Roll back testnet to the previous deployment
SOURCE_ACCOUNT="S..." bash scripts/rollback-contract.sh --network testnet
# List all available rollback targets
bash scripts/rollback-contract.sh --network testnet --list
# Target a specific artifact
SOURCE_ACCOUNT="S..." bash scripts/rollback-contract.sh \
--network testnet \
--artifact deployments/deployment-testnet.prev.json
# Dry run — verify the rollback target is live without modifying artifacts
SOURCE_ACCOUNT="S..." bash scripts/rollback-contract.sh \
--network testnet --dry-runAfter rollback completes, update BRIDGE_CONTRACT_ID in your API server environment and redeploy the API.
The GitHub Actions workflow (deploy-contract.yml) automatically attempts rollback if the deploy step fails.
The deployment pipeline verifies the contract is correctly deployed after every run. You can also verify manually:
# Check the on-chain version
stellar contract invoke \
--id <contract-id> \
--rpc-url https://soroban-rpc.testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015" \
--source <your-secret> \
-- version
# Check the configured fee
stellar contract invoke \
--id <contract-id> \
--rpc-url https://soroban-rpc.testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015" \
--source <your-secret> \
-- fee_bps
# Verify WASM hash matches artifact
sha256sum target/wasm32v1-none/release/onboarding_bridge.wasm
# Compare with deployments/deployment-testnet.json .wasmHashA Markdown report is generated after every deployment and attached as a GitHub Release asset. Generate one manually from an existing artifact:
NETWORK=testnet bash scripts/generate-deployment-report.sh
# Output: reports/deployment-report-testnet-<timestamp>.md
# reports/deployment-report-latest.md# Set environment variables
export SOROBAN_RPC_URL=https://soroban-rpc.testnet.stellar.org
export BRIDGE_CONTRACT_ID=<deployed-contract-id>
export BRIDGE_FEE_BPS=30
export MOONPAY_API_KEY=your-key
export TRANSAK_API_KEY=your-key
export PORT=3001
# Start
npm start -w api
# Or using Docker
docker build -t c-address-bridge-api .
docker run -p 3001:3001 --env-file .env c-address-bridge-apiexport BLUE_URL=https://blue.example.com
export GREEN_URL=https://green.example.com
export DEPLOY_GREEN_COMMAND='bash ./scripts/deploy.sh --network testnet'
export SWITCH_TRAFFIC_COMMAND='echo "switch traffic to {{color}}"'
export POST_SWITCH_CHECK_COMMAND='bash ./scripts/smoke-test.sh'
npm run deploy:blue-greenThe blue-green flow deploys to the inactive environment, runs smoke tests, switches traffic, keeps the previous environment warm for rollback, drains old connections, and records the active color for the next release.
Security documentation lives in docs/security/:
- Threat Model — STRIDE analysis covering all six threat categories, trust boundaries, security assumptions, and known risks
- Incident Response Plan — detection, containment, and recovery playbooks for API key compromise, webhook forgery, contract admin key loss, and supply-chain attacks
- SECURITY.md — responsible disclosure policy, bug reporting instructions, scope, and past audit findings
To report a vulnerability, see SECURITY.md. Do not open a public issue.
Architectural decisions are documented in docs/adr/README.md. Use the ADR template at docs/adr/template.md for new proposals.
| Variable | Required | Default | Description |
|---|---|---|---|
SOROBAN_RPC_URL |
Yes | https://soroban-rpc.testnet.stellar.org |
Soroban RPC endpoint |
SOROBAN_NETWORK_PASSPHRASE |
Yes | Test SDF Network ; September 2015 |
Network passphrase |
BRIDGE_CONTRACT_ID |
Yes | — | Deployed contract ID |
BRIDGE_FEE_BPS |
No | 30 |
Fee in basis points |
MOONPAY_API_KEY |
For Moonpay | — | Moonpay API key |
MOONPAY_SECRET_KEY |
For webhooks | — | Moonpay webhook secret |
TRANSAK_API_KEY |
For Transak | — | Transak API key |
TRANSAK_ENVIRONMENT |
No | STAGING |
STAGING or PRODUCTION |
BINANCE_API_KEY / BINANCE_API_SECRET |
For Binance withdrawals | — | Binance API credentials |
COINBASE_API_KEY / COINBASE_API_SECRET |
For Coinbase withdrawals | — | Coinbase API credentials |
KRAKEN_API_KEY / KRAKEN_API_SECRET |
For Kraken withdrawals | — | Kraken API credentials |
CORS_ORIGINS |
No | — (all origins) | Comma-separated list of allowed CORS origins; leave empty to allow all (development only) |
PORT |
No | 3001 |
API server port |
HOST |
No | 0.0.0.0 |
API server host |
LOG_LEVEL |
No | info |
Pino log level |
┌─────────────────────────────┬────────────┬──────────┬─────────┐
│ Component │ Tests │ Passed │ Build │
├─────────────────────────────┼────────────┼──────────┼─────────┤
│ Soroban Contract (Rust) │ 4 │ 4 │ ✓ │
│ API Server (TypeScript) │ 7 │ 7 │ ✓ │
│ TypeScript SDK │ 6 │ 6 │ ✓ │
└─────────────────────────────┴────────────┴──────────┴─────────┘
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT — see LICENSE for details.