Skip to main content
Canopy exposes every aspect of its market state directly on-chain, meaning you can query offers, series metadata, and complete event history from any standard EVM JSON-RPC node without relying on a proprietary API or off-chain data provider. Every offer creation, fill, cancellation, and claim issuance is recorded either as contract storage or as an indexed event log, giving you a fully auditable and permissionless view of market activity. This guide walks you through the key read patterns — from fetching a single offer to reconstructing a live order book — using ethers.js v6.

Reading a Specific Offer

If you already know an offer ID, you can fetch its current state directly from contract storage with a single offers call. The returned struct contains the seller address, total assets remaining, price, expiry, and fill history.
const offer = await venue.offers(offerId);
console.log(offer.seller, offer.assetRemaining, offer.pricePerUnit, offer.expiry);
Offer IDs are sequential unsigned integers starting from 0. You can enumerate all offers by iterating from 0 to nextOfferId - 1, or by reading the nextOfferId public variable to know the total count.

Reading All Offers for a Trading Pair

To discover all offers for a given asset without knowing individual offer IDs, filter the OfferCreated event log by asset address. This returns every offer ever created for that asset. Cross-reference the results against OfferFilled and OfferCancelled events to determine which offers are still active.
const filter = venue.filters.OfferCreated(null, null, ASSET_ADDRESS);
const events = await venue.queryFilter(filter, 0, "latest");
// Then cross-reference with OfferFilled and OfferCancelled to get current state
On public RPC endpoints, querying from block 0 to "latest" in a single call can time out or be rate-limited. Use a sliding block range window — for example, chunks of 10,000 blocks — to paginate through the full history reliably.

Getting Series Info for Deferred Markets

Canopy forward series are identified by a deterministic series ID derived from the asset address and unlock time. Compute the series ID using the venue’s helper function or locally, then read the series data:
// Using the venue's helper function
const seriesId = await venue.seriesIdFor(assetAddress, unlockTime);

// Or compute locally (must match abi.encode, not abi.encodePacked)
const seriesId = ethers.keccak256(
  ethers.AbiCoder.defaultAbiCoder().encode(
    ["address", "uint64"],
    [assetAddress, unlockTime]
  )
);

const seriesData = await venue.series(seriesId);
console.log(seriesData.claimToken, seriesData.backing, seriesData.unlockTime);
The series key is (asset, unlockTime) — the quote token is not part of the series identifier. All deferred fills for the same asset with the same unlock time share a single claim token, regardless of which quote token was used.

Checking Claim Token Balances

Each series deploys its own ERC-20 claim token at the address stored in seriesData.claimToken. Once you have that address, interact with it using any standard ERC-20 ABI — no Canopy-specific interface is required for balance and transfer reads.
const claimToken = new ethers.Contract(seriesData.claimToken, ERC20_ABI, provider);
const balance = await claimToken.balanceOf(userAddress);

Previewing Fill Cost

The venue provides a view function to compute the exact quote cost for a prospective fill without sending a transaction:
const quoteCost = await venue.quoteCostFor(offerId, assetUnits);
const offer = await venue.offers(offerId);
const fee = quoteCost * BigInt(offer.feeBps) / 10000n;
console.log(`Base cost: ${quoteCost}, Fee: ${fee}, Total: ${quoteCost + fee}`);

Building a Live Order Book

Reconstructing a full order book for a trading pair requires combining event history with simple state-machine logic. Follow this pattern to build an accurate, always-current view of available liquidity:
1

Index OfferCreated from the deployment block

Query all OfferCreated events from the contract’s deployment block to the current head. Store each event’s offerId, asset, quote, escrowedAssetAmount, pricePerUnit, expiry, unlockTime, and allowedBuyer fields. Set assetFilled = 0 and status = OPEN for every new record.
2

Apply OfferFilled deltas

For each OfferFilled event, locate the matching offer by offerId and increment assetFilled by the event’s assetUnits. When assetFilled equals the original escrowed amount, mark the offer FILLED.
3

Remove cancelled offers

For each OfferCancelled event, mark the corresponding offer CANCELLED. Cancelled offers should be excluded from any live depth calculation.
4

Filter by asset and quote

To render a pair’s order book, filter your local state to offers where asset == targetAsset and quote == targetQuote and status == OPEN and expiry > now. Compute assetRemaining = escrowedAssetAmount - assetFilled for each, then sort by price to produce bid and ask ladders.
All the information needed for full state reconstruction is encoded in the events themselves. You do not need to make additional contract storage reads during initial sync — the events are self-contained.