Building a Cross-Chain Bridge API for RustChain: RIP-305 Track C
RustChain is a Proof-of-Antiquity blockchain where vintage hardware mines real crypto. But until now, there was no way to move RTC (RustChain's native token) onto other chains. Today I built the bridge API that makes it possible — connecting RTC to Solana and Base L2.
What Is RIP-305?
RIP-305 is RustChain's cross-chain airdrop protocol. It distributes 50,000 wrapped RTC (wRTC) across two target chains:
- Track A: wRTC as a Solana SPL token (30,000 wRTC)
- Track B: wRTC as a Base ERC-20 token (20,000 wRTC)
- Track C: The bridge API connecting them ← this post
- Track D: An airdrop claim page
Track C is where the magic happens. Without a bridge API, the tokens on Solana and Base would just be isolated assets with no connection to the actual RTC ecosystem.
Architecture: Phase 1 (Admin-Controlled)
Phase 1 is intentionally simple and trustworthy. Rather than building a complex trustless bridge with cross-chain message passing (which adds enormous attack surface), Phase 1 uses an admin-controlled mint pattern:
User → POST /bridge/lock → lock_id
↓
Lock recorded in SQLite ledger
Admin monitors pending locks
Admin mints wRTC on Solana (spl-token) or Base (ERC-20.mint())
↓
POST /bridge/release (release_tx)
↓
state: "complete" — fully transparent on ledger
The tradeoff is obvious: you're trusting the admin. But Phase 1 bridges are common in DeFi (Arbitrum launched this way), and Phase 2 can upgrade to trustless with on-chain verification.
The Lock Ledger
Every bridge action is recorded in a SQLite database with full event history. The ledger is publicly queryable, so anyone can verify:
- How much RTC has been locked
- Whether their lock was released
- The corresponding target chain transaction hash
Here are the key endpoints:
@bridge_bp.route("/ledger", methods=["GET"])
def get_ledger():
"""Filterable by state, chain, sender — transparent to all."""
...
@bridge_bp.route("/status/<lock_id>", methods=["GET"])
def lock_status(lock_id: str):
"""Full status + complete event log for a specific lock."""
...
Sample lock status response:
{
"lock_id": "lock_6752ac1dc0140e90a2852eab",
"state": "complete",
"amount_rtc": 100.0,
"target_chain": "solana",
"target_wallet": "7xKXtg2CW87...",
"release_tx": "4vqsH2kLpUx...",
"events": [
{"type": "lock_created", "actor": "my-wallet", "ts": 1741593600},
{"type": "released", "actor": "admin", "ts": 1741594200}
]
}
Building the Lock Endpoint
The /bridge/lock endpoint has careful input validation:
@bridge_bp.route("/lock", methods=["POST"])
def lock_rtc():
# Validate target chain
if target_chain not in {"solana", "base"}:
return jsonify({"error": f"target_chain must be 'solana' or 'base'"}), 400
# Validate amount
if amount_float < 1.0:
return jsonify({"error": "minimum lock amount is 1 RTC"}), 400
if amount_float > 10_000:
return jsonify({"error": "maximum lock amount is 10,000 RTC"}), 400
# Validate wallet format
if target_chain == "base" and not target_wallet.startswith("0x"):
return jsonify({"error": "Base wallet must be a 0x EVM address"}), 400
# Store with 6 decimal precision
amount_base = int(round(amount_float * 10**6))
lock_id = _generate_lock_id(sender, amount_base, target_chain, now)
...
A key design decision: storing amounts as integers (millionths of RTC, like satoshis in Bitcoin). This avoids floating-point precision issues when doing accounting across the ledger.
Lock States
The state machine tracks every lock through its lifecycle:
pending → confirmed → releasing → complete
↓ ↑
failed refunded
- pending: Lock received, awaiting confirmation
- confirmed: Admin verified the RTC was actually sent
- releasing: wRTC mint transaction in flight on target chain
- complete: wRTC successfully minted, lock closed
- failed/refunded: Something went wrong, RTC returned
Integration with Track A (Solana) and Track B (Base)
The bridge API plugs directly into the SPL token (Track A) and ERC-20 (Track B):
# After admin confirms lock, mint on Solana:
# spl-token mint <WRTC_MINT_ADDRESS> <AMOUNT> <TARGET_WALLET>
# Or on Base:
# cast send <WRTC_CONTRACT> "mint(address,uint256)" <TARGET_WALLET> <AMOUNT>
# Then call /bridge/release with the tx hash:
POST /bridge/release
{
"lock_id": "lock_abc123",
"release_tx": "0xabcdef...",
"notes": "Minted 100 wRTC on Base"
}
Testing: 14 Tests, All Passing
I wrote a full test suite covering all endpoints:
TestLockEndpoint (7 tests) — validation, min/max amounts, bad wallets
TestReleaseEndpoint (3 tests) — admin auth, full cycle, not found
TestLedgerEndpoint (3 tests) — filtering by chain/state/sender
TestStatsEndpoint (1 test) — response structure
Running the tests:
BRIDGE_DB_PATH=/tmp/bridge_test.db \
BRIDGE_ADMIN_KEY=test-key \
python3 -m pytest test_bridge_api.py -v
# === 14 passed in 1.03s ===
Bridge Statistics
The /bridge/stats endpoint gives a birds-eye view:
{
"by_state": {
"pending": {"count": 3, "total_rtc": 150.0},
"complete": {"count": 47, "total_rtc": 3200.0}
},
"by_chain": {
"solana": {"bridged_count": 25, "total_wrtc_minted": 1800.0},
"base": {"bridged_count": 22, "total_wrtc_minted": 1400.0}
},
"all_time": {
"total_locks": 50,
"total_rtc_locked": 3350.0
}
}
Dropping Into the Existing Flask App
The bridge module is a Flask Blueprint, so it drops cleanly into any existing Flask app:
# In your existing node file:
from bridge.bridge_api import register_bridge_routes
app = Flask(__name__)
register_bridge_routes(app) # adds /bridge/* routes
This was important because RustChain's node already runs a complex Flask app with attestation, miner tracking, governance, and more endpoints. I didn't want to touch any of that.
What's Next (Phase 2)
Phase 2 will make the bridge trustless by:
- On-chain lock proofs: RTC locked via a signed transaction that's verifiable without an admin
- Light client verification: SPL/ERC-20 minting triggered by verifiable proofs from RustChain
- Decentralized relayers: Multiple independent nodes process releases
But for Phase 1? The transparent ledger + admin key model is honest about its trust assumptions and gets real wRTC into the hands of RustChain supporters quickly.
Top comments (0)