Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Stellar Governance Guardians

Read Soroban governance on Stellar without trusting a UI: decode the raw ScVal payloads, see which published risk rule matched, index the events into Postgres, and read delegate behaviour as raw counts plus formulas.

The suite is three public repositories with a strict dependency direction:

soroban-governance-parser  →  governance-event-indexer  →  delegate-portal-dashboard
        (Rust + WASM)                (TypeScript)                (Next.js)
RepositoryWhat it does todayStatus
parserDecodes ScVal → JSON; extracts and parses contractspecv0; classifies risk from a published rule table; decodes Script3 proposals and events; models dry-run output from committed captures; ships a WASM package; exposes sgp simulate for validated transaction envelopes.Script3 adapter implemented. OpenZeppelin adapter planned.
indexerIngests raw governance events from the public testnet RPC into Postgres with cursors, ≤1000-ledger windows, retention-gap recording, fixture replay, and a read-only GraphQL health/raw-event connection.Ingestion and initial GraphQL surface built. Decode, reconciliation and deploy remain planned.
dashboardDecodes a pasted base64 ScVal in the browser with the pinned parser WASM; shows indexer health and the exact interface pins.Scaffold merged: /, /simulate, /status. Data pages are planned.

Who this is for

  • Indexer and dashboard builders who need decoded governance payloads without reimplementing governor logic.
  • Delegate-tooling authors who want risk classifications that expose the rule that fired instead of a composite score.
  • Anyone who needs to read a proposal, a vote or a tally from raw chain data and check the reading themselves.

How this site is built

Everything on this site is either a page in docs-site/src/ or generated from a committed source file by a script in docs-site/scripts/:

PageGenerated from
Risk rulescrates/core/src/risk.rs
CLI referencethe sgp binary’s own --help output
API referenceschemas/governance-v1.graphql
Evidencedeployments.json
Upstream pinsdocs-site/sources.lock

CI rebuilds every generated page and fails on any diff, so a page cannot describe a rule table, an export list, a schema or a contract that the source files do not contain. Prose claims are checked by docs-site/scripts/check-docs-claims.py.

What is not built

The honest limitations page aggregates every gap across the three repositories — including the OpenZeppelin adapter, decoded indexer tables, reconciliation, deployment, and the dashboard’s planned data pages. Nothing on this site describes a planned feature as if it existed; planned pages carry a Status: planned banner.

License

MIT, in all three repositories.

Architecture

Three repositories, one dependency direction, and no shared runtime state beyond pinned artifacts.

flowchart LR
  subgraph chain["Stellar testnet"]
    RPC["Soroban RPC"]
  end

  subgraph parser["soroban-governance-parser (Rust)"]
    CORE["core: ScVal, spec, risk,
GovernorAdapter, simulate"] WASM["wasm: wasm-bindgen surface"] CLI["cli: sgp (network IO)"] CORE --> WASM CORE --> CLI end subgraph indexer["governance-event-indexer (TypeScript)"] ING["ingest: windows, cursors,
gaps, replay"] DEC["decode: pinned WASM"] GQL["GraphQL: governance-v1"] ING --> DEC --> GQL end subgraph dashboard["delegate-portal-dashboard (Next.js)"] SIM["/simulate: browser decode"] PAGES["pages: indexer GraphQL"] end RPC -->|getEvents| ING CLI -->|getHealth, getEvents,
simulateTransaction| RPC WASM -->|.tgz release asset| DEC WASM -->|.tgz release asset| SIM GQL -->|HTTP| PAGES SDL["governance-v1.graphql"] --> GQL SDL --> PAGES

Data flow

  1. Chain → parser. The parser decodes raw ScVal values, extracts contractspecv0 from a contract WASM, and applies the published risk rule table. Governor-specific decoding lives behind the GovernorAdapter trait; the Script3 adapter is implemented.
  2. Chain → indexer. The indexer polls the public RPC’s getEvents in windows of at most 1000 ledgers, stores raw events keyed by (ledger, tx_hash, op_index, event_index), and advances one cursor per registered contract in the same transaction. Events older than the RPC’s retention window are recorded in ingest_gaps rather than skipped.
  3. Indexer → dashboard. The dashboard reads the indexer’s GraphQL endpoint from INDEXER_URL and validates every field of every response against the vendored SDL. There is no bundled sample dataset and no cached fallback.
  4. Parser → both consumers. The WASM package and the SDL travel as content-pinned artifacts, never as source imports.

The pin model

A pin is a content hash, not a version range. Each consumer vendors the bytes it depends on and verifies them before use:

ConsumerArtifactPin fileChecked by
indexerparser WASM release assetparser.lockthe indexer’s pin check (offline, every PR)
indexergovernance-v1.graphqlschema.locksame
indexerdeployments.jsonregistry.locksame
dashboardparser WASM release assetparser.locknpm run check:pins (offline, every PR)
dashboardgovernance-v1.graphqlschema.locksame
this sitesibling docs it summarisesdocs-site/sources.lockdocs-site/scripts/check-docs-claims.py

Bumping a pin is a PR that shows the diff and re-runs the contract tests. A pin never points at an asset that does not exist yet: that rule is why the indexer’s decode milestone waits on a real parser release instead of a planned one.

Why the boundaries sit there

  • Governor logic is parser-only. Adapters (matches_event, normalize_event, decode_call) live behind GovernorAdapter. An indexer or dashboard that reimplemented them would drift from the parser’s golden tests.
  • The parser core does no IO. crates/core is sans-IO; network calls live in the CLI (crates/cli/src/rpc.rs) and in the indexer’s RPC client, so decode logic is testable offline.
  • Consumers validate, they do not coerce. The dashboard rejects a response whose fields do not match the SDL instead of filling in defaults.
  • Fail closed, fail loud. Missing spec, unknown function, unreadable ledger range, absent INDEXER_URL, broken pin: each becomes an explicit error or UNVERIFIED value that a reader can see.

Crates and workspaces

PathRoleIO
crates/coreScVal → JSON, WASM custom-section extraction, contractspecv0, risk rules, GovernorAdapter, power/tally replicas, dry-run modelnone
crates/wasmwasm-bindgen surface over core for the browser and Nodenone
crates/clisgp operator toolSoroban JSON-RPC
governance-event-indexer/srcingestion, durable event decode and GraphQLPostgres, Soroban RPC
delegate-portal-dashboard/approutes; /simulate runs in the browserHTTP to the indexer

Quickstart

Each repository runs on its own and has its own gate. Nothing here needs a sibling checkout: cross-repo inputs are pinned artifacts.

RepositoryToolchainTime to a green checkStart here
parserRust 1.96.0, Node 24 for WASM testsa few minutes for a cold buildParser
indexerNode ≥ 24, Postgres 16a few minutesIndexer
dashboardNode ≥ 24, Chrome for e2ea few minutesDashboard

The PR gate in every repository is offline and deterministic. Live-testnet checks run in a nightly or manual workflow and never block a merge.

Parser quickstart

Rust workspace: crates/core (sans-IO decode), crates/wasm (wasm-bindgen), crates/cli (sgp).

Prerequisites

  • Rust 1.96.0 — pinned by rust-toolchain.toml, so rustup picks it up.
  • wasm32-unknown-unknown target for the WASM crate: rustup target add wasm32-unknown-unknown.
  • wasm-pack 0.13.1 for packaging the browser/Node builds.

Build, test, check

git clone https://github.com/Stellar-Governance-Guardians/soroban-governance-parser
cd soroban-governance-parser

cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings   # pedantic, unwrap/expect/panic denied
cargo test --workspace                                  # 85 tests
OFFLINE=1 bash scripts/check-claims.sh                  # 21 pass, 0 fail, 2 skipped (network)
python3 scripts/check-deployments.py                    # registry vs committed fixtures

All of the above run in the PR gate, offline. The two skipped claims need the public testnet and run in the nightly live-checks workflow.

Decode something

# A captured `proposal_created` event topic (symbol ScVal), no network needed
cargo run -p soroban-governance-cli --bin sgp -- decode-scval \
  AAAADwAAABBwcm9wb3NhbF9jcmVhdGVk

The full command list, including ttl, is on the CLI reference.

Package the WASM

wasm-pack build crates/wasm --target nodejs --out-dir pkg/nodejs --out-name sgp_parser_wasm
wasm-pack build crates/wasm --target web    --out-dir pkg/web    --out-name sgp_parser_wasm
node --test crates/wasm/js-tests/node.test.mjs
node crates/wasm/js-tests/browser.test.mjs

The exported surface is listed on WASM usage. The current release is v0.1.0-alpha.2 with sha256 65193e43642f55abd083e2283aa5ed3caee5839c40a74c7b3f30398ea3ec0629; the release lock file is releases/parser-wasm.lock.json.

What is not wired

  • sgp simulate accepts a validated transaction envelope and emits an estimate; it does not assemble proposal actions or estimate rent. See Dry run.
  • The OpenZeppelin adapter is not implemented. See OpenZeppelin adapter.

Indexer quickstart

Node ≥ 24, Postgres 16, and the vendored pinned artifacts (committed).

Run the checks

git clone https://github.com/Stellar-Governance-Guardians/governance-event-indexer
cd governance-event-indexer
npm ci

docker compose up -d postgres
export DATABASE_URL='postgres://indexer:indexer-local-only@localhost:5432/indexer'

npm run check-pins        # sha256 of the vendored WASM, SDL and registry
npm run typecheck
npm run lint
npm run test:unit         # 48 tests, no database, no network
npm run test:integration  # 11 tests against Postgres
npm run build

The integration suite creates a throwaway database next to DATABASE_URL and drops it, so “migrations from empty” is exercised every run. Without DATABASE_URL it fails with an explicit message instead of skipping.

Apply migrations and ingest

node dist/cli.js migrate
node dist/cli.js ingest     # polls getEvents in ≤1000-ledger windows
node dist/cli.js replay <dir>   # replays committed raw responses, source='fixture-replay'

What exists and what does not

Built: ingestion (I2) — registry, cursors, windowed getEvents, idempotent raw storage, retention-gap recording, fixture replay, durable parser-backed event decode, and an initial GraphQL health/raw-event connection.

Not built: proposal/impact/delegate projections, reconciliation (I5), deploy (I6) and the clean-clone prove script (I7). The API reference describes the pinned schema and the implemented health/event surface.

The remaining decode milestone depends on persisted tables and replay semantics, not on a missing parser export; see Honest limitations.

Pins

PinArtifact
parser.lockWASM release asset v0.1.0-alpha.2 (sha256 65193e43…)
schema.lockgovernance-v1.graphql @ parser ba3bf1a3… (sha256 cfbd85a8…)
registry.lockdeployments.json @ the same parser commit

The PR gate verifies all three offline; the nightly nightly-pins workflow checks that the pinned URLs still resolve to identical bytes.

Dashboard quickstart

Next.js 16 App Router, strict TypeScript, Tailwind v4. Node ≥ 24.

Run it

git clone https://github.com/Stellar-Governance-Guardians/delegate-portal-dashboard
cd delegate-portal-dashboard
npm ci
npm run dev     # http://localhost:3000

The dashboard’s predev/prebuild/pretest:unit hooks run its unpack step (unpack-wasm.mjs), which verifies the vendored parser WASM tarball against parser.lock and unpacks it into generated/wasm/. No network access is needed.

To show indexer data, point it at a GraphQL endpoint:

INDEXER_URL=http://127.0.0.1:4000/graphql npm run dev

INDEXER_URL has no default. Unset, malformed or unreachable, every data surface renders the exact reason instead of an empty table.

Checks

npm run check:pins     # vendored WASM + SDL vs the lock files
npm run typecheck
npm run lint
npm run test:unit      # 25 unit/component tests, real pinned WASM, no mock decode
npm run build
npx playwright test    # 4 e2e tests against `next build && next start`
bash scripts/check-claims.sh   # 7/7 README claims

The e2e run deliberately leaves INDEXER_URL unset, so it proves the unavailable-indexer state rather than a happy path that needs a live server.

Routes

RouteBehaviour
/Product summary, indexer health, explicit list of what is not built
/simulateDecodes a pasted base64 XDR ScVal in the browser with the pinned WASM
/statusThe exact interface pins and indexerHealth, or the exact reason it is unavailable

Proposal, delegate and impact pages are planned; they need the indexer’s decoded tables.

Parser

crates/core decodes Soroban governance data without IO. crates/wasm exposes it to JavaScript. crates/cli is the operator tool, and network calls live there (crates/cli/src/rpc.rs), not in the core.

What it decodes

InputOutput
base64 XDR ScValJSON, total over the stellar-xdr 28 type set
contract WASMcontractspecv0 parsed into functions, structs, unions, enums, errors and events
raw eventa NormalizedEvent through a governor adapter, or unverified
proposal calla DecodedCall with named arguments when the spec is verified
proposal votestally and power reconstructed through pure replicas
simulateTransaction responsean ExecutionImpact estimate with simulatedAtLedger

What it refuses to do

  • Guess an argument name without a contract spec: the result is decoding: "unverified", not a plausible label.
  • Downgrade an unknown function to low: it is UNVERIFIED with the reason.
  • Coerce a value into JSON: integers at 64 bits and above become decimal strings, and non-representable maps are errors.
  • Emit a dry-run without a ledger: simulatedAtLedger is required, and every impact carries estimate: true.

Design rules

  • Adapters own governor knowledge. The GovernorAdapter trait (crates/core/src/adapter.rs) is where a governor’s events, calls and state are understood. See the adapter model and the authoring guide.
  • Rules are data. Risk tiers come from the published table in crates/core/src/risk.rs; every classification exposes the matched rule. See Risk rules.
  • The core does no IO. Network access is confined to crates/cli/src/rpc.rs.

Pages

Decoding rules

crates/core/src/scval.rs converts an ScVal to JSON. The mapping is total over the stellar-xdr 28 type set and preserves value exactly; it does not approximate.

Value mapping

ScVal kindJSON
Voidnull
Booltrue / false
U32, I32number
U64, I64, Timepoint, Duration{ "_type": "u64", "value": "<decimal>" } (and i64, timepoint, duration)
U128, I128, U256, I256{ "_type": "i128", "value": "<decimal>" } (and u128, u256, i256)
Bytes{ "_type": "bytes", "value": "<base64>" }
String, Symbolstring
Address (account){ "_type": "address:account", "value": "G…" }
Address (contract){ "_type": "address:contract", "value": "C…" }
Vecarray
Map with string or symbol keysobject
Map with any other key typeerror, not a stringified key
Error{ "_type": "error", "value": "contract:5" }
LedgerKeyContractInstance{ "_type": "ledger_key_contract_instance", "value": null }
LedgerKeyNonce{ "_type": "ledger_key_nonce", "value": "<nonce>" }

Two rules matter in practice:

  1. Wide integers keep their width and value as a string. A 128-bit token amount cannot round-trip through a JavaScript number, so it stays a tagged object with a decimal string ({"_type":"i128","value":"…"}) all the way to the page. Consumers that need arithmetic parse it explicitly.
  2. Non-representable maps are errors. Stringifying a struct key would silently change the value’s identity, so the decoder refuses.

Argument decoding

Positional ScVal arguments are zipped with the input names declared by the contract’s own contractspecv0 section:

  • names verified against the spec ⇒ decoding: "VERIFIED", arguments as a named object;
  • spec missing or governor unrecognized ⇒ decoding: "UNVERIFIED", arguments as a positional array;
  • arity mismatch ⇒ explicit error.

governor: "unverified" is a literal value in the output, never a guess at which governor produced a call.

Risk classification

Every decoded call carries a RiskClassification:

{
  "tier": "CRITICAL",
  "matchedRule": "transfer",
  "verified": true
}
  • The tier comes from an exact, case-sensitive match on the verified function name; the generated rule table is the whole list.
  • No match ⇒ tier: "UNVERIFIED" with matchedRule: "no published rule matched this function name".
  • A function name that could not be verified against a spec is always UNVERIFIED, even if its text matches a rule.
  • Contextual rules (treasury_outflow, self_call, large_value, batched_actions) add evidence from the arguments; they never change the function-name tier and never collapse into a composite score.

Tally and power

Votes are not summed ad hoc. Two pure replicas reproduce the pinned Script3 behaviour:

  • crates/core/src/checkpoint.rs — checkpointed voting power, so a vote counts the power held at the proposal’s snapshot ledger, not at vote time;
  • crates/core/src/tally.rs — tally and quorum arithmetic with the contract’s rounding.

Both are covered by property tests (zero votes, exact ties, i128 extremes, empty checkpoint history) and by differential tests against committed on-chain reads.

Adapter model

A governor is understood by exactly one module behind the GovernorAdapter trait (crates/core/src/adapter.rs). Nothing outside the adapter needs to know the governor exists — that is what keeps governor logic out of TypeScript.

The trait

MethodResponsibility
name()stable adapter id, e.g. script3-soroban-governor
matches_event(event)is this event ours? contract id + topic symbol + exact arity; false on any doubt
normalize_event(event)decode topics and data into a NormalizedEvent; Err on an unexpected shape
decode_call(call, spec)decode a proposal action into a DecodedCall; the spec supplies argument names

Governor-specific helpers — state and tally reconstruction, action enums, contextual rule projection — live in the same module.

Rules that are not optional

  1. Fail closed. Unknown variant ⇒ a preserved Unknown and an Err, never a plausible guess. Missing field ⇒ Err. Absent spec ⇒ decoding: "unverified".
  2. Exact matching. matches_event demands the contract id, the topic symbol and the exact topic arity. A superset match would decode another governor’s events under the wrong rules.
  3. No governor logic elsewhere. Reimplementing proposal state, tally or action variants in the indexer or the dashboard duplicates the parser’s golden tests and drifts from them. The consumers call the pinned WASM.
  4. Unknown is data. A ProposalAction variant the adapter does not know is preserved with its raw payload, not dropped and not mapped to a known one.

Implementation status

AdapterStatusWhere
Script3 (script3-soroban-governor)implementedcrates/core/src/adapters/script3.rs
OpenZeppelinnot implemented; fixtures and research committeddocs/adapters/openzeppelin.md

To add a governor, follow the authoring guide.

Status: implemented and differentially tested against committed on-chain reads. Provenance for every fixture is in tests/fixtures/README.md.

Adapter notes: Script3 soroban-governor

Status: implemented in crates/core/src/adapters/script3.rs (Script3Adapter), verified offline against committed live-testnet captures by crates/core/tests/differential.rs. Everything below was read from the pinned upstream source; the source-to-behavior mapping table at the bottom says which file each decoded shape comes from. Repo lives at github.com/script3/soroban-governor (org script3, NOT script3-io).

Version verified

  • Latest release v1.1.1 (2024-07-09), built with Soroban CLI 21.0.0; workspace pins soroban-sdk = "20.5.0". Source: https://api.github.com/repos/script3/soroban-governor/releases , https://github.com/script3/soroban-governor/blob/main/Cargo.toml

Architecture verified

  • Two contracts: Governor (contracts/governor, crate soroban-governor) and Votes/Voter (contracts/votes, crate soroban-votes). No separate timelock contract — timelock is a Governor setting. Treasury = the Governor contract itself.
  • Voter has feature-flag variants: bonding (default, wraps a Stellar Asset 1:1), admin variant (mint/clawback), sep-0041 variant. Source: https://github.com/script3/soroban-governor/blob/main/docs/architecture.md

Governor functions verified (contracts/governor/src/contract.rs)

  • initialize(votes: Address, council: Address, settings: GovernorSettings)
  • settings() -> GovernorSettings, council() -> Address, vote_token() -> Address
  • propose(creator: Address, title: String, description: String, action: ProposalAction) -> u32
  • get_proposal(proposal_id: u32) -> Option<Proposal>
  • close(proposal_id: u32) — computes eta; there is NO queue function
  • execute(proposal_id: u32), cancel(from: Address, proposal_id: u32)
  • vote(voter: Address, proposal_id: u32, support: u32) (0=Against, 1=For, 2=Abstain)
  • get_vote(voter, proposal_id) -> Option<u32>, get_proposal_votes(proposal_id) -> Option<VoteCount>

Types verified (contracts/governor/src/types.rs)

  • Proposal { id: u32, config: ProposalConfig, data: ProposalData }
  • ProposalConfig { title: String, description: String, action: ProposalAction }
  • ProposalData { creator: Address, vote_start: u32, vote_end: u32, eta: u32, status: ProposalStatus, executable: bool }
  • ProposalStatus: Open=0, Successful=1, Defeated=2, Expired=3, Executed=4, Canceled=5
  • VoteCount { against: i128, _for: i128, abstain: i128 }
  • GovernorSettings { proposal_threshold: i128, vote_delay: u32, vote_period: u32, timelock: u32, grace_period: u32, quorum: u32 (BPS), counting_type: u32 (bitmask), vote_threshold: u32 (BPS) }
  • Proposal calldata: ProposalAction enum = Calldata(Calldata) | Upgrade(BytesN<32>) | Settings(GovernorSettings) | Council(Address) | Snapshot; Calldata { contract_id: Address, function: Symbol, args: Vec<Val>, auths: Vec<Calldata> } (recursive; nested auths become InvokerContractAuthEntry sub-invocations at execute time).

Events verified (contracts/governor/src/events.rs)

topic[0]topics[1..]data
proposal_createdproposal_id: u32, proposer: Addresstitle, description, action, vote_start, vote_end
proposal_canceledproposal_id: u32()
proposal_voting_closedproposal_id: u32, status: u32, eta: u32VoteCount
proposal_executedproposal_id: u32()
proposal_expiredproposal_id: u32()
vote_castproposal_id: u32, voter: Addresssupport: u32, amount: i128

Votes contract events verified (contracts/votes/src/events.rs)

topic[0]topics[1..]data
delegatedelegator, delegateeold_delegatee
votes_changeddelegateold_votes: i128, new_votes: i128
set_adminadminnew_admin
deposit/withdraw/claim (bonding only)accountamount: i128
set_emissionseps: u64, expiration: u64()

Votes checkpoints: packed u128 = (sequence: u32) << 96 | amount (u96); per-user current checkpoint in persistent storage, history Vec<u128> in temporary storage; upper_lookup binary search. Functions: get_votes, get_past_votes(user, sequence), get_delegate, delegate(account, delegatee) (no chaining), total_supply, get_past_total_supply. Source: https://github.com/script3/soroban-governor/tree/main/contracts/votes/src

Published deployments (MAINNET only; from Script3’s governance explorer config)

DAOGovernorVotes
YieldBloxCANSYFVMIP7JVYEZQ463Y2I2VLEVNLDJJ4QNZTDBGLOOGKURPTW4A6FQCAZ2U7Z2LS3N72NKMOR34HOV4WFDCMA2TPWD3KU4EA2VKZARFPMLF3LI
Stellar Community FundCD5BO2F56Q7WK37SWASI7J5J46H37ZWEERJWZFJLPQLP6Y6I76MQZCZOCDHUZKXBNMAHTK62HDS6AVIZANVOABKRGY3SP3EUKYE634RFQ5NPG
Soroban DomainsCDXTT5PBPZ3ERZCC5S6NE2M3NMUNDKL4SJLT26DD3CYUWUWSUV5QPZRWCC4Y5WAI5MF3HF77BAM4YOS3Y4NFZAW3FTRFS25GRLCRBGPDWPYN6VZ3

Source: JS config of https://mainnet.governance.script3.io (linked from https://script3.io).

Source-to-behavior mapping (adapter implementation)

Adapter: crates/core/src/adapters/script3.rs, pinned SHA a2ac6de81055be5bd13e31f922c9546309bfdb8a (same commit as scripts/upstream/upstream.lock.json). Each decoded shape maps to one file:

Adapter code pathUpstream fileWhat it decodes
EVENT_ARITY + matches_eventcontracts/governor/src/events.rstopic[0] symbol + exact topic arity per event
normalize_event proposal_createdcontracts/governor/src/events.rstopics [symbol, id u32, proposer]; data [title, desc, action, vote_start, vote_end]
normalize_event vote_castcontracts/governor/src/events.rstopics [symbol, id u32, voter]; data [support u32, amount i128]
normalize_event proposal_voting_closedcontracts/governor/src/events.rstopics [symbol, id u32, status u32, eta u32]; data VoteCount map
decode_action (5 variants + fail-closed Unknown)contracts/governor/src/types.rs`ProposalAction = Calldata
decode_calldatacontracts/governor/src/types.rsCalldata { contract_id, function, args, auths } (recursive)
decode_settingscontracts/governor/src/types.rsGovernorSettings (all 9 fields required)
decode_vote_countcontracts/governor/src/types.rsVoteCount { against, _for, abstain }
state_from_chaincontracts/governor/src/types.rsProposal { id, config, data } layout from get_proposal
crates/core/src/tally.rscontracts/governor/src/vote_count.rs, constants.rsadd_vote, count_quorum, is_over_quorum, is_over_threshold (strict >, floored)
crates/core/src/checkpoint.rscontracts/votes/src/checkpoints.rspacked u128 checkpoints, upper_lookup floor semantics
AMOUNT_POSITIONScontracts/votes/src/contract.rstransfer(from,to,amount) → amount at args[2]
VotePowerModelcontracts/governor/src/contract.rs:268power = get_past_votes(voter, vote_start) at snapshot

Every row is asserted against committed raw captures in crates/core/tests/differential.rs (ids 0-5, offline).

Vote-weight decay: no decay term found (source reading)

contracts/votes/src/ in the pinned Script3 source contains no decay term — power is a stored checkpoint read (get_past_votes → upper_lookup), never a function of elapsed time. The OpenZeppelin stack (packages/governance/src/votes/) is the same shape. Found by full-text search of both trees at their pinned SHAs on 2026-10-08; this is a source reading, not a live-verified claim. The adapter implements no decay because neither governor’s vote accounting contains one.

UNVERIFIED / honest limitations

  • No official testnet deployment IDs found. The repo Makefile ID is a localhost Standalone placeholder. For testnet proof we must deploy via scripts/seed-testnet.*.
  • Event shapes above are from source reading; not yet observed on live testnet by us. Adapter implementation must re-verify against emitted events from our seeded testnet deployment before marking anything verified at runtime.
  • Whether these exact event shapes are emitted by mainnet deployments at v1.1.1: UNVERIFIED (we have not queried mainnet).

WASM adapter bindings

The WASM package exposes the adapter through sans-IO JSON bindings. All base64 values below are individual XDR-encoded ScVal values; malformed JSON, XDR, unknown event shapes, and missing contract specs return an explicit error or an unverified result. The caller must select this adapter from its own registry; the parser never guesses an adapter from an event alone.

ExportInputsOutput and failure behavior
identify_governor()none"script3-soroban-governor"
matches_event(contractId, ledger, txHash, topicsJson, value)Complete topic vector as a JSON array of base64 XDR plus valueBoolean structural match; malformed input is an error and a non-matching contract/topic shape is false
normalize_event(contractId, ledger, txHash, topicsJson, value)Same event inputsJSON NormalizedEvent with verified payload, or an explicit normalization error
decode_call(contractId, functionName, argsJson, specJson?)JSON array of argument XDR and optional ContractSpec JSONJSON DecodedCall; without a usable spec, arguments remain positional and decoding is unverified

The complete topic vector is required so arity and topic positions are checked; passing just topic[0] is not sufficient for a match.

Status: planned — not implemented. No OpenZeppelin proposal is decoded by the parser today. The commitments below are the shipped test contracts and the research that a future adapter will use; the adapter itself does not exist, so the indexer cannot decode OpenZeppelin governance events yet.

Adapter notes: OpenZeppelin stellar-contracts Governor

Status: researched from source, adapter NOT yet implemented (phase 2 deliverable). Everything below was read from the repository/docs on 2026-10-05. Repo: github.com/OpenZeppelin/stellar-contracts (created 2024-12-11 under this name; no former name found — claims of a rename are UNVERIFIED/not applicable).

Version verified

  • Latest stable v0.7.2 (2026-06-09); latest overall v0.8.0-rc.3 (2026-06-16), whose release notes state it is “not yet been audited, and not ready for use in production”.
  • Governance (Governor/Votes/Timelock) first shipped in v0.7.0 (2026-04-03).
  • Workspace pins soroban-sdk = "28.0.0".
  • README describes the project as preliminary/“as-is” software. Sources: https://api.github.com/repos/OpenZeppelin/stellar-contracts/releases , https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/main/Cargo.toml , https://raw.githubusercontent.com/OpenZeppelin/stellar-contracts/main/README.md

Governor module verified (packages/governance/src/governor/mod.rs, storage.rs)

  • voting_delay() / voting_period() are in ledgers (DAY_IN_LEDGERS = 17280).
  • counting_mode() -> Symbol returns "simple".
  • proposal_threshold() -> u128, quorum(ledger: u32) -> u128 (checkpoint-based), get_token_contract() -> Address, has_voted(proposal_id: BytesN<32>, account) -> bool.
  • propose(targets: Vec<Address>, functions: Vec<Symbol>, args: Vec<Vec<Val>>, description: String, proposer: Address) -> BytesN<32>
  • cast_vote(proposal_id: BytesN<32>, vote_type: u32, reason: String, voter: Address) -> u128 (0=Against, 1=For, 2=Abstain)
  • queue(targets, functions, args, description_hash, eta: u32, operator) -> BytesN<32>; execute(...) / cancel(...) have no default impl (implementation required); proposals_need_queuing() -> bool defaults to false.
  • get_proposal_id(...) = keccak256 of XDR-serialized targets/functions/args/description_hash.

State machine verified

  • ProposalState: Pending=0, Active=1, Defeated=2, Canceled=3, Succeeded=4, Queued=5, Expired=6, Executed=7. Pending/Active/Defeated are time-derived; others stored.
  • CRITICAL FACT: proposal calldata is NOT stored on-chain. Only ProposalCore { proposer: Address, vote_snapshot: u32, vote_end: u32, state: ProposalState } is stored. Callers re-supply targets/functions/args at queue/execute time; the keccak256 proposal_id binds them. Consequence for our product: for OZ governors, the indexer must capture calldata from proposal_created event data (which does carry it) — the contract state alone can never show it.

Events verified (governor/mod.rs #[contractevent] structs)

topic[0]topics[1..]data
proposal_createdproposal_id: BytesN<32>, proposer: Addresstargets, functions, args, vote_snapshot: u32, vote_end: u32, description: String
vote_castvoter: Address, proposal_idvote_type: u32, weight: u128, reason: String
proposal_queuedproposal_ideta: u32
proposal_executedproposal_id—
proposal_cancelledproposal_id—
quorum_changed—old_quorum: u128, new_quorum: u128

Note topic ORDER differs from Script3 for vote_cast (voter before proposal_id).

Votes module verified (packages/governance/src/votes/)

  • get_votes(account) -> u128, get_votes_at_checkpoint(account, ledger: u32) -> u128 (error FutureLookup=4100 if ledger >= current), get_total_supply(), get_total_supply_at_checkpoint(ledger), get_delegate(account) -> Option<Address>, delegate(account, delegatee). Only delegated power counts; no undelegate.
  • Checkpoints: Checkpoint { ledger: u32, votes: u128 }; keys Delegatee(Address), NumCheckpoints(Address), DelegateCheckpoint(Address,u32), VotingUnits(Address).
  • Events: delegate_changed topics [.., delegator] data [from: Option<Address>, to: Address]; delegate_votes_changed topics [.., delegate] data [previous: u128, new: u128].

Timelock verified (packages/governance/src/timelock/)

  • Governor queueing: with proposals_need_queuing()=true, queue moves Succeeded→Queued, emits proposal_queued. eta is informational — not enforced by the governor itself.
  • Timelock trait: get_min_delay() -> u32 (ledgers), hash_operation(target, function, args, predecessor, salt), schedule/execute/cancel/update_delay (no default impls; access control left to implementer).
  • Events: operation_scheduled [id, target] data [function, args, predecessor, salt, delay]; operation_executed [id, target]; operation_cancelled [id]; min_delay_changed.
  • Example integration: examples/fungible-governor-timelock/governor/src/governor.rs.

Deployments

  • UNVERIFIED / none found for governance. No testnet/mainnet governor contract IDs in repo or docs. Only multisig example IDs exist (not governance). For testnet proof we must build and deploy the example governor ourselves via scripts/seed-testnet.*.

UNVERIFIED / honest limitations

  • Whether the v0.7.0 audit covered the governance packages: UNVERIFIED (audit PDF not read).
  • Event shapes are from source reading; not yet observed on live testnet by us.
  • v0.8.0-rc API may change shapes; adapter targets v0.7.x line.

Risk rules

Generated from crates/core/src/risk.rs by docs-site/scripts/gen-risk-rules.py. Do not edit by hand: CI regenerates this page and fails on any diff.

Classification is an exact, case-sensitive match on the verified function name. A function name that matches no rule is UNVERIFIED — never downgraded to LOW. Every classification exposes the rule that matched, so no tier is an opaque score.

The table holds 31 rules across 4 tiers.

Critical — 11 rules

Rules that can move value or change contract code/ownership.

Function nameWhy it is this tier
transfermoves token value
transfer_frommoves token value on behalf of another account
mintcreates new token supply
burndestroys token supply
upgradereplaces contract WASM
update_wasm_hashreplaces contract WASM
set_adminchanges contract administrator
change_adminchanges contract administrator
transfer_ownershipchanges contract owner
renounce_ownershipremoves contract owner
executeexecutes a queued governance payload; effect depends on payload

High — 8 rules

Rules that change governance parameters or state.

Function nameWhy it is this tier
queueschedules a passed proposal for execution
cancelcancels a proposal
set_voting_delaychanges governance timing
set_voting_periodchanges governance timing
set_quorumchanges quorum requirement
set_proposal_thresholdchanges who may propose
update_configchanges contract configuration
set_configchanges contract configuration

Medium — 5 rules

Rules with observable but bounded effects.

Function nameWhy it is this tier
proposecreates a proposal; effect deferred to execution
delegatemoves voting power between accounts
votecasts a vote
cast_votecasts a vote
approvegrants a token allowance

Low — 7 rules

Read-only or informational actions.

Function nameWhy it is this tier
get_proposalread-only accessor
get_votesread-only accessor
balanceread-only accessor
decimalsread-only accessor
nameread-only accessor
symbolread-only accessor
versionread-only accessor

Contextual rules

The rules above match on the function name. These additional rules evaluate the decoded arguments and emit every rule that fired, each with its evidence:

  • batched_actions
  • large_value
  • self_call
  • treasury_outflow

Contextual rules never change the function-name tier; they add evidence next to it. See docs-site/src/parser/decoding-rules.md for what a caller receives.

CLI reference

Generated from the sgp binary (crates/cli) by docs-site/scripts/gen-cli.py. Do not edit by hand: CI regenerates this page and fails on any diff. Binary version: sgp 0.1.0.

sgp is the operator tool. Network IO lives in crates/cli/src/rpc.rs; the core crate stays sans-IO.

Top-level

Soroban governance payload parser (fail-closed)

Usage: sgp <COMMAND>

Commands:
  decode-scval    Decode a base64 XDR ScVal into JSON. Offline, pure
  spec-from-wasm  Extract and parse the contractspecv0 section from a WASM file. Offline
  fetch-spec      Fetch a contract's WASM from a Soroban RPC and parse its contractspecv0
  health          Print RPC health: latest ledger, oldest ledger, retention window
  ttl             Read a contract's instance/code TTL entries (protocol 29) and check remaining ledgers. Fails closed: missing TTL entries are errors
  simulate        Simulate a fully assembled TransactionEnvelope. The result is always labeled as an estimate and can optionally be captured verbatim
  help            Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

sgp decode-scval

Decode a base64 XDR ScVal into JSON. Offline, pure

Decode a base64 XDR ScVal into JSON. Offline, pure

Usage: sgp decode-scval <XDR_BASE64>

Arguments:
  <XDR_BASE64>  base64 XDR of an ScVal (or "-" to read stdin)

Options:
  -h, --help  Print help

sgp spec-from-wasm

Extract and parse the contractspecv0 section from a WASM file. Offline

Extract and parse the contractspecv0 section from a WASM file. Offline

Usage: sgp spec-from-wasm <WASM>

Arguments:
  <WASM>  path to contract .wasm

Options:
  -h, --help  Print help

sgp fetch-spec

Fetch a contract’s WASM from a Soroban RPC and parse its contractspecv0

Fetch a contract's WASM from a Soroban RPC and parse its contractspecv0

Usage: sgp fetch-spec [OPTIONS] --contract <CONTRACT>

Options:
      --contract <CONTRACT>  contract id (strkey C...)
      --rpc <RPC>            Soroban RPC URL (default: SDF public testnet) [default: https://soroban-testnet.stellar.org]
  -h, --help                 Print help

sgp health

Print RPC health: latest ledger, oldest ledger, retention window

Print RPC health: latest ledger, oldest ledger, retention window

Usage: sgp health [OPTIONS]

Options:
      --rpc <RPC>  [default: https://soroban-testnet.stellar.org]
  -h, --help       Print help

sgp ttl

Read a contract’s instance/code TTL entries (protocol 29) and check remaining ledgers. Fails closed: missing TTL entries are errors

Read a contract's instance/code TTL entries (protocol 29) and check remaining ledgers. Fails closed: missing TTL entries are errors

Usage: sgp ttl [OPTIONS] --contract <CONTRACT>

Options:
      --contract <CONTRACT>            contract id (strkey C...)
      --rpc <RPC>                      [default: https://soroban-testnet.stellar.org]
      --min-remaining <MIN_REMAINING>  exit 1 if remaining ledgers fall below this threshold
  -h, --help                           Print help

sgp simulate

Simulate a fully assembled TransactionEnvelope. The result is always labeled as an estimate and can optionally be captured verbatim

Simulate a fully assembled TransactionEnvelope. The result is always labeled as an estimate and can optionally be captured verbatim

Usage: sgp simulate [OPTIONS] <TRANSACTION>

Arguments:
  <TRANSACTION>  base64 XDR TransactionEnvelope (or "-" to read stdin)

Options:
      --rpc <RPC>          [default: https://soroban-testnet.stellar.org]
      --capture <CAPTURE>  write the raw JSON-RPC response to this path
  -h, --help               Print help

WASM usage

crates/wasm exposes the core crate to JavaScript through wasm-bindgen. The package is published as a GitHub pre-release asset, nodejs and web targets in one tarball, and consumed by content pin rather than by version range.

Current release

FieldValue
Version0.1.0-alpha.2
Assetsgg-parser-wasm-0.1.0-alpha.2.tgz
sha25665193e43642f55abd083e2283aa5ed3caee5839c40a74c7b3f30398ea3ec0629
Bytes283,043
Targetsnodejs, web
Schemav1
Toolchainrustc 1.96.0 / wasm-pack 0.13.1

releases/parser-wasm.lock.json is the committed source of this table, and the PR gate checks that the README, the lock file and the asset agree.

Exports

ExportPurpose
decode_scval_base64(xdr_base64)ScVal → JSON string
parse_contract_spec(wasm_bytes)extract and parse contractspecv0
classify_risk(function_name, verified)apply the published rule table
script3_decode_proposal(...)decode a Script3 proposal into actions
script3_state_from_chain(...)reconstruct proposal state from chain reads
script3_tally_from_chain(...)reconstruct the tally from chain reads
evaluate_risk_context(call_json, risk_context_json)contextual rules over decoded arguments
evaluate_tally(...)tally and quorum arithmetic
identify_governor()identify the compiled governor adapter
matches_event(...)complete-topic structural event matching
normalize_event(...)verified event normalization with explicit failures
decode_call(...)spec-aware call decoding, or positional unverified output

Event matching and normalization require the complete topic vector and the caller-supplied registry context; an unregistered contract or malformed shape returns an explicit failure or an unverified fallback. Call decoding without a usable contract spec remains positional and unverified.

Node

const wasm = require('./pkg/nodejs/sgp_parser_wasm.js');

const decoded = JSON.parse(wasm.decode_scval_base64('AAAADwAAABBwcm9wb3NhbF9jcmVhdGVk'));
// → "proposal_created"

Browser

import init, { decode_scval_base64 } from './pkg/web/sgp_parser_wasm.js';

await init();
const decoded = JSON.parse(decode_scval_base64('AAAAAwAAAAE='));
// → 1

The dashboard loads the web target from a hash-verified vendored tarball. Its Content-Security-Policy allows script-src 'self' 'wasm-unsafe-eval', which the WASM instantiation requires.

Tests over the artifact

Both targets are tested against the built package, not against a mock:

  • crates/wasm/js-tests/node.test.mjs — Node test runner;
  • crates/wasm/js-tests/browser.test.mjs — headless Chromium;
  • the dashboard’s Vitest suite loads the same artifact through its nodejs target, and its Playwright suite decodes in a real browser.

Versioning

Consumers pin the asset by sha256. The simulation request/response surface and the OpenZeppelin adapter remain separate future work.

Status: model and CLI boundary implemented. The sans-IO parser models committed real simulateTransaction captures and the offline tests exercise it. sgp simulate accepts a fully assembled transaction envelope; no page in the dashboard or indexer renders an impact today.

Dry-run (simulateTransaction) modeling

Sans-IO parser side of the dry-run feature. Code: crates/core/src/simulate.rs; offline tests: crates/core/tests/simulate.rs (committed real captures).

Charter rule 5

Every dry-run output is an estimate. ExecutionImpact.estimate is always true, and simulated_at_ledger is mandatory — a response without latestLedger is an error, never a default. Simulation runs against the current ledger, not the future execution ledger; timelocks and state changes between now and execution can change the real result.

Modeled fields

Modeled against the committed raw captures under tests/fixtures/seed-v2/rpc/*-simulateTransaction-*.json. The RPC response.result object carries exactly:

RPC fieldExecutionImpact fieldnotes
latestLedgersimulated_at_ledgeralways present; required
minResourceFeemin_resource_fee_stroopsstring; absent ⇒ null
stateChangesstate_changespresent only when the call writes; else null
results(presence only)presence ⇒ success; XDR not decoded here ⇒ decode_status: unverified
cost.cpuInsnscpu_instructionsnot present in any capture ⇒ always null
cost.memBytesmemory_bytesnot present in any capture ⇒ always null

Absent means null; nothing is defaulted to zero or to “success”.

Rent

Not modeled. The captured responses contain no rent figure, and the Soroban rent model depends on entry TTLs and ledger timing the simulation response does not expose directly. Rather than derive a number from an assumption, the field is omitted and this limitation recorded. A future capture that includes a rent figure (or a documented derivation from stateChanges durability + TTL) can add it.

IO split

build_simulate_request(transaction_xdr_base64, request_id) returns the JSON-RPC 2.0 request body and validates the envelope (fail-closed). Assembling the transaction from a decoded action, and sending it, is IO and lives in the CLI’s RpcClient; the core crate stays sans-IO.

The CLI exposes the same boundary without guessing how to assemble a proposal:

sgp simulate <transaction-envelope-base64> --capture raw-simulate-response.json
cat transaction-envelope-base64.txt | sgp simulate -

The command validates the envelope before making the RPC call, prints only the parsed ExecutionImpact, and writes the raw response only when --capture is provided. The output remains an estimate and is not a guarantee of future execution.

Not yet done

  • The CLI accepts a fully assembled transaction envelope; it does not assemble proposal actions it cannot verify.
  • decode_status is unverified: the impact parser does not decode the result XDR against a contract spec.

Indexer

governance-event-indexer moves governance events from the public Stellar testnet RPC into Postgres — raw events are preserved and parser-backed event decodes are stored durably — and serves an initial read-only GraphQL API for governance-v1 schema.

Status: ingestion, durable event decode, the initial GraphQL surface, the deploy recipe (I6) and the clean-clone prove script (I7) are built. Proposal, vote and delegate projections and reconciliation (I5) remain planned. The GraphQL endpoint serves health and normalized event records; proposal, impact and delegate fields remain unavailable until their tables are implemented.

What is built

Registry and pins

  • The pinned deployments.json from the parser repo, hash-verified on every load; GOVERNOR_REGISTRY_OVERRIDE adds contract ids at runtime from validated strkeys, failing closed on anything malformed.
  • Three pins verified offline in every PR: parser.lock (WASM asset), schema.lock (SDL) and registry.lock (deployments). A nightly workflow checks that the pinned URLs still resolve to identical bytes.

Ingestion

  • Windows. getEvents in ranges of at most 1000 ledgers, because the public RPC times out on wider ranges; INGEST_WINDOW_LEDGERS is rejected above 1000.
  • Cursors. One persisted cursor per registered contract, initialized at the start of the RPC’s retention window.
  • Storage. Raw events land in raw_events, keyed (ledger, tx_hash, op_index, event_index), inserted idempotently. A window commits together with its cursor advance, so a killed process re-fetches and converges.
  • Retention gaps. A cursor behind the RPC’s oldest ledger writes the unreadable range to ingest_gaps with reason retention and advances past it in the same transaction — loud, never a silent skip.
  • Replay. node dist/cli.js replay <dir> runs committed raw responses through the same raw-plus-decode path, tagging rows source='fixture-replay'. Live rows are source='live', and a later write never flips a row’s source.

Storage

Five tables exist after migrate (forward-only, tested from an empty database):

TableHolds
raw_eventsevery ingested event, keyed (ledger, tx_hash, op_index, event_index), with source = live or fixture-replay
decoded_eventsparser-versioned normalized topics/payload and explicit VERIFIED or UNVERIFIED status for each raw event
ingest_cursorsone cursor per registered contract, advanced in the same transaction as its window
ingest_gapsunreadable ledger ranges with the reason (retention), written instead of skipping silently
indexer_metaindexer bookkeeping (schema version and run metadata)

Proposal-level tables (proposals, proposal_actions, risk_flags, votes, power_checkpoints, delegations, delegates, reconciliation_runs) arrive with I5 and later. Event-level decode is persisted in decoded_events.

Retry behaviour

Bounded exponential backoff with jitter on RPC failures. There is no unbounded retry loop: a failure that exceeds the bound surfaces as an error, and the cursor does not advance past data that was not stored.

What is not built

MilestoneState
I3 decode through the pinned Script3 adapterevent-level decode persisted through the pinned WASM; proposal projections remain planned
I4 GraphQL (Yoga) implementing the vendored SDL exactlyinitial health and event connection implemented; proposal/impact/delegate fields remain planned
I5 reconciliation of SQL tallies against on-chain readsnot built; drift checks remain planned
I6 Docker/compose deploy reciperecipe and deploy.yml shipped; no instance has been deployed
I7 clean-clone prove scriptbuilt; asserts GraphQL values against the committed fixture replay, never yet run against a live indexer

Decoding governor events in TypeScript would put governor logic outside the parser’s GovernorAdapter and break the project’s rule that governor logic lives in the parser. The event-level boundary now uses the pinned parser artifact; remaining work is the durable projection of proposal entities.

Checks

CommandWhat it proves
npm run check-pinsvendored bytes match all three locks, offline
npm run typecheck, npm run lintstrict TypeScript and ESLint pass
npm run test:unit59 tests, no database, no network
npm run test:integration18 tests: migrations from empty, windowing, idempotency, restart safety, gaps, replay

Provenance

The indexer consumes the parser through pins: the SDL and registry at parser commit ba3bf1a3…, the WASM asset v0.1.0-alpha.2. Its README pin table is the authoritative list; this page summarises it, and the revisions this summary was written against are recorded in docs-site/sources.lock.

API reference (governance-v1)

Generated from schemas/governance-v1.graphql by docs-site/scripts/gen-api.py. Do not edit by hand; CI regenerates it and fails on any diff. SDL sha256: cfbd85a82ee834ac14723cd5dd73ce18a5aba0313a91a0aaf6053d1a486ae275.

This SDL is the shared interface contract. The indexer serves it; the dashboard validates every response field against it. Bumping it requires a schema.lock update in each consumer repo, and the parser’s PR gate refuses non-additive changes to the v1 schema.

Server status: not served yet. The indexer currently stops at raw event ingestion (I2); the GraphQL server (I4) is planned. Response examples on this page are the schema, not captured responses.

schema

Root operations: query: Query.

enum DecodingStatus

ValueDescription
VERIFIEDNames verified against the contract’s contractspecv0 section.
UNVERIFIEDRaw ScVal passthrough; spec unavailable or governor unrecognized.

enum RiskTier

ValueDescription
CRITICAL
HIGH
MEDIUM
LOW
UNVERIFIED

type RiskClassification

FieldTypeArgumentsDescription
tierRiskTier!
matchedRuleString!The exact published rule that matched, or the exact reason for UNVERIFIED. Classifications are data, not verdicts — the rule is always exposed.
verifiedBoolean!

type DecodedCall

FieldTypeArgumentsDescription
contractIdString!
functionNameString!
argumentsJSON!JSON: named object when decoding=VERIFIED, positional array when UNVERIFIED.
decodingDecodingStatus!
riskRiskClassification!

type DecodedProposal

FieldTypeArgumentsDescription
governorString!Adapter id, or the literal ‘unverified’. Never guessed.
proposalIdString!
proposerString
calls[DecodedCall!]!
decodingDecodingStatus!

type NormalizedEvent

FieldTypeArgumentsDescription
governorString!
topics[String!]!
ledgerInt!
txHashString!
contractIdString!
payloadJSON!
decodingDecodingStatus!

type ExecutionImpact

Dry-run simulation output. ALWAYS an estimate (charter rule 5): it runs against the current ledger, not the future execution ledger. Timelocks and state changes between now and execution can alter the real result.

FieldTypeArgumentsDescription
estimateBoolean!
simulatedAtLedgerInt!
cpuInstructionsString
memoryBytesString
minResourceFeeStroopsString
stateChangesJSONnull when the RPC returned no state changes; absence is meaningful.
decodeStatusDecodingStatus!

type DelegateMetrics

Delegate metric: raw counts + published formula. No composite scores.

FieldTypeArgumentsDescription
delegateString!
votesCastInt!Number of governance votes cast by this delegate that we indexed.
proposalsEligibleInt!Number of proposals that were open while this delegate held voting power.
participationRatioFloatvotesCast / proposalsEligible. Inputs are exposed above; formula published in docs. A ratio, not a verdict.
delegatedPowerString!

scalar JSON

Opaque scalar serialized as JSON.

type Query

FieldTypeArgumentsDescription
eventsEventConnection!contractId: String
fromLedger: Int
toLedger: Int
first: Int = 50
after: String
Governance events indexed from live testnet, oldest first.
proposalDecodedProposalproposalId: String!
governorContract: String!
executionImpactExecutionImpact!proposalId: String!
governorContract: String!
Dry-run impact estimate for a proposal. Every field is labeled estimate.
delegateMetricsDelegateMetrics!delegate: String!
indexerHealthIndexerHealth!Indexer health: cursor position, RPC retention window, lag. Fail-loud: a stale or broken indexer surfaces here, never as an empty result set.

type IndexerHealth

FieldTypeArgumentsDescription
cursorLedgerInt!Last ledger the indexer successfully processed.
latestLedgerInt!
oldestLedgerInt!
ledgerRetentionWindowInt!Retention window of the RPC in use, as reported by getHealth.
lagLedgersInt!
healthyBoolean!

type EventConnection

FieldTypeArgumentsDescription
edges[EventEdge!]!
pageInfoPageInfo!

type EventEdge

FieldTypeArgumentsDescription
cursorString!
nodeNormalizedEvent!

type PageInfo

FieldTypeArgumentsDescription
hasNextPageBoolean!
endCursorString

Full SDL

# Soroban Governance — GraphQL SDL v1
# Single source of truth shared by governance-event-indexer (server) and
# delegate-portal-dashboard (client). Field names mirror schema-v1.json.
# Bumping this version requires a schema.lock update in every consumer repo.

schema {
  query: Query
}

enum DecodingStatus {
  """Names verified against the contract's contractspecv0 section."""
  VERIFIED
  """Raw ScVal passthrough; spec unavailable or governor unrecognized."""
  UNVERIFIED
}

enum RiskTier {
  CRITICAL
  HIGH
  MEDIUM
  LOW
  UNVERIFIED
}

type RiskClassification {
  tier: RiskTier!
  """The exact published rule that matched, or the exact reason for UNVERIFIED.
  Classifications are data, not verdicts — the rule is always exposed."""
  matchedRule: String!
  verified: Boolean!
}

type DecodedCall {
  contractId: String!
  functionName: String!
  """JSON: named object when decoding=VERIFIED, positional array when UNVERIFIED."""
  arguments: JSON!
  decoding: DecodingStatus!
  risk: RiskClassification!
}

type DecodedProposal {
  """Adapter id, or the literal 'unverified'. Never guessed."""
  governor: String!
  proposalId: String!
  proposer: String
  calls: [DecodedCall!]!
  decoding: DecodingStatus!
}

type NormalizedEvent {
  governor: String!
  topics: [String!]!
  ledger: Int!
  txHash: String!
  contractId: String!
  payload: JSON!
  decoding: DecodingStatus!
}

"""Dry-run simulation output. ALWAYS an estimate (charter rule 5): it runs
against the current ledger, not the future execution ledger. Timelocks and
state changes between now and execution can alter the real result."""
type ExecutionImpact {
  estimate: Boolean!
  simulatedAtLedger: Int!
  cpuInstructions: String
  memoryBytes: String
  minResourceFeeStroops: String
  """null when the RPC returned no state changes; absence is meaningful."""
  stateChanges: JSON
  decodeStatus: DecodingStatus!
}

"""Delegate metric: raw counts + published formula. No composite scores."""
type DelegateMetrics {
  delegate: String!
  """Number of governance votes cast by this delegate that we indexed."""
  votesCast: Int!
  """Number of proposals that were open while this delegate held voting power."""
  proposalsEligible: Int!
  """votesCast / proposalsEligible. Inputs are exposed above; formula published
  in docs. A ratio, not a verdict."""
  participationRatio: Float
  delegatedPower: String!
}

scalar JSON

type Query {
  """Governance events indexed from live testnet, oldest first."""
  events(
    contractId: String
    fromLedger: Int
    toLedger: Int
    first: Int = 50
    after: String
  ): EventConnection!

  proposal(proposalId: String!, governorContract: String!): DecodedProposal

  """Dry-run impact estimate for a proposal. Every field is labeled estimate."""
  executionImpact(proposalId: String!, governorContract: String!): ExecutionImpact!

  delegateMetrics(delegate: String!): DelegateMetrics!

  """Indexer health: cursor position, RPC retention window, lag. Fail-loud:
  a stale or broken indexer surfaces here, never as an empty result set."""
  indexerHealth: IndexerHealth!
}

type IndexerHealth {
  """Last ledger the indexer successfully processed."""
  cursorLedger: Int!
  latestLedger: Int!
  oldestLedger: Int!
  """Retention window of the RPC in use, as reported by getHealth."""
  ledgerRetentionWindow: Int!
  lagLedgers: Int!
  healthy: Boolean!
}

type EventConnection {
  edges: [EventEdge!]!
  pageInfo: PageInfo!
}

type EventEdge {
  cursor: String!
  node: NormalizedEvent!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

Dashboard

delegate-portal-dashboard is a read-only Next.js application: no wallet, no signing, no keys, no server-side secrets.

Status: scaffold merged. Two working surfaces; proposal, delegate and impact pages are planned. They need the indexer’s decoded tables, which do not exist yet.

Routes

RouteBehaviourDepends on
/Product summary, indexer health, explicit list of what is not builtINDEXER_URL
/simulateDecodes a pasted base64 XDR ScVal in the browser with the pinned parser WASM; validates charset, 64 KiB cap and base64 framing before decodingparser WASM
/statusThe exact interface pins and indexerHealth, or the exact reason it is unavailableINDEXER_URL

Error and staleness states

  • No fallback data. INDEXER_URL has no default. Unset, malformed, credential-bearing, unreachable, non-JSON and schema-mismatched endpoints each render a specific reason.
  • Strict response validation. lib/graphql.ts checks every field against the vendored SDL: integers must be safe non-negative integers, healthy must be a boolean, GraphQL errors are surfaced verbatim. Nothing is coerced or defaulted.
  • No cached health. Health is requested per request (dynamic = 'force-dynamic'), so a stopped indexer cannot read as healthy.
  • No empty-but-fine tables. Planned data pages will show an error state rather than an empty list that reads as “no data”.

Decode path

/simulate loads the web build of the pinned WASM from a vendored, sha256-verified tarball. The same artifact, through its nodejs target, is the decoder used by the Vitest suite — there is no mock decode path. Playwright exercises the decode in real Chrome against the production build.

Interface pins

ArtifactPin
Parser WASMv0.1.0-alpha.2, sha256 65193e43…
GraphQL SDLparser commit ba3bf1a3…, sha256 cfbd85a8…

Checks

CommandResult recorded in the PR gate
npm run check:pinsvendored WASM and SDL match both locks
npm run typecheck, npm run lintstrict TypeScript, ESLint with the no-fixture-imports rule
npm run test:unit25 unit/component tests, real pinned WASM
npm run buildproduction build
npx playwright test4 e2e tests with INDEXER_URL deliberately unset
bash scripts/check-claims.sh7/7 README claims

Planned

  • /proposal/[id], /delegates, /delegate/[address], execution impact with an ESTIMATE at ledger N badge.
  • graphql-codegen from the vendored SDL, replacing the hand-written query.
  • Event XDR decode, transaction-hash lookup and risk classification on /simulate, once the parser exposes those exports.
  • A global staleness banner and visible fixture-replay vs live distinction.
  • Lighthouse accessibility measurement, claimed after it is measured.

Metrics

Status: planned. No metric below is served today: the indexer has no GraphQL API (I4). The governance-v1 SDL already exposes votesCast, proposalsEligible, participationRatio and delegatedPower on DelegateMetrics; the remaining definitions are the published contract the API must implement.

Delegate behaviour is reported as raw counts plus a published formula. There is no composite score and no ranking. Every ratio below names its numerator and its denominator, and both inputs are returned next to it so a reader can recompute the number.

Eligible proposals

Definition. Proposals that were open for voting while the delegate held voting power greater than zero at the proposal’s snapshot ledger.

  • Numerator/denominator: this is a count, not a ratio. It is the denominator of participation.
  • Snapshot rule: power is read at the proposal’s voting-start ledger from checkpointed power, not at the delegate’s current balance.
  • Exclusions: proposals whose voting window closed before the indexer’s cursor started (unreadable retention range) are not counted, and the open gap is reported on the health surface.

Participation

Definition. Of the proposals the delegate was eligible for, the share on which they cast any vote.

participation = votesCast / proposalsEligible
  • Numerator: votesCast — governance votes cast by the delegate that the indexer decoded, one per proposal.
  • Denominator: proposalsEligible — as defined above.
  • proposalsEligible = 0 ⇒ participationRatio is null, not 0. A zero denominator is not “no participation”.

Outcome alignment

Definition. Of the proposals the delegate voted on, the share where their vote side matches the proposal’s final outcome.

outcome_alignment = votesWithWinningOutcome / votesCast
  • Numerator: votes whose side (for / against / abstain) equals the recorded final outcome.
  • Denominator: votesCast.
  • Ties and cancelled proposals: excluded from both numerator and denominator, and counted in a separate excludedProposals figure.
  • This is a descriptive ratio about past votes. It is not a prediction and not a quality score.

Power-majority alignment

Definition. Of the proposals the delegate voted on, the share where their side matches the side that held the majority of voting power.

power_majority_alignment = votesWithPowerMajority / votesCast
  • Numerator: votes whose side equals the side with the greater total voting power at close.
  • Denominator: votesCast.
  • Excluded: proposals with no votes, exact power ties, and proposals whose tally could not be reconstructed (recorded as unverified).

Abstain rate

Definition. Of the delegate’s votes, the share that abstained.

abstain_rate = abstainVotes / votesCast
  • Numerator: votes with side abstain.
  • Denominator: votesCast.

Median vote latency

Definition. The median number of ledgers between a proposal becoming eligible and the delegate’s vote, over the proposals they voted on.

latency_i = voteLedger_i - votingStartLedger_i
median_vote_latency = median(latency_i)
  • Unit: ledgers, reported as a median. The count of samples (votesCast) is returned with it; a median over a small sample is reported as such, not hidden.
  • Votes cast in the same ledger as voting start have latency 0.
  • No votes ⇒ null, not 0.

Data rules

  • Numbers are rendered exactly as the API returned them. Nothing is rounded away and nothing is combined into a single score.
  • Rows produced by fixture replay carry source='fixture-replay'; live rows carry source='live'. No API response mixes them silently.
  • If a denominator is zero, the ratio is null. A missing input is never rendered as zero.

Evidence

Generated from deployments.json by docs-site/scripts/gen-evidence.py (sha256 b8c71c34bd5bdd64183b6dae39bdbcd0641a5d8d7084cc751925f4e7e14a3a6d). Do not edit by hand. The registry is checked against the committed fixtures and upstream pins by scripts/check-deployments.py in the PR gate; this page renders it.

Every contract id below was deployed on live testnet with the recorded transaction hash, or captured from the public source named in tests/fixtures/README.md. There are no mock ids.

Network: testnet

FieldValue
RPChttps://soroban-testnet.stellar.org
PassphraseTest SDF Network ; September 2015
RPC retention window120,960 ledgers
Retention verified2026-10-05
Retention evidencetests/fixtures/phase1-proof.json

Contracts (8)

KeyContract idPurposeDeployed at ledgerDeploy tx
fixtureGovernorCDJWPKSQ4NA67PKT…Real WASM-backed governance-shaped contract for parser proof. NOT a production governor.5,035,901a37b875db6c6…
seedV2Script3.script3VotesCCAUJK6V6GIYQANK…votes (checkpointed voting power) for the Script3 governor5,083,6069896582ef9b4…
seedV2Script3.script3GovernorCCKGJBCBBYNWCMSB…the pinned Script3 governor5,083,6089508d28ca1ae…
seedV2Script3.script3MockSubcallCAQCXFI6YSXWGCB3…Script3-side out-of-surface target used by the s4/o4 unknown-contract-call proposals5,083,61045395fede4b9…
seedV2OpenZeppelin.ozTokenCA77UOHZIPLJKPRV…the pinned OpenZeppelin votes/fungible token (holds voting power)5,083,612026fd6edeeea…
seedV2OpenZeppelin.ozGovernorCAONSV2R2YCNJS6D…the pinned OpenZeppelin governor5,083,614fed6be425422…
seedV2OpenZeppelin.ozUpgradeableV1CC3SMQABLUGPGUD5…upgrade target for the s2/o2 contract-upgrade proposals5,083,616468c69a101d9…
seedV2OpenZeppelin.ozMockSubcallCAXHKR66BEHSTAX4…OpenZeppelin-side out-of-surface target used by the s4/o4 unknown-contract-call proposals5,083,618aec4e10d1de6…

WASM hashes

KeyWASM sha256BytesUpload tx
fixtureGovernora54dd248e58f0fa945c718ac3badfa03dbaa1642d2a08dc409caeaf3d3555b382,9515b6fc833ed21…
seedV2Script3.script3Votese8bbc883b2c7fcd8716b07271929eca801fb40f2d3cf5964b139fa741c40f78724,926not recorded
seedV2Script3.script3Governor16033b38b740b1d5af82025a671490b3cea2a0df59b7ffad005f3f7ca3fa27f039,581not recorded
seedV2Script3.script3MockSubcall670215400fffd3d0f6303000d18dfd5bab0e2b1a7b26219e4a8a3bce08eab7637,652not recorded
seedV2OpenZeppelin.ozTokenb5363f54a65e4134f838c03ae9b90718cd7913be5bb3b446aa731e72c7d97e4b23,856not recorded
seedV2OpenZeppelin.ozGovernor77a0400e446947330f8b80f3a57f078d375a793e7aacf3674f5289bacb66edf726,096not recorded
seedV2OpenZeppelin.ozUpgradeableV11018bca637343ad206e568dd78a89f0b2a2f768477947be2546682113216c24815,640not recorded
seedV2OpenZeppelin.ozMockSubcall670215400fffd3d0f6303000d18dfd5bab0e2b1a7b26219e4a8a3bce08eab7637,652not recorded

Fixtures and proofs

The raw RPC responses these contracts were deployed with are committed under tests/fixtures/; tests/fixtures/README.md records the provenance of every capture, and tests/fixtures/phase1-proof.json is the recorded live-testnet proof that the phase 1 claims rest on. python3 scripts/check-deployments.py re-checks this registry against those fixtures and the upstream pins in the PR gate.

Totals

  • Registered contracts: 8
  • Networks: 1

The contract ids above are testnet deployments. Testnet contracts can be removed, archived or lose their TTL entries; sgp ttl --contract <id> checks the live remaining ledgers and fails closed when entries are missing.

Honest limitations

Everything on this page is a gap, not a claim. If a feature appears elsewhere on the site, it is either implemented and checked, or it carries a Status: planned banner. Where a gap blocks a milestone, the blocker is named.

Parser

LimitationDetail
OpenZeppelin adapter not implementedResearch, committed test contracts and fixtures exist; no adapter decodes OZ proposals or events. claims.json asserts this state (oz-adapter-not-implemented).
WASM event surfacematches_event, normalize_event and decode_call are exported in v0.1.0-alpha.2; the indexer still needs persisted decode tables.
CLI simulation boundarysgp simulate accepts a validated transaction envelope and emits an estimate; proposal-action assembly and rent remain out of scope.
Rent is not modelledThe committed simulateTransaction captures contain no rent figure; the field is omitted rather than estimated from an assumption.
Dry-run results are estimatesestimate: true and simulatedAtLedger are mandatory. A simulation runs against the current ledger, not the future execution ledger.
No mainnet DAO exercisedFixtures come from testnet deployments listed in Evidence.
TLS advisories fixed by patch updatecargo audit reported six advisories in rustls 0.23.45, rustls-webpki 0.103.15 and webpki-root-certs 1.0.9. Updated to 0.23.46, 0.103.16 and 1.0.11; the nightly audit job passes. One unsound-API warning (tokio, RUSTSEC-2026-0386) remains a warning with no reported patched release.
Expired live fixtures need the deployer keySix seed-v2 contracts deployed on 2026-10-08 answer with liveUntilLedger: 0. The live tier reports each one as a warning with the re-seed command; refreshing them needs the funded deployer account, which CI does not hold. The phase-1 fixture governor and the seed-v2 governor remain live.

Indexer

LimitationDetail
No proposal-level decodeEvent normalization is persisted through the pinned WASM; proposal/action projections remain planned.
GraphQL surface scope/graphql exposes health and a raw-event connection; proposal, impact, and delegate fields remain unimplemented.
Health surface is narrow/health fails on open ingestion gaps, but reconciliation drift checks arrive with I5.
No reconciliationSQL tallies have not been compared against on-chain reads (I5).
No deploy, no soakThe Docker/compose recipe and deploy.yml (I6) ship, but no instance has been deployed and no 72-hour run has happened.
Indexer prove is fixture-onlyThe indexer’s clean-clone proof (I7) replays committed fixtures and asserts the GraphQL values; it has not been run against a live indexer or a live ledger.
Integration tests need Postgresnpm test fails loudly without DATABASE_URL instead of skipping; CI provides a service container.
Testnet resetsThe public testnet is reset periodically; pinned artifacts and contract ids can become unreachable. Health and gap reporting are designed to make that visible.

Dashboard

LimitationDetail
Data pages not built/proposal/[id], /delegates, /delegate/[address] and execution-impact panels need the indexer’s decoded tables.
No codegenThe single query is hand-written and strictly validated; graphql-codegen waits for the full schema to be served.
/simulate decodes one ScValEvent XDR, transaction-hash lookup, proposal call decode and risk flags are not wired into the dashboard yet.
No live-indexer runNo page has been exercised end to end against a deployed indexer; the local GraphQL contract now exists.
Accessibility checked structurally, not measuredLandmarks, skip link, labelled controls and an aria-live region are in place; no Lighthouse score has been measured and none is claimed.
CSP uses 'unsafe-inline'Required for Next’s hydration bootstrap today; a nonce-based policy is planned.

This site

LimitationDetail
Sibling prose is summarised, not embeddedThe indexer and dashboard pages are parser-repo summaries written against pinned revisions recorded in sources.lock. They are not the sibling repositories’ own documentation, and they will lag until the pin is bumped each phase.
External links checked nightlyThe PR gate builds and link-checks offline; live URL checks (including the deployed site) run in the nightly tier and never block a merge.
No translationThe site is English.

Adding a governor adapter

A new governor is one module behind the GovernorAdapter trait (crates/core/src/adapter.rs). Nothing outside the adapter should need to know the governor exists — that is the whole point of the trait.

The trait

Implement these in a new crates/core/src/adapters/<governor>.rs, then add pub mod <governor>; to crates/core/src/adapters/mod.rs:

methodresponsibility
name()stable adapter id, e.g. script3-soroban-governor
matches_event(event)is this event ours? contract id + topic symbol + exact arity; return false on any doubt
normalize_event(event)decode topics/data into NormalizedEvent; Err on an unexpected shape
decode_call(call, spec)decode a proposal action into DecodedCall; use spec only for argument names

Governor-specific helpers (state/tally reconstruction, action enums, contextual rules projection) live in the same file.

Rules that are not optional

  1. Fail closed. Unknown variant ⇒ a preserved Unknown / Err, never a plausible guess. Missing field ⇒ Err. Absent spec ⇒ decoding: "unverified" with positional args, never invented names.
  2. No mock data. The adapter is pure: it takes RawEvent / RawCall / ScVal values. It must not import tests/fixtures (CI enforces this).
  3. Every shape is cited. Transcribe each decoded structure from the pinned upstream source at the SHA in scripts/upstream/upstream.lock.json, and add a row to a docs/adapters/<governor>.md source-to-behavior table.
  4. Differential test it. Add offline tests that decode committed raw captures and assert equality with the on-chain reads. If a read you need was not captured, extend the capture tooling, re-capture, and commit — do not invent the value.
  5. Register the claims. Anything the README then states about the adapter gets a claims.json entry.

Sketch

#![allow(unused)]
fn main() {
// crates/core/src/adapters/acme.rs
use crate::adapter::{GovernorAdapter, RawCall, RawEvent};
use crate::error::AdapterError;
use crate::types::{DecodedCall, DecodingStatus, NormalizedEvent};

pub const ADAPTER_NAME: &str = "acme-governor";

pub struct AcmeAdapter;

impl GovernorAdapter for AcmeAdapter {
    fn name(&self) -> &'static str { ADAPTER_NAME }

    fn matches_event(&self, event: &RawEvent<'_>) -> bool {
        // contract id + topic symbol + exact arity; false on doubt
        todo!()
    }

    fn normalize_event(&self, event: &RawEvent<'_>) -> Result<NormalizedEvent, AdapterError> {
        todo!()
    }

    fn decode_call(
        &self,
        call: &RawCall<'_>,
        spec: Option<&crate::spec::ContractSpec>,
    ) -> Result<DecodedCall, AdapterError> {
        // names only from spec; positional + Unverified otherwise
        todo!()
    }
}
}

Checklist before a PR

  • cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace
  • OFFLINE=1 bash scripts/check-claims.sh (add claims for new README statements)
  • python3 scripts/check-deployments.py if you registered fixtures
  • differential tests cover every captured proposal of the governor

Contributing

The full guide is in CONTRIBUTING.md. This page summarises what a pull request must satisfy in each repository.

Rules that apply everywhere

  1. Land every change through a branch and a PR. One logical unit per commit, conventional commit message, and a PR body that lists evidence for each “verified” claim.
  2. The PR gate is offline and deterministic. Tests pass with the network disabled. Live-testnet and external-link checks run in the nightly tier and never block a merge.
  3. Fixtures are committed raw responses with provenance. Production code never imports them; CI enforces this in every repository.
  4. Cross-repo coupling is by pin. A commit SHA plus sha256, or a release asset URL plus sha256. No relative-path dependency between repositories.
  5. No secrets in git. gitleaks runs over the worktree and the full history.
  6. Claims are machine-checked. If a README states a fact, claims.json has a command that verifies it.
  7. Never loosen a check to make it pass. Fix the cause, or report the blocker.

Per repository

RepositoryLocal gate
parsercargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace, OFFLINE=1 bash scripts/check-claims.sh, python3 scripts/check-deployments.py
indexernpm run check-pins, npm run typecheck, npm run lint, npm run test:unit, npm run test:integration, npm run build
dashboardnpm run check:pins, npm run typecheck, npm run lint, npm run test:unit, npm run build, npx playwright test, bash scripts/check-claims.sh

Documentation

A feature PR is not done until its documentation exists in that repository’s docs/ and is pinned into this site by a parser-repo PR that bumps docs-site/sources.lock. Generated pages are never edited by hand:

cd docs-site
python3 scripts/gen-risk-rules.py
python3 scripts/gen-cli.py
python3 scripts/gen-api.py
python3 scripts/gen-evidence.py
python3 scripts/gen-upstream.py

Then build and check:

mdbook build
python3 scripts/check-links.py
python3 scripts/check-docs-claims.py

Code of conduct

Participation is covered by the Code of Conduct. Security reports go through SECURITY.md, never a public issue.

Roadmap

Ordered by dependency, not by desire: each step unblocks the next.

Done

StepState
ScVal → JSON, contractspecv0, risk rules, GovernorAdapter, power/tally replicasimplemented, differentially tested
Script3 adapterimplemented, tested against committed on-chain reads
v1 schema frozen, additive-only gateimplemented
WASM package v0.1.0-alpha.2 with a reproducible sha256released as a pre-release
Dry-run model and sgp simulate commandimplemented, offline-tested; accepts validated transaction envelopes
Indexer ingestion (I0–I2)implemented, integration-tested
Dashboard scaffold: /, /simulate, /statusmerged, CI green
This documentation sitebuilt and deployed from the parser repo

Next

StepDepends onStatus
Dependency refresh (TLS advisories)—done: rustls 0.23.46, rustls-webpki 0.103.16, webpki-root-certs 1.0.11
Parser WASM v0.1.0-alpha.2: adapter event surface + simulation request/responsedependency refreshimplemented
Indexer I3: decode through the pinned adapteralpha.2 assetplanned
Indexer I4: GraphQL health and raw-event connectionI2implemented; decoded entities remain planned
Indexer I5: reconciliationI4planned
Indexer I6–I7: deploy recipe, clean-clone proofI4shipped; no deployed instance yet
Dashboard data pages, codegen, live-indexer e2eI4planned
OpenZeppelin adapterfixtures already committedplanned, bounded effort
72-hour soak on a hostdeploy recipe + a hostblocked on infrastructure

Not scheduled

  • Vote casting or any write path: the suite is read-only.
  • A composite trust score for delegates: metrics stay raw counts plus formulas.
  • Mainnet deployments: fixtures and evidence are testnet.

Dates are deliberately absent. Each row moves when its dependency lands and its evidence exists, and this page is updated in the same PR.

Changelog

Release history for the parser, which carries the suite’s tagged releases. Indexer and dashboard changes are tracked in their own repositories and summarised on their pages here.

v0.1.0-alpha.1 — 2026-10-08

Pre-release: parser WASM package.

  • Deterministic wasm-pack build for the nodejs and web targets, packaged into one .tgz.
  • Committed release lock with the asset sha256 7b2eb9420f0cf0582d079f9603beb1e8548e1c7bac165724688cbae5b9237ecd (245,210 bytes), which is what consumers pin.
  • Node test-runner and headless-Chromium tests drive the built artifact against committed captures.
  • CI gains the wasm-package job.
  • Exports: decode_scval_base64, parse_contract_spec, classify_risk, script3_decode_proposal, script3_state_from_chain, script3_tally_from_chain, evaluate_risk_context, evaluate_tally.

Release notes: https://github.com/Stellar-Governance-Guardians/soroban-governance-parser/releases/tag/v0.1.0-alpha.1

Schema v1 — 2026-10-08

  • schemas/governance-v1.graphql frozen with an additive-only gate: the PR gate rejects any removal or incompatible change relative to governance-v1.baseline.graphql.
  • sha256 cfbd85a82ee834ac14723cd5dd73ce18a5aba0313a91a0aaf6053d1a486ae275, pinned by the indexer and the dashboard.

Earlier milestones

DateChange
2026-10-08Script3 adapter and snapshot power; sans-IO dry-run model from real captures; hardening (fuzz targets, audit, adapter guide); seed-v2 verification that works from a clean clone
2026-10-06Step 0 governance files and charter CI across the three repositories
2026-10-05Phase 1 testnet proof: deployments registry and committed fixtures

Unreleased

alpha.3 line — export-surface break (no release yet)

  • Breaking change on the alpha line: the governor_identify, governor_matches_event, governor_normalize_event and governor_decode_call exports are removed. They read just topic[0], ignored the topic count, dropped the contract id on call decode, and two of the four carried no #[wasm_bindgen] attribute. Use identify_governor, matches_event, normalize_event and decode_call, which take the complete topic vector and preserve the contract id.

  • Exactly one matches_event contract remains: full topic vector plus value, false for a foreign contract or wrong topic shape, an explicit error for malformed XDR.

  • The Node and headless-browser suites now cover identify_governor, matches_event (match, foreign contract, topic-count mismatch, wrong topic shape, malformed XDR) and normalize_event (verified decode plus a matched event with an unexpected payload shape), and decode_call with and without a contract spec.

  • No asset has been published for this line yet: releases/parser-wasm.lock.json still pins v0.1.0-alpha.2.

  • v0.1.0-alpha.2: adapter event surface (matches_event / identify, normalize_event with the unverified fallback, decode_call) and the simulation request/response surface.

  • Dependency refresh for the open cargo audit advisories.

  • This documentation site.

Upstream pins

Generated from docs-site/sources.lock by docs-site/scripts/gen-upstream.py. Do not edit by hand.

The indexer and dashboard pages on this site are summaries written in this repository, not embedded copies of those repositories’ own documentation. Each summary names the exact revision it was written against below, and the bytes of that revision are vendored under docs-site/pinned/ and hashed by the PR gate. The nightly docs-live workflow re-fetches the pinned file and fails if the content no longer matches — so a silent change upstream cannot leave this site claiming something stale.

RepositoryPathCommitsha256
https://github.com/Stellar-Governance-Guardians/governance-event-indexerREADME.md68aa657c68cb1cc1fca1f87da5f55ec74e7272bfb78f22a64605059e2db939671280835a4ee072947a0d7c1cbe7200da44ea49b3
https://github.com/Stellar-Governance-Guardians/delegate-portal-dashboardREADME.md0c622709848637eb5908d9d0432475ce1cc92fe69406bedbc6fcdddbde6278e6bb0ee166390035241fd3d6810660ca899b73bb6c

Bumping a pin is a parser-repo PR that shows the vendored byte diff and the review on this site that the summary still matches. This happens at the end of every phase.

What is not pinned

Nothing from a sibling repository is rendered as prose on this site today. If a later phase embeds sibling documentation directly, it must enter through this lock file first.