Lumera Protocol

Build your own integration

Reusable patterns for any Injective × Cascade product

Inscribe is one shape of an Injective × Cascade product. This page is the recipe book for building your own: the architecture every integration shares, the conventions that make it clean, and the pitfalls worth a deliberate decision up front.

The shape every integration takes

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

Frontendsigns Injective txs · optionally Lumera txsInjective contractholds STATEholds FUNDS, emits EVENTSholds POINTERS (action_id)Cascade (Lumera)holds the BYTESone action_id per artifactIndexerjoins on-chain state with Cascade contentcid pointer

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

Decision: who signs the Cascade write?

The most important call you make. The options are detailed 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 path, no Lumera address for usersThe artifact must exist whenever the state transition happens, with no user in the loop
Users already need a Lumera address for other reasonsYou're OK funding ulume at the operator levelIndexers and trustless readers need the artifact guaranteed-present
You want zero server-side custodyYou want a single key you can monitor and rotateYou accept the IBC round-trip latency and operational complexity

Start with Pattern B. It's the fastest path to a working product and the one the reference implementation uses for everything; the table above covers when to deviate.

Contract-side conventions

Five conventions make an Injective contract Cascade-friendly. Adopt them and your indexer and frontend become trivial.

1. Every Cascade-bound field is a String

Not a Vec<u8>, not a custom type, a plain UTF-8 String the contract treats as opaque. Validation is !cid.trim().is_empty() and nothing more. Cascade action_ids are numeric strings; store them as-is to keep indexer queries simple.

pub struct StoredArtifact {
    pub cid: String,            // ← Cascade action_id
    pub submitter: Addr,
    pub block_height: u64,
}

2. Emit the CID as a wasm event attribute

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

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

3. Expose CIDs in queries

A read-only client with no indexer should be able to drive a viewer from contract queries alone. Expose every CID-bearing record:

#[returns(Vec<StoredArtifact>)]
ListArtifacts {},

A developer can then query the chain, get every CID, fetch each from a Cascade gateway, and render the full state without an indexer.

4. State machines, not free-form

Model Cascade-bound workflows as a strict state machine where each state defines exactly which CIDs may be added. The reference implementation uses Open → Proposed → Challenged → Voting → Final, and an actor cannot, say, add a "vote justification" in the Open state, the contract rejects it, so the indexer never sees a confusing half-state.

5. Atomic cross-contract calls for funds + CIDs

If a CID submission also moves funds (a bond, a fee, a deposit), emit a CosmosMsg::Wasm(WasmMsg::Execute) in the same response. The whole tx succeeds or reverts together, the indexer never sees a state where the CID was registered but the bond was not.

Indexer responsibilities

Three jobs, in priority order:

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

The third 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. Because Cascade content is immutable, the cache is permanent: no invalidation strategy.

Frontend conventions

  • getOfflineSignerOnlyAmino for Injective signing. Direct-mode signing does not work with eth_secp256k1. Always Amino. See the primer.
  • Server Components for read paths. Fetch your joined /things/{id} view and render server-side; no client-side fetching on initial load.
  • Client Components for write actions. Render only the actions valid in the current state, so the UI drives the user through the state machine without offering calls the contract would reject.
  • 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. wasm execute failed: WrongState(...) is usually more actionable than anything you'd rewrite it into.

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 must 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

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

None are blockers, but each deserves a deliberate decision.

Gateway as a single point of failure

In Pattern B, your POST /cascade/upload depends on one cascade-api endpoint holding a signing key. If it's down, uploads are down. Either rely on the HA Lumera-operated cascade-api at api.lumera.help, or run your own behind a domain you control. The download path is more forgiving, it can fan out across multiple gateways via a URL resolver but the upload path is single-homed by nature.

ulume balance

Every Cascade write spends ~10–15k ulume; a 1M-ulume balance covers ~70 uploads. Monitor your service key's balance and alert before exhaustion. If you expose uploads to users, add per-user quotas, an open upload endpoint is a way to drain your key.

eth_secp256k1 vs secp256k1

Injective uses eth_secp256k1; Lumera uses standard secp256k1. Derived from the same Keplr mnemonic, the user has two different addresses (different coinType paths). Any UI that surfaces both must not confuse them.

Indexer lag

A 10-second poll interval means the UI can see stale state for up to 10 seconds after a tx confirms. Mitigate with optimistic local updates, a shorter interval (at the cost of more LCD load), or the simple refresh-after-12s pattern, which is fine for testnet UX but fragile under heavy load.

IBC latency (only if you use Pattern C)

A contract-driven Cascade write over ICA is a slow IBC round-trip that can stall on relayer hiccups (see the primer's note on IBC for the latency). Never put it on a user's hot path: defer it to canonical artifacts that don't gate user actions, or use Pattern B with a fast server, as the reference implementation does.

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 for very long-horizon use cases consider a Cascade-native reader that talks to Supernodes directly.

Other product shapes that fit

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

  • NFT marketplaces where metadata permanence matters: the contract holds the token + an action_id; metadata and media live on Cascade.
  • DAOs where proposals, debate threads, and voting rationale need to be archival. Snapshot's "proposals on IPFS" pattern, but permanent.
  • DeFi protocols publishing oracle inputs with a permanent receipts trail (Chainlink-style 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 markets with settlement dates years out. Cascade's permanence outlives the underlying market.

Next steps

Edit this page