audit rows from chain events. Interpretation decrypts those rows and writes normalized audit_interpretation rows used by cases, reports, and review screens.
End-to-end pipeline
Scanner runtime components
| Component | Responsibility |
|---|---|
ScannerModule.register | Registers scanner providers for configured Stellar and Solana chain names |
StellarRpcModule.register | Loads a chain row by name, validates type = stellar, creates @stellar/stellar-sdk/rpc.Server |
LedgerScannerSchedulerService | Runs bootstrap scan and scheduled Stellar scan ticks |
StellarLedgerScannerService | Lists contracts for one Stellar chain and scans each contract |
performStellarContractScan | Calculates ledger span from checkpoint, RPC retention, and scanner config |
collectStellarAuditRows | Reads ledger pages and collects accepted audit rows |
buildAuditFromStellarEvent | Maps Soroban event and calldata into an audit upsert object |
persistScanResultsQuery | Upserts audit, writes audit_chain, updates stellar_scans |
Contract selection
The scanner does not scan arbitrary chain activity.applications.association.contract_id links an Arcane application to a registered contract. That link is used later by application-scoped API routes and disclosure workflows.
Ledger span selection
For each contract, the scanner determines:| Input | Source |
|---|---|
| Previous checkpoint | stellar_scans.block_end for (contract_id, chain_id) |
| Initial hint | chain.initial_ledger_hint or STELLAR_INITIAL_LEDGER |
| RPC lower bound | getLedgers(...).oldestLedger |
| RPC tip | getLatestLedger().sequence |
| Batch size | SCANNER_LEDGERS_PER_REQUEST |
max(previousEnd + 1, oldestLedger). If the next ledger is past the RPC tip, the scan is skipped for that contract.
Event parsing
For each ledger page:- Read ledger metadata from Stellar RPC.
- Extract Soroban events for the registered contract address.
- Ignore events that are not in successful contract calls.
- Read
transactcalldata from ledger metadata when available. - Map the current
audittopic toevent_type = transact.
AuditEncodedDigest under the audit topic with message_name = "transact".
Raw audit row
The scanner writes oneaudit row per accepted event.
| Column | Source |
|---|---|
contract_id | Registered contracts.id |
tx_id | Stellar event transaction hash |
soroban_event_id | Stellar event id; unique with contract_id |
event_type | transact for current AuditEncodedDigest events |
cyphertext | Sealed audit payload from the event value |
created_at | Ledger close time |
interpreted | false until interpretation succeeds |
signer_account | Signer resolved from transact calldata, fallback unknown |
public_signals_json | Parsed transaction public signals |
interpretation_error | Last interpretation error, if any |
audit_chain and advances stellar_scans. The audit table is unique by (contract_id, soroban_event_id).
Interpretation runner
AuditInterpretationRunnerService runs on a 12-second interval.
Batch behavior:
- Open a database transaction.
- Lock up to 25 uninterpreted audit rows.
- Route Solana confidential-token rows to
interpretCtAuditRow. - Route all other rows to
interpretStellarAuditRow. - Replace interpretation rows for each successfully interpreted audit row.
- Store a truncated interpretation error on the source audit row if interpretation fails.
Stellar interpretation path
interpretStellarAuditRow performs:
| Step | Detail |
|---|---|
| Decode-key check | Requires audit.contract.decoding_key |
| Public signal parsing | Validates audit.public_signals_json |
| Payload decode | Calls decryptAndDecodeTransaction(cyphertext, decoding_key) |
| Planning | Calls planInterpretations(tx, publicSignals) |
| Persistence | Replaces rows in audit_interpretation for audit_id |
| Error capture | Writes audit.interpretation_error |
Interpreted records
audit_interpretation rows are normalized records.
| Column | Description |
|---|---|
audit_id | Source raw audit row |
seq | Sequence when one audit row creates multiple interpreted rows |
kind | Normalized kind: deposit, withdraw, or transfer |
payload | JSONB payload used by case review and reports |
amount_stroops | Parsed amount when available |
(audit_id, seq).
Event type versus interpretation kind
| Level | Example | Meaning |
|---|---|---|
| Raw event | transact | Soroban event/contract-call shape indexed from chain |
| Interpretation kind | deposit, withdraw, transfer | Normalized branch planned from decrypted transaction data and public signals |
transact event can produce multiple interpreted rows.
Failure behavior
| Failure | Effect |
|---|---|
| No contracts for chain | Scanner logs and returns |
| RPC retention moved past checkpoint | Start ledger clamps to oldestLedger |
| RPC/network failure | Scan cycle logs an error; checkpoint is not advanced for failed work |
| Parser returns no audit row | Event is ignored |
| Duplicate event | Upsert updates the existing audit row |
| Missing decoding key | Interpretation error is written to audit.interpretation_error |
| Decryption or planning failure | Interpretation error is written; indexing is not rolled back |