Lumera Protocol

Cascade from Injective

How any Injective dApp uses Cascade as its permanent storage layer.

This is the reusable primitive the rest of this section builds on. The shape is the same whether you're shipping an NFT marketplace, a DAO, a derivatives venue, or prediction markets: Injective holds state and pointers, Cascade holds the bytes, and an action_id ties them together.

The action_id pointer pattern

Cascade's unit of storage is an action. A MsgRequestAction on Lumera registers an artifact's metadata and returns a numeric action_id. Once stored, the artifact is retrievable forever by GET /download/<action_id> against any cascade-aware gateway. See How Cascade Works for the full lifecycle.

Inside your Injective contract, every Cascade artifact is referenced by a single value: that action_id, stored as a String in CosmWasm state.

a contract storing a Cascade pointer
pub struct StoredArtifact {
    pub cid: String,         // ← Cascade action_id, e.g. "15823"
    pub submitter: Addr,
    pub block_height: u64,
}

That single String is the entirety of the cross-chain coupling. The contract enforces only that the field is non-empty. It does not enforce that the artifact resolves, because it cannot read across chains synchronously. Resolution and integrity are read-time concerns, handled by an indexer or the frontend, off the consensus path.

Two ways to write to Cascade

There are two production patterns for getting bytes onto Cascade from an Injective context. They are not mutually exclusive. Many deployments use both, choosing per artifact type.

Pattern A: user-signed (the user signs the Lumera tx)

The user holds both an inj1… and a lumera1… address in their wallet. The dApp signs two transactions for a single action: first a Lumera tx that uploads to Cascade and returns an action_id, then an Injective tx that submits that action_id to your contract.

Wallet(Keplr)Lumera(Cascade)Injective(your CW)① sign Lumera tx② action_id③ submit(cid: action_id)

Choose this when provenance matters where every artifact carries a direct, on-chain link back to the user's own Lumera key. The cost is one extra signature and a small ulume fee the user pays.

A backend holds a funded Lumera signing key and uploads on behalf of users. The user signs only the Injective transaction, the upload is a server-side HTTP call. The simplest way to run this is to forward to the Lumera-operated cascade-api at api.lumera.help, which holds the Lumera key, signs MsgRequestAction, and pays the ulume fee for you.

User(web)Your backendforwards to cascade-apiLumera(Cascade)Injective(your CW)① POST /uploadaction_idsigns · action_id② sign Injective tx · submit(cid: action_id)

Choose this when UX matters: one signature instead of two, no Lumera address required for the user, and a single rate-limited signing key you can monitor and rotate. This is the path the Inscribe reference implementation uses end-to-end, and it's the fastest way to ship.

The trade-off is provenance: every artifact is signed by the operator's key, not the user's. If you need on-chain proof that user X uploaded artifact Y, record the binding on Injective by having your contract store (user, action_id) so the pairing is the on-chain proof rather than the Lumera signature.

The server-side forward, in detail

A minimal backend just streams the multipart body to the cascade-api and returns its response. The browser sends the file, the backend forwards it under its bearer token, and the cascade-api signs MsgRequestAction, broadcasts on Lumera, and replies with action_id + tx_hash + block_height + task_id (typically 6–15s). The backend relays that response back to the browser, which then submits the action_id on Injective.

BrowserYour backendcascade-api① file② POST /upload③ action_idto client

There is no IBC round-trip on this path. The cascade-api signs on Lumera directly. The bearer token is your operator key, scoped by the cascade-api's quota.

If you'd rather hold the Lumera key yourself instead of using the hosted cascade-api, the equivalent with @lumera-protocol/sdk-js is:

server-side upload with your own Lumera key
import { createLumeraClient } from "@lumera-protocol/sdk-js";
 
// One funded Lumera key uploads on behalf of users.
const client = await createLumeraClient({
  preset: "testnet",
  signer,
  address: serverLumeraAddress,
  gasPrice: "0.025ulume",
});
 
const result = await client.Cascade.uploader.uploadFile(file, {
  fileName: "artifact.json",
  isPublic: true,
  expirationTime: String(Math.floor(Date.now() / 1000) + 24 * 60 * 60),
});
// result.action_id  → 15823

Pattern C: contract-driven via ICA (advanced)

There is a third pattern, where a CosmWasm contract on Injective writes to Cascade autonomously over IBC: the contract packs a MsgRequestAction, sends it across the IBC channel to a registered Interchain Account on Lumera, and the ICA executes it.

This is not on the hot path, and you should not reach for it first. An earlier prototype of the reference implementation dispatched Cascade writes this way via cw-ica-controller and waited for a Hermes relayer. Reserve Pattern C for canonical, contract-only artifacts where there is genuinely no user in the loop and the contract itself must be the signer.

Rule of thumb: use A when on-chain provenance to the user's key is required, B for everything else (and as your default), and consider C only for contract-deterministic canonical writes where IBC latency is acceptable.

Reading from Cascade

Reading is independent of the write pattern. Once an artifact has an action_id, anyone can fetch it, with no authentication, from any cascade-aware gateway:

EndpointAuthReturns
GET https://api.lumera.help/download/{action_id}noneraw bytes, Content-Type sniffed
GET <your-backend>/cascade/{action_id}your choiceraw bytes, ideally with an immutable cache header
Lumera SDKwallet signaturereconstructed bytes streamed

Wiring the pointer into your contract

The Cascade integration on the CosmWasm side is small. Accept the action_id as an opaque String at write time and validate only that it is non-empty:

pub fn exec_submit(
    deps: DepsMut, env: Env, info: MessageInfo, cid: String,
) -> Result<Response, ContractError> {
    if cid.trim().is_empty() {
        return Err(ContractError::InvalidCid("empty cid".into()));
    }
    // ... append to state, emit event with cid as attribute
}

From there, two conventions make the data easy to consume: emit the CID as a wasm event attribute so an indexer can pick it up, and expose CIDs in queries so a read-only client can drive a viewer without one. That is the essential contract-side surface: the contract does not download from Cascade, does not verify the artifact, does not even care that it's a Cascade CID. It holds a string; verification and resolution belong to the indexer and the frontend.

Build your own integration has the code for both, plus two production conventions: modelling the workflow as a state machine, and locking funds atomically with the CID write.

Funding the writes

Cascade writes are paid in ulume. Which account pays depends on the pattern:

AccountWhat it pays forHow to fund
The user's lumera1… (Pattern A)Each MsgRequestAction they signFaucet
Your service's Lumera key (Pattern B)Each MsgRequestAction your service signs on behalf of usersFaucet
The ICA's lumera1… (Pattern C)Each Cascade write the contract triggersTop up over IBC from your Injective treasury

A Cascade upload costs roughly 10–15k ulume on testnet.

Next steps

Edit this page