diff --git a/scripts/generate_sdk_models.py b/scripts/generate_sdk_models.py index 0fdbaa6..e82315a 100755 --- a/scripts/generate_sdk_models.py +++ b/scripts/generate_sdk_models.py @@ -144,6 +144,14 @@ def generate_rust() -> None: ) TARGETS["rust"].write_text(output) + # Run rustfmt over the result so this file is byte-identical to what + # `cargo fmt --all` produces. Without it the two CI jobs contradict each + # other: quicktype emits `use serde::{Serialize, Deserialize};`, rustfmt + # reorders that to `{Deserialize, Serialize}`, and whichever job ran last + # left the other one red — the Rust job on a formatting diff, or this + # script's own drift check on the reordered import. + run_checked(["rustfmt", "--edition", "2021", str(TARGETS["rust"])]) + def generate_typescript() -> None: run_checked( diff --git a/sdk/go/openapi/models_gen.go b/sdk/go/openapi/models_gen.go index 761c9b2..6e9d0b3 100644 --- a/sdk/go/openapi/models_gen.go +++ b/sdk/go/openapi/models_gen.go @@ -21,13 +21,70 @@ func (r *OpenAPIModels) Marshal() ([]byte, error) { } type OpenAPIModels struct { - ContractStats *ContractStats `json:"ContractStats,omitempty"` - ContractStatsResponse *ContractStatsResponse `json:"ContractStatsResponse,omitempty"` - ErrorResponse *ErrorResponse `json:"ErrorResponse,omitempty"` - EventListResponse *EventListResponse `json:"EventListResponse,omitempty"` - HealthResponse *HealthResponse `json:"HealthResponse,omitempty"` - IndexerStatsResponse *IndexerStatsResponse `json:"IndexerStatsResponse,omitempty"` - SorobanEvent *SorobanEvent `json:"SorobanEvent,omitempty"` + ContractEventFieldSchema *ContractEventFieldSchema `json:"ContractEventFieldSchema,omitempty"` + ContractEventSchema *ContractEventSchema `json:"ContractEventSchema,omitempty"` + ContractEventSchemaResponse *ContractEventSchemaResponse `json:"ContractEventSchemaResponse,omitempty"` + ContractSpecFunction *ContractSpecFunction `json:"ContractSpecFunction,omitempty"` + ContractSpecResponse *ContractSpecResponse `json:"ContractSpecResponse,omitempty"` + ContractStats *ContractStats `json:"ContractStats,omitempty"` + ContractStatsResponse *ContractStatsResponse `json:"ContractStatsResponse,omitempty"` + ContractStorageResponse *ContractStorageResponse `json:"ContractStorageResponse,omitempty"` + ContractStorageValue *ContractStorageValue `json:"ContractStorageValue,omitempty"` + ErrorResponse *ErrorResponse `json:"ErrorResponse,omitempty"` + EventListResponse *EventListResponse `json:"EventListResponse,omitempty"` + IndexerStatsResponse *IndexerStatsResponse `json:"IndexerStatsResponse,omitempty"` + LivenessResponse *LivenessResponse `json:"LivenessResponse,omitempty"` + ReadyChecks *ReadyChecks `json:"ReadyChecks,omitempty"` + ReadyResponse *ReadyResponse `json:"ReadyResponse,omitempty"` + SorobanEvent *SorobanEvent `json:"SorobanEvent,omitempty"` + TokenMetadataResponse *TokenMetadataResponse `json:"TokenMetadataResponse,omitempty"` +} + +type ContractEventFieldSchema struct { + // Stable field name for this event payload position or property + Name string `json:"name"` + // Field type inferred from the contract interface or observed payloads + Type string `json:"type"` +} + +type ContractEventSchema struct { + // Contract event name (topic_0) + EventName string `json:"event_name"` + // Named fields for this event payload + Fields []ContractEventFieldSchema `json:"fields"` +} + +type ContractEventSchemaResponse struct { + // Contract code hash for this schema version + CodeHash string `json:"code_hash"` + // Soroban contract address + ContractID string `json:"contract_id"` + // Observed event names and their typed field schemas + Events []ContractEventSchema `json:"events"` + // Network queried + Network Network `json:"network"` +} + +type ContractSpecFunction struct { + // Exported function name + Name string `json:"name"` +} + +type ContractSpecResponse struct { + // Deployed WASM code hash this spec was parsed from + CodeHash string `json:"code_hash"` + // Soroban contract address + ContractID string `json:"contract_id"` + // Primary classification derived from detected interfaces (e.g. token, nft, custom) + ContractType string `json:"contract_type"` + // Functions captured from the contract's spec + Functions []ContractSpecFunction `json:"functions"` + // Whether an embedded contractspecv0 section was found + HasSpec bool `json:"has_spec"` + // Every standard interface detected from the contract's spec functions + Interfaces []string `json:"interfaces"` + // Network queried + Network Network `json:"network"` } type ContractStats struct { @@ -54,6 +111,28 @@ type ContractStatsResponse struct { ToLedger int64 `json:"to_ledger"` } +type ContractStorageResponse struct { + // Soroban contract address + ContractID string `json:"contract_id"` + // Network queried + Network Network `json:"network"` + // Storage snapshot values (latest, or full history when queried via /storage/history) + Values []ContractStorageValue `json:"values"` +} + +type ContractStorageValue struct { + // Human-readable decoded storage key + Key interface{} `json:"key"` + // Ledger sequence at which this value was observed + LedgerSequence int64 `json:"ledger_sequence"` + // Timestamp this snapshot row was recorded + ObservedAt time.Time `json:"observed_at"` + // Base64-encoded XDR LedgerKey this value was read from + StorageKey string `json:"storage_key"` + // Human-readable decoded value (absent when the entry was removed) + Value interface{} `json:"value"` +} + type ErrorResponse struct { Error Error `json:"error"` } @@ -99,38 +178,71 @@ type SorobanEvent struct { TransactionHash string `json:"transaction_hash"` } -type HealthResponse struct { - Indexer Indexer `json:"indexer"` - // Overall system status - Status HealthResponseStatus `json:"status"` +type IndexerStatsResponse struct { + // Average poll duration in milliseconds + AvgPollDurationMS *int64 `json:"avg_poll_duration_ms,omitempty"` + // Current chain tip ledger (from RPC) + ChainTipLedger *int64 `json:"chain_tip_ledger,omitempty"` + // Cumulative events indexed + EventsIndexedTotal *int64 `json:"events_indexed_total,omitempty"` + // Events processed in last poll + EventsLastPoll *int64 `json:"events_last_poll,omitempty"` + // Number of ledgers behind chain tip + LagLedgers *int64 `json:"lag_ledgers,omitempty"` + // Estimated wall-clock staleness in seconds: lag_ledgers times Stellar's protocol-target + // ledger close time (~5s). Null whenever lag_ledgers is null. See + // docs/observability/data-freshness.md for the full freshness contract this field is part + // of. + LagSecondsEstimated *float64 `json:"lag_seconds_estimated,omitempty"` + // Latest indexed ledger sequence + LastLedgerIndexed *int64 `json:"last_ledger_indexed,omitempty"` + // Timestamp of last successful poll + LastPollAt *time.Time `json:"last_poll_at,omitempty"` + // Network name from NETWORK environment variable + Network string `json:"network"` + // Indexer health status + Status IndexerStatsResponseStatus `json:"status"` } -type Indexer struct { - // Latest indexed ledger sequence - LastLedgerIndexed int64 `json:"last_ledger_indexed"` - // Timestamp of last successful indexer poll - LastPollAt *time.Time `json:"last_poll_at,omitempty"` +type LivenessResponse struct { + // Always "ok" while the process is up — no dependency checks. + Status LivenessResponseStatus `json:"status"` } -type IndexerStatsResponse struct { - // Average poll duration in milliseconds - AvgPollDurationMS *int64 `json:"avg_poll_duration_ms,omitempty"` - // Current chain tip ledger (from RPC) - ChainTipLedger *int64 `json:"chain_tip_ledger,omitempty"` - // Cumulative events indexed - EventsIndexedTotal *int64 `json:"events_indexed_total,omitempty"` - // Events processed in last poll - EventsLastPoll *int64 `json:"events_last_poll,omitempty"` - // Number of ledgers behind chain tip - LagLedgers *int64 `json:"lag_ledgers,omitempty"` - // Latest indexed ledger sequence - LastLedgerIndexed *int64 `json:"last_ledger_indexed,omitempty"` - // Timestamp of last successful poll - LastPollAt *time.Time `json:"last_poll_at,omitempty"` - // Network name from NETWORK environment variable - Network string `json:"network"` - // Indexer health status - Status IndexerStatsResponseStatus `json:"status"` +type ReadyChecks struct { + // "ok" or "error: " + GrpcAPI string `json:"grpc_api"` + // "ok" or "error: " + Postgres string `json:"postgres"` + // "ok" or "error: " + Redis string `json:"redis"` +} + +type ReadyResponse struct { + Checks ReadyChecks `json:"checks"` + // Ledgers behind chain tip, from system_state. Null when Postgres is unreachable or the + // chain-tip cache hasn't been populated yet. + IndexerLag int64 `json:"indexer_lag"` + // "degraded" when any dependency check in `checks` failed. + Status ReadyResponseStatus `json:"status"` +} + +type TokenMetadataResponse struct { + // Soroban contract address + ContractID string `json:"contract_id"` + // Token decimals, from decimals(). Null unless is_token is true. + Decimals *int64 `json:"decimals,omitempty"` + // True when the contract was resolved and implements the SEP-41 read interface. False for + // both "not yet resolved" and "resolved, not a token". + IsToken bool `json:"is_token"` + // Token name, from name(). Null unless is_token is true. + Name *string `json:"name,omitempty"` + // Network queried + Network Network `json:"network"` + // When this contract was last resolved. Null if never resolved. + ResolvedAt *time.Time `json:"resolved_at,omitempty"` + // Token symbol, from symbol(). Null unless is_token is true. + Symbol *string `json:"symbol,omitempty"` } // Network queried @@ -150,14 +262,6 @@ const ( System EventType = "system" ) -// Overall system status -type HealthResponseStatus string - -const ( - Degraded HealthResponseStatus = "degraded" - Ok HealthResponseStatus = "ok" -) - // Indexer health status type IndexerStatsResponseStatus string @@ -166,3 +270,18 @@ const ( Lagging IndexerStatsResponseStatus = "lagging" Stalled IndexerStatsResponseStatus = "stalled" ) + +// Always "ok" while the process is up — no dependency checks. +type LivenessResponseStatus string + +const ( + PurpleOk LivenessResponseStatus = "ok" +) + +// "degraded" when any dependency check in `checks` failed. +type ReadyResponseStatus string + +const ( + Degraded ReadyResponseStatus = "degraded" + FluffyOk ReadyResponseStatus = "ok" +) diff --git a/sdk/python/src/trident_indexer/__init__.py b/sdk/python/src/trident_indexer/__init__.py index 13c86c1..fafd950 100644 --- a/sdk/python/src/trident_indexer/__init__.py +++ b/sdk/python/src/trident_indexer/__init__.py @@ -9,7 +9,7 @@ from .errors import TridentApiError from .retry import DEFAULT_RETRY_CONFIG, RetryConfig from .types import SorobanEvent, PaginatedEvents, Network -from .openapi_models_gen import OpenAPIModels, SorobanEvent as OpenAPISorobanEvent, EventListResponse, HealthResponse, IndexerStatsResponse, ContractStats, ContractStatsResponse, ErrorResponse +from .openapi_models_gen import OpenAPIModels, SorobanEvent as OpenAPISorobanEvent, EventListResponse, LivenessResponse, ReadyResponse, ReadyChecks, IndexerStatsResponse, ContractStats, ContractStatsResponse, ErrorResponse try: __version__ = _version("trident-indexer") @@ -30,7 +30,9 @@ "OpenAPIModels", "OpenAPISorobanEvent", "EventListResponse", - "HealthResponse", + "LivenessResponse", + "ReadyResponse", + "ReadyChecks", "IndexerStatsResponse", "ContractStats", "ContractStatsResponse", diff --git a/sdk/python/src/trident_indexer/openapi_models_gen.py b/sdk/python/src/trident_indexer/openapi_models_gen.py index 1ab3039..976723b 100644 --- a/sdk/python/src/trident_indexer/openapi_models_gen.py +++ b/sdk/python/src/trident_indexer/openapi_models_gen.py @@ -13,11 +13,6 @@ def from_str(x: Any) -> str: return x -def from_int(x: Any) -> int: - assert isinstance(x, int) and not isinstance(x, bool) - return x - - def from_list(f: Callable[[Any], T], x: Any) -> list[T]: assert isinstance(x, list) return [f(y) for y in x] @@ -33,6 +28,16 @@ def to_enum(c: Type[EnumT], x: Any) -> EnumT: return x.value +def from_bool(x: Any) -> bool: + assert isinstance(x, bool) + return x + + +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + return x + + def from_none(x: Any) -> Any: assert x is None return x @@ -47,11 +52,163 @@ def from_union(fs, x): assert False -def from_bool(x: Any) -> bool: - assert isinstance(x, bool) +def from_float(x: Any) -> float: + assert isinstance(x, (float, int)) and not isinstance(x, bool) + return float(x) + + +def to_float(x: Any) -> float: + assert isinstance(x, (int, float)) return x +@dataclass +class ContractEventFieldSchema: + name: str + """Stable field name for this event payload position or property""" + + type: str + """Field type inferred from the contract interface or observed payloads""" + + @staticmethod + def from_dict(obj: Any) -> 'ContractEventFieldSchema': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + type = from_str(obj.get("type")) + return ContractEventFieldSchema(name, type) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["type"] = from_str(self.type) + return result + + +@dataclass +class ContractEventSchema: + event_name: str + """Contract event name (topic_0)""" + + fields: list[ContractEventFieldSchema] + """Named fields for this event payload""" + + @staticmethod + def from_dict(obj: Any) -> 'ContractEventSchema': + assert isinstance(obj, dict) + event_name = from_str(obj.get("event_name")) + fields = from_list(ContractEventFieldSchema.from_dict, obj.get("fields")) + return ContractEventSchema(event_name, fields) + + def to_dict(self) -> dict: + result: dict = {} + result["event_name"] = from_str(self.event_name) + result["fields"] = from_list(lambda x: to_class(ContractEventFieldSchema, x), self.fields) + return result + + +class Network(Enum): + """Network queried""" + + MAINNET = "mainnet" + TESTNET = "testnet" + + +@dataclass +class ContractEventSchemaResponse: + code_hash: str + """Contract code hash for this schema version""" + + contract_id: str + """Soroban contract address""" + + events: list[ContractEventSchema] + """Observed event names and their typed field schemas""" + + network: Network + """Network queried""" + + @staticmethod + def from_dict(obj: Any) -> 'ContractEventSchemaResponse': + assert isinstance(obj, dict) + code_hash = from_str(obj.get("code_hash")) + contract_id = from_str(obj.get("contract_id")) + events = from_list(ContractEventSchema.from_dict, obj.get("events")) + network = Network(obj.get("network")) + return ContractEventSchemaResponse(code_hash, contract_id, events, network) + + def to_dict(self) -> dict: + result: dict = {} + result["code_hash"] = from_str(self.code_hash) + result["contract_id"] = from_str(self.contract_id) + result["events"] = from_list(lambda x: to_class(ContractEventSchema, x), self.events) + result["network"] = to_enum(Network, self.network) + return result + + +@dataclass +class ContractSpecFunction: + name: str + """Exported function name""" + + @staticmethod + def from_dict(obj: Any) -> 'ContractSpecFunction': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + return ContractSpecFunction(name) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + return result + + +@dataclass +class ContractSpecResponse: + code_hash: str + """Deployed WASM code hash this spec was parsed from""" + + contract_id: str + """Soroban contract address""" + + contract_type: str + """Primary classification derived from detected interfaces (e.g. token, nft, custom)""" + + functions: list[ContractSpecFunction] + """Functions captured from the contract's spec""" + + has_spec: bool + """Whether an embedded contractspecv0 section was found""" + + interfaces: list[str] + """Every standard interface detected from the contract's spec functions""" + + network: Network + """Network queried""" + + @staticmethod + def from_dict(obj: Any) -> 'ContractSpecResponse': + assert isinstance(obj, dict) + code_hash = from_str(obj.get("code_hash")) + contract_id = from_str(obj.get("contract_id")) + contract_type = from_str(obj.get("contract_type")) + functions = from_list(ContractSpecFunction.from_dict, obj.get("functions")) + has_spec = from_bool(obj.get("has_spec")) + interfaces = from_list(from_str, obj.get("interfaces")) + network = Network(obj.get("network")) + return ContractSpecResponse(code_hash, contract_id, contract_type, functions, has_spec, interfaces, network) + + def to_dict(self) -> dict: + result: dict = {} + result["code_hash"] = from_str(self.code_hash) + result["contract_id"] = from_str(self.contract_id) + result["contract_type"] = from_str(self.contract_type) + result["functions"] = from_list(lambda x: to_class(ContractSpecFunction, x), self.functions) + result["has_spec"] = from_bool(self.has_spec) + result["interfaces"] = from_list(from_str, self.interfaces) + result["network"] = to_enum(Network, self.network) + return result + + @dataclass class ContractStats: contract_id: str @@ -84,13 +241,6 @@ def to_dict(self) -> dict: return result -class Network(Enum): - """Network queried""" - - MAINNET = "mainnet" - TESTNET = "testnet" - - @dataclass class ContractStatsResponse: contracts: list[ContractStats] @@ -128,6 +278,71 @@ def to_dict(self) -> dict: return result +@dataclass +class ContractStorageValue: + ledger_sequence: int + """Ledger sequence at which this value was observed""" + + observed_at: str + """Timestamp this snapshot row was recorded""" + + storage_key: str + """Base64-encoded XDR LedgerKey this value was read from""" + + key: Any = None + """Human-readable decoded storage key""" + + value: Any = None + """Human-readable decoded value (absent when the entry was removed)""" + + @staticmethod + def from_dict(obj: Any) -> 'ContractStorageValue': + assert isinstance(obj, dict) + ledger_sequence = from_int(obj.get("ledger_sequence")) + observed_at = from_str(obj.get("observed_at")) + storage_key = from_str(obj.get("storage_key")) + key = obj.get("key") + value = obj.get("value") + return ContractStorageValue(ledger_sequence, observed_at, storage_key, key, value) + + def to_dict(self) -> dict: + result: dict = {} + result["ledger_sequence"] = from_int(self.ledger_sequence) + result["observed_at"] = from_str(self.observed_at) + result["storage_key"] = from_str(self.storage_key) + result["key"] = self.key + if self.value is not None: + result["value"] = self.value + return result + + +@dataclass +class ContractStorageResponse: + contract_id: str + """Soroban contract address""" + + network: Network + """Network queried""" + + values: list[ContractStorageValue] + """Storage snapshot values (latest, or full history when queried via /storage/history)""" + + @staticmethod + def from_dict(obj: Any) -> 'ContractStorageResponse': + assert isinstance(obj, dict) + contract_id = from_str(obj.get("contract_id")) + network = Network(obj.get("network")) + values = from_list(ContractStorageValue.from_dict, obj.get("values")) + return ContractStorageResponse(contract_id, network, values) + + def to_dict(self) -> dict: + result: dict = {} + result["contract_id"] = from_str(self.contract_id) + result["network"] = to_enum(Network, self.network) + result["values"] = from_list(lambda x: to_class(ContractStorageValue, x), self.values) + return result + + @dataclass class Error: code: str @@ -270,56 +485,6 @@ def to_dict(self) -> dict: return result -@dataclass -class Indexer: - last_ledger_indexed: int - """Latest indexed ledger sequence""" - - last_poll_at: str | None = None - """Timestamp of last successful indexer poll""" - - @staticmethod - def from_dict(obj: Any) -> 'Indexer': - assert isinstance(obj, dict) - last_ledger_indexed = from_int(obj.get("last_ledger_indexed")) - last_poll_at = from_union([from_str, from_none], obj.get("last_poll_at")) - return Indexer(last_ledger_indexed, last_poll_at) - - def to_dict(self) -> dict: - result: dict = {} - result["last_ledger_indexed"] = from_int(self.last_ledger_indexed) - if self.last_poll_at is not None: - result["last_poll_at"] = from_union([from_str, from_none], self.last_poll_at) - return result - - -class HealthResponseStatus(Enum): - """Overall system status""" - - DEGRADED = "degraded" - OK = "ok" - - -@dataclass -class HealthResponse: - indexer: Indexer - status: HealthResponseStatus - """Overall system status""" - - @staticmethod - def from_dict(obj: Any) -> 'HealthResponse': - assert isinstance(obj, dict) - indexer = Indexer.from_dict(obj.get("indexer")) - status = HealthResponseStatus(obj.get("status")) - return HealthResponse(indexer, status) - - def to_dict(self) -> dict: - result: dict = {} - result["indexer"] = to_class(Indexer, self.indexer) - result["status"] = to_enum(HealthResponseStatus, self.status) - return result - - class IndexerStatsResponseStatus(Enum): """Indexer health status""" @@ -351,6 +516,12 @@ class IndexerStatsResponse: lag_ledgers: int | None = None """Number of ledgers behind chain tip""" + lag_seconds_estimated: float | None = None + """Estimated wall-clock staleness in seconds: lag_ledgers times Stellar's protocol-target + ledger close time (~5s). Null whenever lag_ledgers is null. See + docs/observability/data-freshness.md for the full freshness contract this field is part + of. + """ last_ledger_indexed: int | None = None """Latest indexed ledger sequence""" @@ -367,9 +538,10 @@ def from_dict(obj: Any) -> 'IndexerStatsResponse': events_indexed_total = from_union([from_int, from_none], obj.get("events_indexed_total")) events_last_poll = from_union([from_int, from_none], obj.get("events_last_poll")) lag_ledgers = from_union([from_int, from_none], obj.get("lag_ledgers")) + lag_seconds_estimated = from_union([from_float, from_none], obj.get("lag_seconds_estimated")) last_ledger_indexed = from_union([from_int, from_none], obj.get("last_ledger_indexed")) last_poll_at = from_union([from_str, from_none], obj.get("last_poll_at")) - return IndexerStatsResponse(network, status, avg_poll_duration_ms, chain_tip_ledger, events_indexed_total, events_last_poll, lag_ledgers, last_ledger_indexed, last_poll_at) + return IndexerStatsResponse(network, status, avg_poll_duration_ms, chain_tip_ledger, events_indexed_total, events_last_poll, lag_ledgers, lag_seconds_estimated, last_ledger_indexed, last_poll_at) def to_dict(self) -> dict: result: dict = {} @@ -385,6 +557,8 @@ def to_dict(self) -> dict: result["events_last_poll"] = from_union([from_int, from_none], self.events_last_poll) if self.lag_ledgers is not None: result["lag_ledgers"] = from_union([from_int, from_none], self.lag_ledgers) + if self.lag_seconds_estimated is not None: + result["lag_seconds_estimated"] = from_union([to_float, from_none], self.lag_seconds_estimated) if self.last_ledger_indexed is not None: result["last_ledger_indexed"] = from_union([from_int, from_none], self.last_ledger_indexed) if self.last_poll_at is not None: @@ -392,44 +566,219 @@ def to_dict(self) -> dict: return result +class LivenessResponseStatus(Enum): + """Always "ok" while the process is up — no dependency checks.""" + + OK = "ok" + + +@dataclass +class LivenessResponse: + status: LivenessResponseStatus + """Always "ok" while the process is up — no dependency checks.""" + + @staticmethod + def from_dict(obj: Any) -> 'LivenessResponse': + assert isinstance(obj, dict) + status = LivenessResponseStatus(obj.get("status")) + return LivenessResponse(status) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(LivenessResponseStatus, self.status) + return result + + +@dataclass +class ReadyChecks: + grpc_api: str + """"ok" or "error: \"""" + + postgres: str + """"ok" or "error: \"""" + + redis: str + """"ok" or "error: \"""" + + @staticmethod + def from_dict(obj: Any) -> 'ReadyChecks': + assert isinstance(obj, dict) + grpc_api = from_str(obj.get("grpc_api")) + postgres = from_str(obj.get("postgres")) + redis = from_str(obj.get("redis")) + return ReadyChecks(grpc_api, postgres, redis) + + def to_dict(self) -> dict: + result: dict = {} + result["grpc_api"] = from_str(self.grpc_api) + result["postgres"] = from_str(self.postgres) + result["redis"] = from_str(self.redis) + return result + + +class ReadyResponseStatus(Enum): + """"degraded" when any dependency check in `checks` failed.""" + + DEGRADED = "degraded" + OK = "ok" + + +@dataclass +class ReadyResponse: + checks: ReadyChecks + indexer_lag: int + """Ledgers behind chain tip, from system_state. Null when Postgres is unreachable or the + chain-tip cache hasn't been populated yet. + """ + status: ReadyResponseStatus + """"degraded" when any dependency check in `checks` failed.""" + + @staticmethod + def from_dict(obj: Any) -> 'ReadyResponse': + assert isinstance(obj, dict) + checks = ReadyChecks.from_dict(obj.get("checks")) + indexer_lag = from_int(obj.get("indexer_lag")) + status = ReadyResponseStatus(obj.get("status")) + return ReadyResponse(checks, indexer_lag, status) + + def to_dict(self) -> dict: + result: dict = {} + result["checks"] = to_class(ReadyChecks, self.checks) + result["indexer_lag"] = from_int(self.indexer_lag) + result["status"] = to_enum(ReadyResponseStatus, self.status) + return result + + +@dataclass +class TokenMetadataResponse: + contract_id: str + """Soroban contract address""" + + is_token: bool + """True when the contract was resolved and implements the SEP-41 read interface. False for + both "not yet resolved" and "resolved, not a token". + """ + network: Network + """Network queried""" + + decimals: int | None = None + """Token decimals, from decimals(). Null unless is_token is true.""" + + name: str | None = None + """Token name, from name(). Null unless is_token is true.""" + + resolved_at: str | None = None + """When this contract was last resolved. Null if never resolved.""" + + symbol: str | None = None + """Token symbol, from symbol(). Null unless is_token is true.""" + + @staticmethod + def from_dict(obj: Any) -> 'TokenMetadataResponse': + assert isinstance(obj, dict) + contract_id = from_str(obj.get("contract_id")) + is_token = from_bool(obj.get("is_token")) + network = Network(obj.get("network")) + decimals = from_union([from_int, from_none], obj.get("decimals")) + name = from_union([from_str, from_none], obj.get("name")) + resolved_at = from_union([from_str, from_none], obj.get("resolved_at")) + symbol = from_union([from_str, from_none], obj.get("symbol")) + return TokenMetadataResponse(contract_id, is_token, network, decimals, name, resolved_at, symbol) + + def to_dict(self) -> dict: + result: dict = {} + result["contract_id"] = from_str(self.contract_id) + result["is_token"] = from_bool(self.is_token) + result["network"] = to_enum(Network, self.network) + if self.decimals is not None: + result["decimals"] = from_union([from_int, from_none], self.decimals) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) + if self.resolved_at is not None: + result["resolved_at"] = from_union([from_str, from_none], self.resolved_at) + if self.symbol is not None: + result["symbol"] = from_union([from_str, from_none], self.symbol) + return result + + @dataclass class OpenAPIModels: + contract_event_field_schema: ContractEventFieldSchema | None = None + contract_event_schema: ContractEventSchema | None = None + contract_event_schema_response: ContractEventSchemaResponse | None = None + contract_spec_function: ContractSpecFunction | None = None + contract_spec_response: ContractSpecResponse | None = None contract_stats: ContractStats | None = None contract_stats_response: ContractStatsResponse | None = None + contract_storage_response: ContractStorageResponse | None = None + contract_storage_value: ContractStorageValue | None = None error_response: ErrorResponse | None = None event_list_response: EventListResponse | None = None - health_response: HealthResponse | None = None indexer_stats_response: IndexerStatsResponse | None = None + liveness_response: LivenessResponse | None = None + ready_checks: ReadyChecks | None = None + ready_response: ReadyResponse | None = None soroban_event: SorobanEvent | None = None + token_metadata_response: TokenMetadataResponse | None = None @staticmethod def from_dict(obj: Any) -> 'OpenAPIModels': assert isinstance(obj, dict) + contract_event_field_schema = from_union([ContractEventFieldSchema.from_dict, from_none], obj.get("ContractEventFieldSchema")) + contract_event_schema = from_union([ContractEventSchema.from_dict, from_none], obj.get("ContractEventSchema")) + contract_event_schema_response = from_union([ContractEventSchemaResponse.from_dict, from_none], obj.get("ContractEventSchemaResponse")) + contract_spec_function = from_union([ContractSpecFunction.from_dict, from_none], obj.get("ContractSpecFunction")) + contract_spec_response = from_union([ContractSpecResponse.from_dict, from_none], obj.get("ContractSpecResponse")) contract_stats = from_union([ContractStats.from_dict, from_none], obj.get("ContractStats")) contract_stats_response = from_union([ContractStatsResponse.from_dict, from_none], obj.get("ContractStatsResponse")) + contract_storage_response = from_union([ContractStorageResponse.from_dict, from_none], obj.get("ContractStorageResponse")) + contract_storage_value = from_union([ContractStorageValue.from_dict, from_none], obj.get("ContractStorageValue")) error_response = from_union([ErrorResponse.from_dict, from_none], obj.get("ErrorResponse")) event_list_response = from_union([EventListResponse.from_dict, from_none], obj.get("EventListResponse")) - health_response = from_union([HealthResponse.from_dict, from_none], obj.get("HealthResponse")) indexer_stats_response = from_union([IndexerStatsResponse.from_dict, from_none], obj.get("IndexerStatsResponse")) + liveness_response = from_union([LivenessResponse.from_dict, from_none], obj.get("LivenessResponse")) + ready_checks = from_union([ReadyChecks.from_dict, from_none], obj.get("ReadyChecks")) + ready_response = from_union([ReadyResponse.from_dict, from_none], obj.get("ReadyResponse")) soroban_event = from_union([SorobanEvent.from_dict, from_none], obj.get("SorobanEvent")) - return OpenAPIModels(contract_stats, contract_stats_response, error_response, event_list_response, health_response, indexer_stats_response, soroban_event) + token_metadata_response = from_union([TokenMetadataResponse.from_dict, from_none], obj.get("TokenMetadataResponse")) + return OpenAPIModels(contract_event_field_schema, contract_event_schema, contract_event_schema_response, contract_spec_function, contract_spec_response, contract_stats, contract_stats_response, contract_storage_response, contract_storage_value, error_response, event_list_response, indexer_stats_response, liveness_response, ready_checks, ready_response, soroban_event, token_metadata_response) def to_dict(self) -> dict: result: dict = {} + if self.contract_event_field_schema is not None: + result["ContractEventFieldSchema"] = from_union([lambda x: to_class(ContractEventFieldSchema, x), from_none], self.contract_event_field_schema) + if self.contract_event_schema is not None: + result["ContractEventSchema"] = from_union([lambda x: to_class(ContractEventSchema, x), from_none], self.contract_event_schema) + if self.contract_event_schema_response is not None: + result["ContractEventSchemaResponse"] = from_union([lambda x: to_class(ContractEventSchemaResponse, x), from_none], self.contract_event_schema_response) + if self.contract_spec_function is not None: + result["ContractSpecFunction"] = from_union([lambda x: to_class(ContractSpecFunction, x), from_none], self.contract_spec_function) + if self.contract_spec_response is not None: + result["ContractSpecResponse"] = from_union([lambda x: to_class(ContractSpecResponse, x), from_none], self.contract_spec_response) if self.contract_stats is not None: result["ContractStats"] = from_union([lambda x: to_class(ContractStats, x), from_none], self.contract_stats) if self.contract_stats_response is not None: result["ContractStatsResponse"] = from_union([lambda x: to_class(ContractStatsResponse, x), from_none], self.contract_stats_response) + if self.contract_storage_response is not None: + result["ContractStorageResponse"] = from_union([lambda x: to_class(ContractStorageResponse, x), from_none], self.contract_storage_response) + if self.contract_storage_value is not None: + result["ContractStorageValue"] = from_union([lambda x: to_class(ContractStorageValue, x), from_none], self.contract_storage_value) if self.error_response is not None: result["ErrorResponse"] = from_union([lambda x: to_class(ErrorResponse, x), from_none], self.error_response) if self.event_list_response is not None: result["EventListResponse"] = from_union([lambda x: to_class(EventListResponse, x), from_none], self.event_list_response) - if self.health_response is not None: - result["HealthResponse"] = from_union([lambda x: to_class(HealthResponse, x), from_none], self.health_response) if self.indexer_stats_response is not None: result["IndexerStatsResponse"] = from_union([lambda x: to_class(IndexerStatsResponse, x), from_none], self.indexer_stats_response) + if self.liveness_response is not None: + result["LivenessResponse"] = from_union([lambda x: to_class(LivenessResponse, x), from_none], self.liveness_response) + if self.ready_checks is not None: + result["ReadyChecks"] = from_union([lambda x: to_class(ReadyChecks, x), from_none], self.ready_checks) + if self.ready_response is not None: + result["ReadyResponse"] = from_union([lambda x: to_class(ReadyResponse, x), from_none], self.ready_response) if self.soroban_event is not None: result["SorobanEvent"] = from_union([lambda x: to_class(SorobanEvent, x), from_none], self.soroban_event) + if self.token_metadata_response is not None: + result["TokenMetadataResponse"] = from_union([lambda x: to_class(TokenMetadataResponse, x), from_none], self.token_metadata_response) return result diff --git a/sdk/rust/src/openapi_models_gen.rs b/sdk/rust/src/openapi_models_gen.rs index 743610d..7f9b7fa 100644 --- a/sdk/rust/src/openapi_models_gen.rs +++ b/sdk/rust/src/openapi_models_gen.rs @@ -16,19 +16,112 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct OpenApiModels { + pub contract_event_field_schema: Option, + + pub contract_event_schema: Option, + + pub contract_event_schema_response: Option, + + pub contract_spec_function: Option, + + pub contract_spec_response: Option, + pub contract_stats: Option, pub contract_stats_response: Option, + pub contract_storage_response: Option, + + pub contract_storage_value: Option, + pub error_response: Option, pub event_list_response: Option, - pub health_response: Option, - pub indexer_stats_response: Option, + pub liveness_response: Option, + + pub ready_checks: Option, + + pub ready_response: Option, + pub soroban_event: Option, + + pub token_metadata_response: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractEventFieldSchema { + /// Stable field name for this event payload position or property + pub name: String, + + /// Field type inferred from the contract interface or observed payloads + #[serde(rename = "type")] + pub contract_event_field_schema_type: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractEventSchema { + /// Contract event name (topic_0) + pub event_name: String, + + /// Named fields for this event payload + pub fields: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractEventSchemaResponse { + /// Contract code hash for this schema version + pub code_hash: String, + + /// Soroban contract address + pub contract_id: String, + + /// Observed event names and their typed field schemas + pub events: Vec, + + /// Network queried + pub network: Network, +} + +/// Network queried +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Network { + Mainnet, + + Testnet, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractSpecFunction { + /// Exported function name + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractSpecResponse { + /// Deployed WASM code hash this spec was parsed from + pub code_hash: String, + + /// Soroban contract address + pub contract_id: String, + + /// Primary classification derived from detected interfaces (e.g. token, nft, custom) + pub contract_type: String, + + /// Functions captured from the contract's spec + pub functions: Vec, + + /// Whether an embedded contractspecv0 section was found + pub has_spec: bool, + + /// Every standard interface detected from the contract's spec functions + pub interfaces: Vec, + + /// Network queried + pub network: Network, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -64,13 +157,34 @@ pub struct ContractStatsResponse { pub to_ledger: i64, } -/// Network queried #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Network { - Mainnet, +pub struct ContractStorageResponse { + /// Soroban contract address + pub contract_id: String, - Testnet, + /// Network queried + pub network: Network, + + /// Storage snapshot values (latest, or full history when queried via /storage/history) + pub values: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractStorageValue { + /// Human-readable decoded storage key + pub key: Option, + + /// Ledger sequence at which this value was observed + pub ledger_sequence: i64, + + /// Timestamp this snapshot row was recorded + pub observed_at: String, + + /// Base64-encoded XDR LedgerKey this value was read from + pub storage_key: String, + + /// Human-readable decoded value (absent when the entry was removed) + pub value: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -146,33 +260,6 @@ pub enum EventType { System, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HealthResponse { - pub indexer: Indexer, - - /// Overall system status - pub status: HealthResponseStatus, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Indexer { - /// Latest indexed ledger sequence - pub last_ledger_indexed: i64, - - /// Timestamp of last successful indexer poll - pub last_poll_at: Option, -} - -/// Overall system status -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum HealthResponseStatus { - Degraded, - - #[serde(rename = "ok")] - StatusOk, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexerStatsResponse { /// Average poll duration in milliseconds @@ -190,6 +277,12 @@ pub struct IndexerStatsResponse { /// Number of ledgers behind chain tip pub lag_ledgers: Option, + /// Estimated wall-clock staleness in seconds: lag_ledgers times Stellar's protocol-target + /// ledger close time (~5s). Null whenever lag_ledgers is null. See + /// docs/observability/data-freshness.md for the full freshness contract this field is part + /// of. + pub lag_seconds_estimated: Option, + /// Latest indexed ledger sequence pub last_ledger_indexed: Option, @@ -213,3 +306,76 @@ pub enum IndexerStatsResponseStatus { Stalled, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LivenessResponse { + /// Always "ok" while the process is up — no dependency checks. + pub status: LivenessResponseStatus, +} + +/// Always "ok" while the process is up — no dependency checks. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum LivenessResponseStatus { + #[serde(rename = "ok")] + StatusOk, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReadyChecks { + /// "ok" or "error: " + pub grpc_api: String, + + /// "ok" or "error: " + pub postgres: String, + + /// "ok" or "error: " + pub redis: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReadyResponse { + pub checks: ReadyChecks, + + /// Ledgers behind chain tip, from system_state. Null when Postgres is unreachable or the + /// chain-tip cache hasn't been populated yet. + pub indexer_lag: i64, + + /// "degraded" when any dependency check in `checks` failed. + pub status: ReadyResponseStatus, +} + +/// "degraded" when any dependency check in `checks` failed. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ReadyResponseStatus { + Degraded, + + #[serde(rename = "ok")] + StatusOk, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TokenMetadataResponse { + /// Soroban contract address + pub contract_id: String, + + /// Token decimals, from decimals(). Null unless is_token is true. + pub decimals: Option, + + /// True when the contract was resolved and implements the SEP-41 read interface. False for + /// both "not yet resolved" and "resolved, not a token". + pub is_token: bool, + + /// Token name, from name(). Null unless is_token is true. + pub name: Option, + + /// Network queried + pub network: Network, + + /// When this contract was last resolved. Null if never resolved. + pub resolved_at: Option, + + /// Token symbol, from symbol(). Null unless is_token is true. + pub symbol: Option, +} diff --git a/sdk/typescript/src/api-types.gen.ts b/sdk/typescript/src/api-types.gen.ts index 756635b..9cd6f17 100644 --- a/sdk/typescript/src/api-types.gen.ts +++ b/sdk/typescript/src/api-types.gen.ts @@ -12,8 +12,8 @@ export interface paths { cookie?: never; }; /** - * Health check - * @description Returns indexer health status and last indexed ledger + * Liveness check + * @description Cheap process-liveness check (issue #243) — no dependency calls (no Postgres/Redis/gRPC). Always 200 while the process is up and serving requests. Intended for Kubernetes' liveness probe. For dependency health (Postgres/Redis/gRPC), see GET /v1/ready instead. */ get: operations["getHealth"]; put?: never; @@ -24,6 +24,26 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/ready": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Readiness check + * @description Verifies Postgres, Redis, and the gRPC backend concurrently, each with a 3-second timeout (issue #243). Returns 503 if any dependency check fails. Intended for Kubernetes' readiness probe / Fly's HTTP service check, so a pod with a broken dependency is pulled out of rotation instead of continuing to receive traffic it can't serve. + */ + get: operations["getReady"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/events": { parameters: { query?: never; @@ -369,24 +389,33 @@ export interface components { /** @description Opaque cursor for next page (null if has_more is false) */ next_cursor?: string | null; }; - HealthResponse: { + LivenessResponse: { /** - * @description Overall system status + * @description Always "ok" while the process is up — no dependency checks. + * @enum {string} + */ + status: "ok"; + }; + ReadyChecks: { + /** @description "ok" or "error: " */ + postgres: string; + /** @description "ok" or "error: " */ + redis: string; + /** @description "ok" or "error: " */ + grpc_api: string; + }; + ReadyResponse: { + /** + * @description "degraded" when any dependency check in `checks` failed. * @enum {string} */ status: "ok" | "degraded"; - indexer: { - /** - * Format: int64 - * @description Latest indexed ledger sequence - */ - last_ledger_indexed: number | null; - /** - * Format: date-time - * @description Timestamp of last successful indexer poll - */ - last_poll_at?: string | null; - }; + /** + * Format: int64 + * @description Ledgers behind chain tip, from system_state. Null when Postgres is unreachable or the chain-tip cache hasn't been populated yet. + */ + indexer_lag: number | null; + checks: components["schemas"]["ReadyChecks"]; }; IndexerStatsResponse: { /** @@ -621,9 +650,33 @@ export interface components { "application/json": components["schemas"]["ErrorResponse"]; }; }; - /** @description Service temporarily unavailable */ + /** @description Rate limit exceeded for this API key's tier (error.code RATE_LIMITED). Carries the same X-RateLimit-* headers as a successful response (X-RateLimit-Remaining is 0) plus Retry-After. */ + RateLimitExceeded: { + headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; + "Retry-After": components["headers"]["Retry-After"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Per-IP rate limit exceeded (error.code RATE_LIMITED). Applies to endpoints not covered by per-API-key limiting (public endpoints, or admin endpoints authenticated via ADMIN_API_KEY rather than X-API-Key) — only Retry-After is set, no X-RateLimit-* headers. */ + TooManyRequestsIPOnly: { + headers: { + "Retry-After": components["headers"]["Retry-After"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Service temporarily unavailable — either a dependency (database, Redis, gRPC backend) is down, or the server is shedding load under the global concurrency cap. When load-shedding is the cause, Retry-After is set; otherwise it is absent. */ ServiceUnavailable: { headers: { + "Retry-After": components["headers"]["Retry-After"]; [name: string]: unknown; }; content: { @@ -633,7 +686,33 @@ export interface components { }; parameters: never; requestBodies: never; - headers: never; + headers: { + /** + * @description Requests allowed per window for this API key's rate-limit tier. Present on every response from an endpoint secured by ApiKeyAuth (2xx and 429 alike) once a valid X-API-Key was presented. + * @example 50 + */ + "X-RateLimit-Limit": number; + /** + * @description Requests remaining in the current window for this API key. 0 on the response that triggers a 429. + * @example 12 + */ + "X-RateLimit-Remaining": number; + /** + * @description Unix timestamp (seconds) when the current rate-limit window resets. + * @example 1732900000 + */ + "X-RateLimit-Reset": number; + /** + * @description Seconds to wait before retrying. Present on 429 (rate limit exceeded, per-API-key or per-IP) and on 503 responses caused by the global concurrency cap shedding load. Not present on a 503 caused by an unavailable dependency (database/Redis/gRPC backend) — check the error envelope's `error.code` to distinguish the two. + * @example 1 + */ + "Retry-After": number; + /** + * @description Whether this response was served from the Redis response cache (HIT) or freshly computed (MISS). Only emitted by endpoints that cache their response. + * @example HIT + */ + "X-Cache": "HIT" | "MISS"; + }; pathItems: never; } export type $defs = Record; @@ -647,18 +726,50 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Indexer is healthy or degraded */ + /** @description Process is alive */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["HealthResponse"]; + "application/json": components["schemas"]["LivenessResponse"]; }; }; + 429: components["responses"]["TooManyRequestsIPOnly"]; 503: components["responses"]["ServiceUnavailable"]; }; }; + getReady: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description All dependencies reachable */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ReadyResponse"]; + }; + }; + 429: components["responses"]["TooManyRequestsIPOnly"]; + /** @description Either one or more dependencies is unreachable (ReadyResponse body, status "degraded", the failing entry in checks set to "error: ..."), or the server is shedding load under the global concurrency cap (ErrorResponse body, Retry-After header set) — the same outermost load-shedding behavior every endpoint shares. */ + 503: { + headers: { + "Retry-After": components["headers"]["Retry-After"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ReadyResponse"] | components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; listEvents: { parameters: { query?: { @@ -679,8 +790,8 @@ export interface operations { network?: "testnet" | "mainnet"; /** @description Maximum number of events to return */ limit?: number; - /** @description Opaque pagination cursor from previous response (for next page) */ - after?: string; + /** @description Opaque pagination cursor from previous response's next_cursor (for next page) */ + cursor?: string; }; header?: never; path?: never; @@ -691,6 +802,9 @@ export interface operations { /** @description List of events with pagination metadata */ 200: { headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; [name: string]: unknown; }; content: { @@ -699,6 +813,7 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimitExceeded"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -720,6 +835,9 @@ export interface operations { /** @description Event details */ 200: { headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; [name: string]: unknown; }; content: { @@ -739,6 +857,7 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; + 429: components["responses"]["RateLimitExceeded"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -759,6 +878,9 @@ export interface operations { /** @description Server-Sent Events stream */ 200: { headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; [name: string]: unknown; }; content: { @@ -767,6 +889,7 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimitExceeded"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -792,6 +915,9 @@ export interface operations { */ 200: { headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; [name: string]: unknown; }; content: { @@ -805,6 +931,7 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimitExceeded"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -823,6 +950,9 @@ export interface operations { /** @description Contract event schema registry entry */ 200: { headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; [name: string]: unknown; }; content: { @@ -831,6 +961,7 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimitExceeded"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -849,6 +980,9 @@ export interface operations { /** @description Contract spec and detected interfaces */ 200: { headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; [name: string]: unknown; }; content: { @@ -858,6 +992,7 @@ export interface operations { 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimitExceeded"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -876,6 +1011,9 @@ export interface operations { /** @description Latest known value per storage key */ 200: { headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; [name: string]: unknown; }; content: { @@ -884,6 +1022,7 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimitExceeded"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -905,6 +1044,9 @@ export interface operations { /** @description Recorded changes for the requested storage key */ 200: { headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; [name: string]: unknown; }; content: { @@ -913,6 +1055,7 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimitExceeded"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -934,6 +1077,7 @@ export interface operations { "application/json": components["schemas"]["IndexerStatsResponse"]; }; }; + 429: components["responses"]["TooManyRequestsIPOnly"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -955,9 +1099,13 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Contract activity statistics */ + /** @description Contract activity statistics. X-Cache indicates whether this response was served from the 60s Redis response cache (HIT) or freshly computed (MISS). */ 200: { headers: { + "X-RateLimit-Limit": components["headers"]["X-RateLimit-Limit"]; + "X-RateLimit-Remaining": components["headers"]["X-RateLimit-Remaining"]; + "X-RateLimit-Reset": components["headers"]["X-RateLimit-Reset"]; + "X-Cache": components["headers"]["X-Cache"]; [name: string]: unknown; }; content: { @@ -966,6 +1114,7 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimitExceeded"]; 503: components["responses"]["ServiceUnavailable"]; }; }; @@ -1002,6 +1151,7 @@ export interface operations { }; }; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["TooManyRequestsIPOnly"]; }; }; createApiKey: { @@ -1050,6 +1200,7 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["TooManyRequestsIPOnly"]; }; }; deleteApiKey: { @@ -1081,6 +1232,7 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; + 429: components["responses"]["TooManyRequestsIPOnly"]; }; }; getAdminDbStats: { @@ -1102,6 +1254,7 @@ export interface operations { }; }; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["TooManyRequestsIPOnly"]; }; }; getMetrics: {