Human-readable PERO-J contract events on Stellar. Instead of raw XDR bytes, users see: "Address GABC… swapped 100 USDC → 98.7 XLM on StellarSwap at ledger #4521983."
PERO-J have excellent support for classic assets but poor support for Soroban smart contracts. When a user calls swap on a DEX, explorers show raw XDR bytes — unreadable to anyone. This "black box" experience dampens DeFi, NFT, and web3 growth on Stellar.
PERO-J decodes contract calls on the fly using an ABI-like metadata registry, turning opaque XDR into plain English.
| Before | After |
|---|---|
AAAAA9hZ...[Raw XDR]...== |
Address GABC… swapped 100 USDC → 98.7 XLM on StellarSwap at ledger #4521983 |
┌─────────────────────────────────────────────────────────┐
│ PERO-J RPC / Horizon │
│ (getEvents, getTransaction) │
└────────────────────┬────────────────────────────────────┘
│ poll every 5 s
┌────────────────────▼────────────────────────────────────┐
│ Indexer (Node.js) │
│ • Fetches raw events via PERO-JRpc.getEvents() │
│ • Decodes XDR → human text using ABI registry │
│ • Stores decoded events in PostgreSQL │
│ • Exposes REST API on :3001 │
└────────────────────┬────────────────────────────────────┘
│ REST /api/*
┌────────────────────▼────────────────────────────────────┐
│ React Frontend (Vite + TanStack Query) │
│ • Home: paginated event feed + function filter │
│ • /contract/:id — ABI metadata + event history │
│ • /wallet/:address — wallet transaction history │
│ • /event/:seq — full decoded event detail │
└─────────────────────────────────────────────────────────┘
▲
┌────────────────────┴────────────────────────────────────┐
│ PERO-J Contract (Rust) │
│ • ContractRegistry — stores ABI-like metadata │
│ • EventDecoder — persists decoded events on-chain │
└─────────────────────────────────────────────────────────┘
- Rust +
wasm32-unknown-unknowntarget - Stellar CLI
- Node.js ≥ 20
- PostgreSQL
git clone https://github.com/PERO-J
cd PERO-J
cp .env.example .env
# Edit .env with your RPC URL and DATABASE_URLmake build # compile to WASM
make test # run unit tests
make deploy # deploy to testnet, prints CONTRACT_IDCopy the printed contract ID into .env as EXPLORER_CONTRACT_ID.
make indexer-install
make indexermake frontend-install
make frontend
# Open http://localhost:5173Or run both together:
make install
make dev| Function | Description |
|---|---|
init(admin) |
Initialise contract with admin address |
transfer_admin(current_admin, new_admin) |
Transfer admin rights; both parties must sign |
add_indexer(admin, indexer) |
Allowlist a hot wallet as a trusted event submitter (max 20) |
remove_indexer(admin, indexer) |
Revoke a previously allowlisted indexer |
get_indexers() |
List allowlisted indexer addresses |
is_indexer(address) |
Whether an address may submit events (admin or allowlisted) |
register_contract(caller, contract_id, meta) |
Register ABI metadata for a contract |
update_contract(caller, contract_id, meta) |
Update metadata (admin or registrant); emits update |
get_contract(contract_id) |
Fetch contract metadata |
submit_event(...) |
Persist a decoded event (admin or allowlisted indexer) |
get_event(seq) |
Fetch event by sequence number |
get_events(from, limit) |
Paginated event list; limit capped at 200 |
event_count() |
Total stored events |
Events emitted: register, update, decoded, adm_xfr, idx_add, idx_rm.
The update topic lets the indexer invalidate its ABI cache without polling storage.
| Endpoint | Description |
|---|---|
GET /health |
Liveness + lag probe — returns lag_seconds, uptime_seconds, last_ledger. HTTP 200 when healthy, 503 when lag_seconds > 30. |
GET /api/events?contract=&fn=&page= |
Paginated event list: { events, total, page, limit } |
GET /api/events/:seq |
Single event |
GET /api/contracts/:id |
Contract ABI metadata |
POST /api/contracts |
Register contract metadata |
GET /api/wallet/:address |
Wallet event history |
GET /api/tokens/:id/volume?decimals= |
24-hour rolling transfer volume for a SEP-41 token. Optional decimals query param (integer 0–38) overrides the on-chain metadata lookup. |
PostgreSQL events.seq is the canonical REST/frontend identifier. On-chain
EventSeq values are stored separately as nullable onchain_seq values because
the database row sequence and contract submission sequence are different
namespaces and can diverge.
GET /api/tokens/:id/volume returns the 24-hour rolling transfer volume for a SEP-41 token.
| Parameter | Type | Required | Description |
|---|---|---|---|
id (path) |
string | yes | Contract ID of the SEP-41 token |
decimals (query) |
integer 0–38 | no | Override decimal precision. When omitted, decimals are resolved from on-chain metadata / simulation (defaults to 7 if unavailable). |
Example response:
{
"contract_id": "CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP",
"window": "24h",
"volume": "1048576.0000000",
"decimals": 7
}Configure an external monitor (UptimeRobot, Better Uptime, or similar) to call
GET /health every 60 seconds and alert when the response is HTTP 503 or
lag_seconds > 30. This satisfies ROADMAP Tranche 2 deliverable 2.7 (< 10 s
index lag under normal load, alert threshold 30 s).
Example healthy response:
{
"status": "ok",
"uptime_seconds": 3600,
"lag_seconds": 4,
"last_ledger": 5214892,
"last_indexed_at": "2026-07-25T21:00:00.000Z"
}Example degraded response (HTTP 503):
{
"status": "degraded",
"uptime_seconds": 7200,
"lag_seconds": 142,
"last_ledger": 5214750,
"last_indexed_at": "2026-07-25T20:57:38.000Z"
}Override the alert threshold via the LAG_ALERT_THRESHOLD_S environment variable
(default 30).
The decoder recognises SEP-41 token events (transfer, mint, burn) and formats amounts with the correct symbol, alongside classic Stellar assets fetched from Horizon.
- Confirmed gap: StellarExpert and Stellar.expert (the two primary Stellar explorers) show raw XDR bytes for all Soroban contract events as of May 2026 — no human-readable decoding exists.
- Community signal: Developers in
#soroban-devon Stellar Discord regularly ask how to inspect their own contract events in a readable form. No existing tool answers this. - Comparable success: Etherscan's ABI decoder is one of its most-used features. Solscan built the same for Solana and became the primary explorer for Solana DeFi. Stellar has no equivalent for Soroban.
- Target users: Soroban dApp developers, DeFi users, NFT traders, auditors — anyone who needs to understand what is happening on-chain.
┌──────────────────────────────────────────────────────────────┐
│ Stellar Network │
│ ┌─────────────────────┐ ┌──────────────────────────────┐ │
│ │ PERO-J RPC │ │ Horizon API │ │
│ │ getEvents() │ │ Classic asset metadata │ │
│ │ getTransaction() │ │ (asset codes, issuers) │ │
│ └──────────┬──────────┘ └──────────────┬───────────────┘ │
└─────────────┼────────────────────────────┼─────────────────┘
│ poll every 5 s │ on-demand
┌─────────────▼────────────────────────────▼─────────────────┐
│ Indexer (Node.js) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ decoder.js │ │
│ │ scValToNative(topic/data) → match ABI registry │ │
│ │ → "Address GA… swapped 100 USDC → 98.7 XLM" │ │
│ └──────────────────────┬───────────────────────────────┘ │
│ ┌──────────────────────▼───────────────────────────────┐ │
│ │ db.js (PostgreSQL) │ │
│ │ events table · contracts table │ │
│ │ indexes on contract_id, function, ledger │ │
│ └──────────────────────┬───────────────────────────────┘ │
│ ┌──────────────────────▼───────────────────────────────┐ │
│ │ api.js (Express REST) │ │
│ │ GET /api/events · GET /api/contracts/:id │ │
│ │ GET /api/wallet/:address · POST /api/contracts │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────┬───────────────────────────────┘
│ REST /api/*
┌─────────────────────────────▼───────────────────────────────┐
│ React Frontend (Vite + TanStack Query) │
│ / — paginated feed, function filter │
│ /contract/:id — ABI metadata + event history │
│ /wallet/:addr — all events for a Stellar address │
│ /event/:seq — full decoded event detail │
└─────────────────────────────────────────────────────────────┘
▲
┌─────────────────────────────┴───────────────────────────────┐
│ PERO-J Contract (Rust) — on-chain source of truth │
│ ContractRegistry register_contract / get_contract │
│ EventDecoder submit_event / get_events / event_count │
└─────────────────────────────────────────────────────────────┘
Data flow for a decoded event:
- PERO-J contract emits an event (e.g.,
swapon StellarSwap) - Indexer fetches it via
SorobanRpc.getEvents() decoder.jscallsscValToNative()on topics/data, looks up registered ABI- Produces human-readable string → stored in PostgreSQL + submitted to on-chain contract
- Frontend queries REST API and displays the decoded event
| Document | Description |
|---|---|
| CHANGELOG.md | Full release history — what changed, what broke, what was added |
| ROADMAP.md | 3-tranche milestone plan (MVP → Testnet → Mainnet) |
| BUDGET.md | Engineering hours and cost breakdown per tranche |
| TEAM.md | Team bios and qualification evidence |
| MANIFEST.md | Full project manifest |
| stellar.toml | SEP-1 compliant network info |
Automated backups protect all decoded event history and registered ABI metadata stored in PostgreSQL.
scripts/backup.sh uses pg_dump to produce a plain-text SQL dump of the soroban_explorer database.
./scripts/backup.shEnvironment variables:
| Variable | Default | Description |
|---|---|---|
PGHOST |
localhost |
PostgreSQL host |
PGPORT |
5432 |
PostgreSQL port |
PGUSER |
user |
PostgreSQL user |
PGDATABASE |
soroban_explorer |
Database name |
PGPASSWORD |
(from env) | PostgreSQL password |
BACKUP_DIR |
./backups |
Directory for dump files |
LOG_FILE |
./logs/backup.log |
Backup log path |
Schedule daily backups at 02:00 UTC:
0 2 * * * /workspaces/PERO-J/scripts/backup.sh >> /var/log/backup.log 2>&1Or deploy with a systemd timer, Docker cron, or your platform's scheduled task scheduler.
To restore a backup into PostgreSQL:
# Stop the indexer to avoid data inconsistency
# Then pipe the dump into psql:
psql -h <host> -U <user> -d <database> -f backups/soroban_explorer_<timestamp>.sqlOr restore to a new database for verification:
createdb -h <host> -U <user> soroban_explorer_restore
psql -h <host> -U <user> -d soroban_explorer_restore -f backups/soroban_explorer_<timestamp>.sqlFor cloud-hosted PostgreSQL, enable automated backups via the managed service:
| Platform | Setting |
|---|---|
| AWS RDS | Enable automated backups in the RDS instance configuration; set backup retention period (recommended: 7+ days). Use snapshots for point-in-time recovery. |
| Google Cloud SQL | Enable automated backups in the instance settings; set backup start time and retention period. Use scheduled exports to Cloud Storage for additional safety. |
| Supabase | Dashboard > Project Settings > Database > Backups. Enable daily automatic backups. |
| Neon | Dashboard > Settings > Branches & Backups. Configure branch protection and auto-backup retention. |
For any cloud provider, also export a pg_dump weekly to object storage (S3, GCS) as an offsite copy.
PRs welcome. Please open an issue first for large changes.