diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 43981306..e994dfb4 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -45,6 +45,11 @@ jobs: cd stellar-lend cargo fmt --all -- --check + - name: Check event schema compliance + run: | + cd stellar-lend + python3 scripts/check_event_schemas.py + - name: Run clippy run: | cd stellar-lend diff --git a/docs/event-indexing.md b/docs/event-indexing.md index bb45d63c..a0e363fd 100644 --- a/docs/event-indexing.md +++ b/docs/event-indexing.md @@ -2,6 +2,13 @@ This guide documents the event surfaces that exist in the current StellarLend workspace and how to consume them safely. +The canonical event compatibility rules live in +[`stellar-lend/docs/event-schema.md`](../stellar-lend/docs/event-schema.md), with the +machine-readable schema in +[`stellar-lend/docs/event-schema.v1.json`](../stellar-lend/docs/event-schema.v1.json). +CI runs `stellar-lend/scripts/check_event_schemas.py` to keep `#[contractevent]` definitions, +legacy manual topics, and the schema version constants in sync. + It is written around the code that exists today: - `stellar-lend/contracts/hello-world` is the main protocol contract currently used by the API. @@ -26,6 +33,8 @@ For this repo there are two emission styles: Important decoding notes: +- Decoded rows should include `_schema_version: 1` and `_event_topic` metadata when persisted by + indexers. The prototype `indexing_system` annotates parsed payloads with these fields. - `Option
` means the native asset can appear as `None`. - Amounts are raw integer amounts in the token's smallest unit. - Risk parameters and reserve factors are basis points unless the source struct says otherwise. diff --git a/package.json b/package.json index 49794f0a..ec94cab1 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "deploy:verify": "bash scripts/verify-deployment.sh", "deploy:rollback": "bash scripts/rollback.sh", "verify:contract": "bash scripts/verify-contract.sh", + "contracts:event-schema:check": "python stellar-lend/scripts/check_event_schemas.py", "mutation:api": "npm --workspace api run mutation:test", "mutation:api:check": "npm --workspace api run mutation:check" }, diff --git a/stellar-lend/Cargo.toml b/stellar-lend/Cargo.toml index e1cea76e..5afa9c8b 100644 --- a/stellar-lend/Cargo.toml +++ b/stellar-lend/Cargo.toml @@ -13,7 +13,7 @@ members = [ "contracts/institutional-wallet", "contracts/migration-hub", ] -exclude = ["fuzz"] +exclude = ["fuzz", "indexing_system"] [workspace.dependencies] soroban-sdk = "23.4.1" diff --git a/stellar-lend/contracts/common/src/event_schema.rs b/stellar-lend/contracts/common/src/event_schema.rs new file mode 100644 index 00000000..02415003 --- /dev/null +++ b/stellar-lend/contracts/common/src/event_schema.rs @@ -0,0 +1,27 @@ +//! Shared event schema constants for StellarLend contracts and indexers. +//! +//! Version 1 is intentionally additive: existing event topics and payloads remain stable while +//! new documentation, indexer metadata, and CI checks describe the canonical schema. + +use soroban_sdk::contracttype; + +pub const EVENT_SCHEMA_VERSION: u32 = 1; +pub const EVENT_SCHEMA_VERSION_FIELD: &str = "_schema_version"; +pub const EVENT_TOPIC_FIELD: &str = "_event_topic"; +pub const MAX_SOROBAN_EVENT_TOPICS: u32 = 4; + +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum EventSchemaVersion { + V1 = 1, +} + +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum EventPayloadFormat { + ContractEvent = 1, + SingleValue = 2, + LegacyTuple = 3, +} diff --git a/stellar-lend/contracts/common/src/lib.rs b/stellar-lend/contracts/common/src/lib.rs index b806f230..774469b7 100644 --- a/stellar-lend/contracts/common/src/lib.rs +++ b/stellar-lend/contracts/common/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] #![allow(deprecated)] pub mod cache; +pub mod event_schema; pub mod events; pub mod message_bus; pub mod shared_types; diff --git a/stellar-lend/docs/event-schema.md b/stellar-lend/docs/event-schema.md new file mode 100644 index 00000000..c6155f23 --- /dev/null +++ b/stellar-lend/docs/event-schema.md @@ -0,0 +1,41 @@ +# StellarLend Event Schema v1 + +Schema Version: 1 + +This document defines the event compatibility rules for the StellarLend Soroban contracts. The +machine-readable companion file is `event-schema.v1.json`. + +## Compatibility Rules + +- Existing event topics are append-only. Do not rename or remove a topic without adding a migration note. +- New typed events should use `#[contractevent]` and lower snake case topic names. +- Manual `env.events().publish(...)` calls are allowed only for legacy compatibility and must be listed in the schema JSON. +- Event topics must fit Soroban's four-topic limit, including static topics and `#[topic]` fields. +- Field names must use lower snake case and payload fields should stay append-only. +- Amounts are raw token units. Percentages use basis points where `10000` is `100%`. +- Indexers should persist raw topics and decoded payloads, then annotate decoded rows with `_schema_version` and `_event_topic`. + +## Common Field Semantics + +| Field | Meaning | +| --- | --- | +| `user` | End-user account that initiated or owns a position-changing action. | +| `actor` | Authorized protocol account that performed an administrative action. | +| `caller` | Authenticated account that invoked an administrative or governance entrypoint. | +| `asset` | Soroban token contract address; `None` represents native asset where supported. | +| `amount` | Raw integer amount in the asset's smallest unit. | +| `fee` | Raw integer fee in the asset's smallest unit. | +| `timestamp` | Ledger timestamp in seconds. | +| `*_bps` | Basis points; `10000` is `100%`. | + +## Versioning + +Version 1 is additive. It standardizes documentation, CI validation, and indexer metadata without +changing existing runtime event topics or payload shapes. + +For a future breaking change: + +1. Add a new `event-schema.vN.json` file. +2. Keep the old schema available for historical decoding. +3. Add an explicit migration section to `docs/event-indexing.md`. +4. Update indexers to decode both versions during the migration window. diff --git a/stellar-lend/docs/event-schema.v1.json b/stellar-lend/docs/event-schema.v1.json new file mode 100644 index 00000000..4f4a3fba --- /dev/null +++ b/stellar-lend/docs/event-schema.v1.json @@ -0,0 +1,140 @@ +{ + "schema_version": 1, + "topic_style": "lower_snake_case", + "contracts": [ + { + "name": "hello-world", + "source_globs": ["contracts/hello-world/src/**/*.rs"], + "active": true + }, + { + "name": "lending", + "source_globs": ["contracts/lending/src/**/*.rs"], + "active": true + }, + { + "name": "amm", + "source_globs": ["contracts/amm/src/**/*.rs"], + "active": true + }, + { + "name": "bridge", + "source_globs": ["contracts/bridge/src/**/*.rs"], + "active": true + }, + { + "name": "stablecoin", + "source_globs": ["contracts/stablecoin/src/**/*.rs"], + "active": true + }, + { + "name": "common", + "source_globs": ["contracts/common/src/**/*.rs"], + "active": true + } + ], + "allow_overloaded_topics": [ + { + "contract": "lending", + "topic": "deposit_event", + "reason": "Vault deposits and borrow-collateral deposits predate this schema and are distinguished by payload shape." + } + ], + "legacy_manual_events": [ + { + "contract": "hello-world", + "topic": "bridge", + "format": "legacy_tuple" + }, + { + "contract": "hello-world", + "topic": "mon_hlth", + "format": "legacy_tuple" + }, + { + "contract": "hello-world", + "topic": "mon_init", + "format": "legacy_tuple" + }, + { + "contract": "hello-world", + "topic": "mon_perf", + "format": "legacy_tuple" + }, + { + "contract": "hello-world", + "topic": "mon_sec", + "format": "legacy_tuple" + }, + { + "contract": "hello-world", + "topic": "rep_add", + "format": "legacy_tuple" + }, + { + "contract": "hello-world", + "topic": "rep_del", + "format": "legacy_tuple" + }, + { + "contract": "lending", + "topic": "bad_debt", + "format": "legacy_tuple" + }, + { + "contract": "lending", + "topic": "bad_debt_recovered", + "format": "legacy_tuple" + }, + { + "contract": "lending", + "topic": "yield_compounded", + "format": "legacy_tuple" + }, + { + "contract": "lending", + "topic": "yield_deposit", + "format": "legacy_tuple" + }, + { + "contract": "stablecoin", + "topic": "stablecoin_initialized", + "format": "legacy_tuple" + }, + { + "contract": "stablecoin", + "topic": "shutdown_set", + "format": "legacy_tuple" + }, + { + "contract": "stablecoin", + "topic": "reserve_ratio_set", + "format": "legacy_tuple" + }, + { + "contract": "stablecoin", + "topic": "collateral_deposited", + "format": "legacy_tuple" + }, + { + "contract": "stablecoin", + "topic": "stablecoin_minted", + "format": "legacy_tuple" + }, + { + "contract": "stablecoin", + "topic": "stablecoin_redeemed", + "format": "legacy_tuple" + } + ], + "field_semantics": { + "user": "End-user account that initiated or owns the position-changing operation.", + "actor": "Authorized protocol account that performed an administrative operation.", + "caller": "Authenticated account that invoked an administrative or governance entrypoint.", + "asset": "Soroban token contract address; null/None represents the native asset where supported.", + "amount": "Raw integer amount in the asset's smallest unit.", + "fee": "Raw integer fee amount in the asset's smallest unit.", + "timestamp": "Ledger timestamp in seconds.", + "bps": "Basis points, where 10000 is 100%." + } +} diff --git a/stellar-lend/indexing_system/src/lib.rs b/stellar-lend/indexing_system/src/lib.rs index 9d7a2655..b8099c1f 100644 --- a/stellar-lend/indexing_system/src/lib.rs +++ b/stellar-lend/indexing_system/src/lib.rs @@ -8,6 +8,7 @@ pub mod parallel_indexer; pub mod parser; pub mod query; pub mod repository; +pub mod schema; #[cfg(test)] pub mod tests; @@ -20,10 +21,16 @@ pub use metrics::{IndexingMetrics, MetricsSnapshot}; pub use models::{ CreateEvent, Event, EventQuery, EventStats, EventUpdate, IndexingMetadata, UpdateType, }; -pub use parallel_indexer::{BlockProcessor, BlockRangeResult, BlockRangeTask, ParallelIndexer, StateManager}; +pub use parallel_indexer::{ + BlockProcessor, BlockRangeResult, BlockRangeTask, ParallelIndexer, StateManager, +}; pub use parser::{create_erc20_abi, EventParser}; pub use query::QueryService; pub use repository::EventRepository; +pub use schema::{ + annotate_event_payload, load_standard_event_schema, normalize_event_topic, EventSchemaDocument, + STANDARD_EVENT_SCHEMA_VERSION, +}; pub fn init_tracing() { use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; @@ -77,12 +84,9 @@ pub async fn run_migrations(database_url: &str) -> IndexerResult<()> { /// /// This is the main initialization function that should be called /// to set up the entire indexing system. -pub async fn initialize_system(config: &Config) -> IndexerResult<( - EventRepository, - CacheService, - QueryService, - IndexerService, -)> { +pub async fn initialize_system( + config: &Config, +) -> IndexerResult<(EventRepository, CacheService, QueryService, IndexerService)> { info!("Initializing indexing system..."); // Run database migrations first diff --git a/stellar-lend/indexing_system/src/parser.rs b/stellar-lend/indexing_system/src/parser.rs index b3f9ef97..4622f81e 100644 --- a/stellar-lend/indexing_system/src/parser.rs +++ b/stellar-lend/indexing_system/src/parser.rs @@ -1,6 +1,7 @@ /// Event parser for decoding smart contract events use crate::error::{IndexerError, IndexerResult}; use crate::models::CreateEvent; +use crate::schema::annotate_event_payload; use ethers::abi::{Abi, Event as AbiEvent, RawLog}; use ethers::prelude::*; use serde_json::Value; @@ -100,6 +101,7 @@ impl EventParser { let value = self.token_to_json(¶m.value)?; event_data.insert(param.name, value); } + annotate_event_payload(&mut event_data, &event_def.name); Ok(Some(CreateEvent { contract_address, diff --git a/stellar-lend/indexing_system/src/schema.rs b/stellar-lend/indexing_system/src/schema.rs new file mode 100644 index 00000000..dd064073 --- /dev/null +++ b/stellar-lend/indexing_system/src/schema.rs @@ -0,0 +1,105 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +pub const STANDARD_EVENT_SCHEMA_VERSION: u32 = 1; +pub const EVENT_SCHEMA_VERSION_FIELD: &str = "_schema_version"; +pub const EVENT_TOPIC_FIELD: &str = "_event_topic"; +pub const EVENT_SCHEMA_JSON: &str = include_str!("../../docs/event-schema.v1.json"); + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct EventSchemaDocument { + pub schema_version: u32, + pub topic_style: String, + pub contracts: Vec