Skip to content
john2ydep2-gtPublic

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

745 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⬡ PERO-J

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

License: MIT Built on Stellar


The Problem

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.

The Solution

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

Architecture

┌─────────────────────────────────────────────────────────┐
│  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      │
└─────────────────────────────────────────────────────────┘

Quick Start

Prerequisites

  • Rust + wasm32-unknown-unknown target
  • Stellar CLI
  • Node.js ≥ 20
  • PostgreSQL

Just want to explore the running app? You can skip everything below and bring up the full stack (PostgreSQL, Redis, Soroban sandbox, indexer, and frontend — pre-seeded with test markets and simulated users) with a single command using Docker. See the Docker Compose section:

docker compose up --build

Then open http://localhost:5173.

1. Clone & configure

git clone https://github.com/PERO-J
cd PERO-J
cp .env.example .env
# Edit .env with your RPC URL and DATABASE_URL. See [.env.example](.env.example)
# for all supported indexer, database, contract, and SEP-41 settings.

2. Build & deploy the contract

make build      # compile to WASM
make test       # run unit tests
make deploy     # deploy to testnet, prints CONTRACT_ID

Copy the printed contract ID into .env as EXPLORER_CONTRACT_ID.

3. Start the indexer + API

The indexer validates NETWORK_PASSPHRASE on startup by querying the RPC endpoint. If the configured passphrase does not match the RPC-reported network, the indexer logs an error and exits with code 1. This prevents mainnet/testnet mix-ups.

make indexer-install
make indexer

4. Start the frontend

make frontend-install
make frontend
# Open http://localhost:5173

Or run both together:

make install
make dev

Docker Compose (local development)

Spin up the entire stack — PostgreSQL, Redis, Soroban local sandbox, indexer API (with hot-reload), and the frontend (with hot-reload), plus pre-seeded test markets and simulated user accounts — with a single command:

docker compose up --build

That's it. The first run builds the images and pre-applies the database migrations before the indexer starts; the seed one-shot service then pre-seeds three test markets (StellarSwap, Blend, Phoenix) and a dozen sample decoded events for four simulated wallet addresses, so the frontend has data to show immediately.

Then open:

Services

Service Port Notes
postgres 5432 PostgreSQL 16; data persisted in the postgres-data volume
redis 6379 Redis 7 (AOF); provisioned for future caching/rate-limit use
soroban 8000 stellar/quickstart local network with Soroban RPC enabled
migrate — One-shot: pre-applies schema migrations before the indexer starts
indexer 3001 Node.js API + poller; hot-reload via node --watch (source bind-mounted)
seed — One-shot: pre-seeds test markets + simulated user accounts (idempotent)
frontend 5173 Vite dev server; hot-reload (source bind-mounted); proxies /api to the indexer

How it fits together

  • The migrate service runs the indexer's schema migrations (npm run migrate, which calls db.init()) and exits 0 before the indexer starts, so the schema is pre-applied.
  • The indexer waits for PostgreSQL, the Soroban sandbox, and migrate to be ready, then polls the local sandbox (not testnet) and serves the REST API.
  • The frontend runs the Vite dev server with the source bind-mounted for hot-reload. It proxies /api/* to the indexer via the VITE_API_PROXY environment variable (default http://indexer:3001 inside the compose network), so no CORS configuration is required.
  • The seed service upserts the ABI fixtures and inserts sample decoded events (only when the events table is empty, so re-running docker compose up does not duplicate seed rows).

Configuration

Defaults work out of the box. Override any value by exporting the variable or placing it in a .env file next to docker-compose.yml (see .env.example):

Variable Default Description
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB peroj PostgreSQL credentials/database
DATABASE_URL postgres://peroj:peroj@postgres:5432/peroj Indexer DB connection string
DATABASE_POOL_SIZE 5 Indexer Postgres pool size (migrate) / 20 (indexer)
NETWORK_PASSPHRASE Standalone Network ; February 2017 Sandbox network passphrase
START_LEDGER 1 Ledger to start indexing from
POLL_MS 5000 Indexer poll interval (ms)
SOROBAN_EXPLORER_CONTRACT_ID (empty) Set to submit decoded events on-chain (optional)
SOROBAN_EXPLORER_ADMIN_SECRET (empty) Admin secret for on-chain submit_event (optional)
API_ADMIN_KEY (empty) Required for POST/DELETE /api/contracts
VITE_API_PROXY http://indexer:3001 Vite dev proxy target for /api

Stop the stack with docker compose down (add -v to also drop the database and Redis volumes).

Resetting the seed data

The seed is idempotent — it only inserts sample events when the events table is empty. To re-seed from scratch:

docker compose down -v        # wipe the postgres volume
docker compose up --build      # migrate + seed run again

Contract API

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.


REST API

Endpoint Description
GET /health Liveness + lag probe — returns lag_seconds, uptime_seconds, last_ledger. HTTP 200 when healthy, 503 when lag_seconds > LAG_ALERT_THRESHOLD_S (default 30).
GET /api/events?contract=&fn=&page= Paginated event list: { events, total, page, limit }
GET /api/events/:seq Single event
GET /api/events/:seq/raw Raw un-decoded event topics and data: { seq, raw_topics, raw_data, tx_hash }
GET /api/contracts/:id Contract ABI metadata
GET /api/contracts/:id/events?fn=&page= Paginated event list for contract: { events, total, page, limit }
POST /api/contracts Register contract metadata
DELETE /api/contracts/:id Remove contract ABI metadata (requires Authorization: Bearer <API_ADMIN_KEY>)
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.
GET /api/v1/oracle/disputes Active Oracle disputes with asserted outcomes, disputer addresses, council votes and tallies, deadline, and remaining voting time. Requires ORACLE_CONTRACT_ID; returns HTTP 503 if it is unset.
POST /api/v1/oracle/resolutions/:submissionId/evidence Store immutable raw source evidence for a resolution (requires API_ADMIN_KEY).
GET /api/v1/oracle/resolutions/:submissionId/evidence Publicly retrieve the evidence bundle and its SHA-256 hash for verification.

The disputes endpoint reads the Oracle contract on each request. Each dispute includes affirmative_votes, negative_votes, the individual votes, an ISO 8601 deadline, and remaining_seconds / human-readable remaining_time.

The resolution evidence write endpoint is the ingestion hook for the automated resolution worker. It accepts response_payload (the exact raw response body as a string), response_status (100–599), response_headers (string-valued object), and resolved_at (timestamp). Each submission ID can be recorded once; retries with an identical evidence bundle return the original record, while different evidence for an existing submission returns HTTP 409. The database rejects updates, deletes, and truncation of audit records.

The public endpoint returns submission_id, response_payload, response_status, response_headers, resolved_at, recorded_at, and evidence_hash. The hash is SHA-256 over the UTF-8 bytes of the JSON serialization of those evidence fields except recorded_at and evidence_hash, in this order: submission_id, response_payload, response_status, response_headers, resolved_at. Header keys are sorted in ascending ASCII order and represented in the hash input as an array of [name, value] pairs; the timestamp is normalized to ISO 8601 before hashing. This lets clients independently reproduce the digest.

Oracle council inactivity alerts

When ORACLE_CONTRACT_ID, ORACLE_COUNCIL_ROSTER, and PAGERDUTY_ROUTING_KEY are configured, the indexer checks active disputes hourly and sends PagerDuty Events API v2 alerts when a dispute has not reached quorum after 24, 48, and 60 hours of its 72-hour voting period. The roster is a comma-separated list of council Stellar addresses. ORACLE_COUNCIL_QUORUM can set the required number of votes; when omitted, a strict majority of the configured roster is used. The PagerDuty service associated with the routing key should use an escalation policy that notifies the council roster. The monitor is disabled with a startup warning if any required setting is absent.

Event sequence namespaces

The events table tracks two distinct sequence numbers:

Column Type Description
seq BIGSERIAL (PK) Auto-increment database sequence. The canonical identifier used by the REST API and frontend (e.g. GET /api/events/:seq). Always present.
onchain_seq BIGINT (nullable) The EventSeq value returned by the on-chain ExplorerContract after a successful submission. NULL when the event was not submitted or the submission failed.

These two namespaces are independent and will diverge over time — not every database row has a corresponding on-chain submission, and on-chain sequence numbers are scoped to the contract, not the database. Always use seq to reference events in API calls and frontend URLs.

Volume endpoint

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
}

Uptime Monitoring

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 > LAG_ALERT_THRESHOLD_S (default 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). Non-numeric values fall back to the default of 30.


SEP-41 Token Support

The decoder recognises SEP-41 token events (transfer, mint, burn) and formats amounts with the correct symbol, alongside classic Stellar assets fetched from Horizon.


Validated Need & Traction

  • 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

Detailed Architecture

┌──────────────────────────────────────────────────────────────┐
│  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:

  1. PERO-J contract emits an event (e.g., swap on StellarSwap)
  2. Indexer fetches it via SorobanRpc.getEvents()
  3. decoder.js calls scValToNative() on topics/data, looks up registered ABI
  4. Produces human-readable string → stored in PostgreSQL + submitted to on-chain contract
  5. Frontend queries REST API and displays the decoded event

SCF Submission Documents

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

Database Backup

Automated backups protect all decoded event history and registered ABI metadata stored in PostgreSQL.

Local Backup Script

scripts/backup.sh uses pg_dump to produce a plain-text SQL dump of the soroban_explorer database.

./scripts/backup.sh

Environment 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

Automated Cron Job

Schedule daily backups at 02:00 UTC:

0 2 * * * /workspaces/PERO-J/scripts/backup.sh >> /var/log/backup.log 2>&1

Or deploy with a systemd timer, Docker cron, or your platform's scheduled task scheduler.

Restore Procedure

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

Or 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>.sql

Cloud Deployments

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


Production Docker images

The indexer ships an optimized multi-stage production image (indexer/Dockerfile / indexer/Dockerfile.indexer): production-only dependencies (no dev deps), the transitive sodium-native native package stripped, docs/tests/source-maps pruned, and a non-root appuser runtime. The result is a ~140MB uncompressed (~50MB compressed) image that never runs as root, with the Kubernetes deployment enforcing runAsNonRoot and dropping all Linux capabilities. See docs/kubernetes-deployment.md for the hardening details.

Contributing

PRs welcome. Please open an issue first for large changes.


Note to maintainers

  • Issue #792: EventPage already validates the seq param (isValidSeq) and skips the API request for invalid values; no code change needed.
  • Issue #793: CopyButton already accepts an ariaLabel prop applied as aria-label, and all usages pass labels; no code change needed.
  • Issue #794: Skeleton already supports variant="table" and "card" and is used in Home, ContractPage and WalletPage; no code change needed.
  • Issue #795: Home.tsx already debounces the search query (300ms, cleared on unmount) via debouncedSearchQuery; no code change needed.

License

MIT

Handsoff notes

  • #850: Implement transfer_admin requiring both parties to sign
  • #745: init() must be permanently irreversible after first call

Documentation

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages