Skip to content
Zeno Docs

Integrating

This guide covers the client side: a RAILGUN wallet fork acting for a maker, and a filler bot or relayer acting for the other leg.

As a maker: submitting an order

The maker is entering the pool. They escrow public tokens and describe the shielded note they want back.

  1. Choose the note recipient key. mpk is the pool master public key that must be able to spend the delivered note.
  2. Approve the contract to move takingAmount of takingToken.
  3. Call submitOrder, which takes custody of the escrow immediately and emits OrderCreated.
function submitOrder(
    bytes32 mpk,
    TokenData calldata makingToken,   // token wanted as a shielded note
    uint120 makingAmount,             // note value wanted
    TokenData calldata takingToken,   // public token escrowed as payment
    uint256 takingAmount,             // amount escrowed
    uint256 expiry,                   // must be in the future
    bytes32 listProviderPPoIKey       // recorded on the order and emitted; not read on-chain today
) external;

The ratio of takingAmount to makingAmount is the limit price. TokenData is RAILGUN's (uint8 tokenType, address tokenAddress, uint256 tokenSubID).

listProviderPPoIKey has no default and no on-chain effect. Pass bytes32(0) unless you are coordinating with a list provider out of band and want the key recorded on the order for consumers of the OrderCreated event.

As a filler: discovering orders

Query the subgraph for the order book. The subgraph cannot compare against wall-clock time, so filter for live orders client-side:

query OpenOrders($now: BigInt!) {
  orders(
    where: { status: OPEN, remainingAmount_gt: 0, expiry_gt: $now }
    orderBy: createdAtTimestamp
    orderDirection: desc
  ) {
    id
    orderId
    mpk
    makingTokenAddress
    makingAmount
    takingTokenAddress
    takingAmount
    remainingAmount
    expiry
  }
}

See the subgraph reference for the full schema.

As a filler: filling an order

A fill delivers a shielded note to the maker and claims a pro-rata share of the escrow.

  1. Pick a blinding. Choose random and compute the note the maker asked for: npk = poseidon2(mpk, random), then commitment = poseidon3(npk, tokenID, value).
  2. Build the RAILGUN transaction that creates that commitment, spending your own shielded notes.
  3. Bind the bundle. Set adaptContract to the ZenoLOP address and adaptParams to keccak256(abi.encode(nullifiers, transactions.length, fills, recipient)) % SNARK_SCALAR_FIELD, then generate the shielded proof over those bound params.
  4. Prove the note requirements. Produce the Noir proof whose public signals are token, value, mpk, and the resulting commitment.
  5. Submit both together:
function fillOrder(
    Transaction[] calldata _transactions,  // RAILGUN transactions creating the notes
    Fill[] calldata _fills,                // one per delivered note
    address _recipient                     // where the payout goes on a successful claim
) external;

Each Fill carries { orderId, amount, commitment, proof }. The call reverts atomically if any fill fails, including on the shielded side.

Retain the random you chose: it is required to claim.

As a filler: claiming the payout

fillOrder escrows the payout rather than releasing it. Watch for your OrderFilled event to learn the payoutId and deadline, then claim before the deadline:

function claimPayout(uint256 _pendingPayoutId, bytes32 _random, bytes calldata _proof) external;

The contract recomputes the commitment from the order's mpk, the delivered note amount, and your random, and requires it to match the note delivered.

On success the escrow is released to the recipient minus the taker fee, and PayoutExecuted is emitted, revealing random publicly.

If the deadline passes without a claim, the maker can call reclaimPayout and recover that escrow.