Cascade

Build your own integration

Reusable patterns for any Injective × Cascade product — the action_id pointer pattern, contract-side conventions, indexer responsibilities, and what to copy from Inscribe.

Inscribe is one shape of Injective × Cascade product (prediction markets with permanent justifications). It is not the only one. This page is a recipe book for building a different one: where to copy from Inscribe, where to diverge, and the failure modes to design around.

The shape every integration takes

Regardless of what your dApp does, the architecture is the same three-layer split:

┌──────────────────────────────────────────────────────────────┐
│  Frontend  ── signs Injective txs, optionally Lumera txs ──  │
└────────┬────────────────────────────────────┬────────────────┘
         │                                    │
         │ user wants                         │ user-driven OR
         │ to "do thing X"                    │ server-signed
         ▼                                    ▼
┌─────────────────────┐               ┌──────────────────────┐
│  Injective contract │               │  Cascade (Lumera)    │
│  - holds STATE      │  cid pointer  │  - holds the BYTES   │
│  - holds FUNDS      │◄─────────────►│  - one action_id     │
│  - emits EVENTS     │               │    per artifact      │
│  - holds POINTERS   │               │                      │
└──────────┬──────────┘               └─────────┬────────────┘
           │                                    │
           ▼                                    ▼
┌──────────────────────────────────────────────────────────────┐
│  Indexer  — joins on-chain state with Cascade content        │
│             so the frontend gets one fast read               │
└──────────────────────────────────────────────────────────────┘

The thing you customise per product is what the bytes are: trade receipts, NFT media + metadata, governance proposals, oracle inputs, audit logs, archival timeline data, off-chain order signatures, encrypted messages, attestations, votes, you name it. Everything else is plumbing.

Decision: who signs the Cascade write?

The most important decision in the design phase. The options are spelled out in Cascade from Injective; the short version:

Choose Pattern A (user-signed) if...Choose Pattern B (server-signed) if...Choose Pattern C (contract-driven) if...
Provenance matters — the user must be the on-chain authorUX matters — one signature, faster pathThe artifact must exist whenever the state transition happens, with no user in the loop
Users already need a Lumera address for other reasons (faucet, staking)Users may not have a funded Lumera addressIndexers and trustless readers need the artifact guaranteed-present
You want zero server-side custodyYou're OK funding ulume at the operator levelYou're prepared for the IBC packet round-trip latency and operational complexity

Most production integrations end up using A for user-authored content and B or C for derived/canonical content. Inscribe uses B for everything in the MVP because that ships faster; it migrates user-content paths to A as wallet onboarding matures.

Contract-side conventions

These three patterns are what make an Injective contract Cascade-friendly. Adopt them and your indexer and frontend become trivial.

Convention 1: Every Cascade-bound field is a String

Not a Vec<u8>, not a custom type — a plain UTF-8 String that the contract treats as opaque. The contract checks !cid.trim().is_empty() and that's the entire validation. Resolution and integrity are off-chain concerns.

pub struct EvidenceItem {
    pub cid: String,            // ← Cascade action_id
    pub submitter: Addr,
    pub role: String,
    pub block_height: u64,
}
 
if cid.trim().is_empty() {
    return Err(ContractError::InvalidCid("empty cid".into()));
}

Don't store base32 / base58 prefixes ("bafy…"). Cascade action_ids are numeric strings; storing them as-is keeps your indexer's queries simple.

Convention 2: Emit the CID as a wasm event attribute

Indexers reconstruct state from wasm events. Every state transition that introduces a new CID should emit it as a discoverable attribute alongside action, submitter, and the rest.

Ok(Response::new()
    .add_attribute("action", "submit_evidence")
    .add_attribute("cid", cid)
    .add_attribute("role", role)
    .add_attribute("submitter", info.sender))

Indexers can then either (a) decode events live and avoid smart queries entirely, or (b) use events as triggers and smart-query for full state. The Inscribe indexer uses (b) for simplicity.

Convention 3: Expose CIDs in queries

A read-only client that doesn't run an indexer should be able to drive a viewer from contract queries alone. That means at minimum:

#[returns(Vec<EvidenceItem>)]
ListEvidence {},
 
#[returns(Option<Proposal>)]    // proposal includes justification_cid
GetProposal {},
 
#[returns(Option<Settlement>)]  // settlement includes winning_justification_cid + settlement_cid
GetSettlement {},

This makes the contract self-describing: a developer with no indexer can query the chain, get every CID, fetch each from a Cascade gateway, and render the full state.

Convention 4: State machines, not free-form

The Inscribe market is a strict state machine: Open → Proposed → Challenged → Voting → Final. Every execute message checks the current state and either advances it or rejects. This is the cleanest way to model Cascade-bound workflows because each state defines exactly which CIDs may be added:

StateWhat CIDs are added
Openevidence
Proposedevidence, proposal justification
Challengedevidence, challenge justification
Votingevidence, vote justifications
Finalsettlement record

An attacker cannot add a "vote justification" in the Open state and confuse the indexer; the contract rejects it.

Convention 5: Atomic cross-contract calls for funds + CIDs

If your CID submission also moves funds (a bond, a fee, a deposit), use the standard CosmWasm pattern: emit a CosmosMsg::Wasm(WasmMsg::Execute) in the same response. The Inscribe market does this for proposer / challenger bond locking against bond-vault. The whole tx either succeeds or reverts — the indexer never sees a half-state where the CID was registered but the bond was not.

Indexer responsibilities

Three jobs, in order of priority:

  1. Mirror on-chain state into a queryable store. Smart-query each contract instance every N seconds. Postgres is more than enough; the data volume is bounded by the number of markets/objects, not by user traffic.
  2. Eagerly resolve every new CID. When you see a new event with a CID attribute, kick off a background fetch from a Cascade gateway and cache the body locally. Frontend reads should hit your cache, not Lumera.
  3. Serve joined views for the frontend. GET /things/{id} should return on-chain state and inlined Cascade content in one response.

The third one is what makes the UX feel fast. A naive frontend doing 20 round trips per page (one for state, 19 for CIDs) is painful; the same data served as one JSON blob is instant.

Frontend conventions

The Inscribe web app is ~1500 lines of Next.js + Keplr. Patterns worth copying:

  • getOfflineSignerOnlyAmino for Injective signing. Direct-mode signing does not work with eth_secp256k1. Always Amino. See Injective primer.
  • Server Components for read paths. The Inscribe market detail page is a Server Component that fetches /markets/{addr} and renders. No client-side data fetching on initial load.
  • Client Components for write actions. A single <MarketActions> Client Component renders only the buttons appropriate to the current state (Open → bet + evidence + propose; Proposed → evidence + challenge + finalize; etc.). Drives the user through the state machine without showing actions that would be rejected on chain.
  • Refresh after a delay. After a successful execute, schedule a router.refresh() 10–12 seconds out so the indexer has one full poll-tick to catch up before the UI re-reads.
  • Surface contract errors verbatim. Don't try to translate wasm execute failed: WrongState(...) into something prettier — the verbatim error is usually the most actionable thing the user can see.

When to skip Cascade entirely

Cascade is the right home for content that needs to be permanent, verifiable, and large. It is not the right home for everything an Injective dApp produces.

Use Cascade if...Use plain on-chain state if...
The artifact is > a few hundred bytesThe artifact is a small enum or numeric value
The artifact needs to outlive the dApp's lifecycleThe artifact is intermediate / cache state
The artifact will be cited or audited in the futureThe artifact is purely operational
You want pay-once economicsYou don't need permanence at all
The artifact is user-content (proof, evidence, justification)The artifact is metadata about contract state

A 32-byte hash, an enum, a counter — these belong in CosmWasm state. A 500-byte JSON blob with a claim and citations, a 5-MB PDF, a 3D NFT model — these belong on Cascade with a String pointer in CosmWasm state.

Things to design around

Pitfalls the Inscribe build surfaced. None are blockers, but each deserves a deliberate decision.

IBC packet latency

If you use Pattern C (contract-driven Cascade writes), the round-trip is 30–60 seconds in the happy path and can stall on relayer hiccups. Don't put it on the user's hot path. Defer it to canonical artifacts that don't gate user actions, or use Pattern B with a fast server.

sn-control as a SPOF

sn-control is a single HTTP service holding a bearer token. If it's down, your POST /cascade/upload is down. Either run your own sn-control (Inscribe inherits one from the cascadego deployment) or use a Cascade gateway with HA. The download path can use multiple gateways via a URL resolver; the upload path is currently single-homed.

Quota and ulume balance

Every Cascade write spends ~10–15k ulume. A funded address with 1M ulume covers ~70 uploads. Monitor your service balance via /healthz (cascade-api) or whatever your gateway exposes; alert before exhaustion. The Inscribe API does not yet enforce per-user quotas; production deployments should.

eth_secp256k1 vs secp256k1

Injective uses eth_secp256k1; Lumera uses standard secp256k1. If you derive both from the same Keplr mnemonic, the user has two different addresses (different coinType paths). UI flows must surface both addresses and not confuse them.

Indexer lag

A 10-second poll interval means UI sees a stale state for up to 10 seconds after a tx confirms. Two mitigations:

  1. After a successful tx, optimistically update the local UI state.
  2. Use shorter intervals (3–5s) at the cost of more LCD load.

Inscribe chose the simpler refreshSoon() pattern: wait 12 seconds, then router.refresh() and let the Server Component re-fetch. Good enough for the testnet UX, fragile under heavy load.

Re-litigation needs gateway redundancy

The "anyone can fetch the full record forever" property depends on at least one Cascade gateway being reachable. The bytes are permanent on the Supernode network, but a reader who only knows an HTTPS URL needs that URL to resolve. Plan for at least two gateways in production (your own + Lumera-operated) and consider exposing a "Cascade-native" reader that talks to Supernodes directly for very long-horizon use cases.

What to copy from Inscribe

Concrete artifacts worth lifting if you're building in a similar shape:

Inscribe fileWhat to copy / adapt
contracts/bond-vault/The custody-by-market pattern with a whitelist of caller contracts
contracts/voter-registry/src/sampling.rsDeterministic weighted-without-replacement committee sampling
contracts/inscribe-market-v2/src/contract.rs::compute_payoutPari-mutuel payout formula with Invalid refund path
services/internal/indexer/indexer.goPoll-and-mirror loop with eager Cascade resolution
services/internal/api/server.go::auditMarketThe fat audit endpoint pattern
services/internal/api/upload.goPattern B server-signed upload pipeline
web/lib/inj.ts + web/lib/keplr.tsInjective + Keplr signing with Amino-only
web/components/market-actions.tsxState-machine-aware action UI
scripts/deploy-testnet.sh + smoke-test-testnet.shReproducible deploy + bring-up

Other product shapes that fit

Beyond Inscribe-style prediction markets, the same Injective × Cascade pattern is a natural fit for:

  • NFT marketplaces where metadata permanence matters (parallel to the Lukso LSP-4 / LSP-8 integration): contract holds the token + a Cascade action_id; metadata + media live on Cascade.
  • DAOs where proposals, debate threads, and voting rationale need to be archival: Snapshot's "proposals on IPFS" pattern, but with permanence.
  • DeFi protocols publishing oracle inputs with a permanent receipts trail (think Chainlink data feeds where the input set is part of the audit).
  • Audit / compliance logs that need decade-scale verifiability, signed by the on-chain contract.
  • Encrypted messaging layered on Cascade's encrypted storage with Injective contracts handling access control.
  • Long-horizon prediction markets with settlement dates years out — Cascade's permanence guarantee outlives the underlying market.

In each case the integration shape is what's in the diagram at the top of this page; only the schema of "what the bytes are" changes.

Next steps

Edit this page