DEV Community

Haruki Kondo
Haruki Kondo

Posted on

Hands-on with Sera Protocol Tutorial: Mastering On-Chain Trade Execution!

Introduction

In my previous articles, I’ve covered the overview of Sera Protocol, how to use its GraphQL API, and how to build a basic data retrieval script.

In this article, we’ll take it to the next level: Executing trades by calling Sera Protocol's smart contracts!

Understanding Sera Protocol's Core Contracts

The Sera Protocol ecosystem is powered by four primary core contracts, each designed for a specific role in trade execution, order management, and price calculation.

1. Market Router

The main entry point for all user interactions. Instead of interacting with individual order books directly, users typically go through the Router.

  • Limit Orders: Provides functions like limitBid (Buy), limitAsk (Sell), and limitOrder (for placing multiple orders at once).
  • Market Orders: Executes marketBid or marketAsk for immediate fills.
  • Claiming: Provides the claim function to collect profits from filled orders, supporting claims across multiple markets simultaneously.
  • Composite Operations: Includes gas-saving utilities like limitBidAfterClaim, which allows you to place a new order immediately after claiming profits.
  • Market Verification: Use isRegisteredMarket to verify if a market is officially registered and valid.

2. OrderBook

Each trading pair has its own dedicated OrderBook contract responsible for storing, matching, and settling orders.

  • State Queries: Check liquidity with getDepth, find the best current price with bestPriceIndex, or verify if the book is empty with isEmpty.
  • Order Details: Use getOrder to fetch specific order data and getClaimable to check fill amounts and fee details.
  • Simulation: Use getExpectedAmount to simulate the input/output amounts expected for a market order before executing it.

3. PriceBook

A specialized contract that handles calculations based on Sera's unique Arithmetic Price Model.

  • Price Conversion: Provides indexToPrice (converts price index to actual price) and priceToIndex (converts actual price to the nearest index).
  • Price Boundaries: Provides metadata such as maxPriceIndex, minPrice, and priceUpperBound.

4. Order Canceler

A utility contract designed for batch canceling orders efficiently across multiple markets. Orders are identified by their NFT IDs.

  • Batch Cancel: Provides cancel (returns assets to your address) and cancelTo (sends assets to a specified address).
  • Auto-Claim: When an order is canceled, any already-filled portion is automatically claimed.

Bonus: Order NFT

Every limit order issued on Sera is represented by an NFT. This allows order ownership to be transferred to other addresses or reused within other DeFi protocols as a composable asset.

Let's Implement the Sample Code!

The code we’ll be using is available in the following GitHub repository:

GitHub logo mashharuki / SeraProtocol-Sample

Sera Protocol Sample Repo

SeraProtocol-Sample

Sera Protocol Sample Repo

Sample Query

Market

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ market(id: \"0xd99802ee8f16d6ff929e27546de15d03fdcce4bd\") { id quoteToken { symbol decimals } baseToken { symbol decimals } quoteUnit makerFee takerFee minPrice tickSpace latestPrice } }"}' \
  https://api.goldsky.com/api/public/project_cmicv6kkbhyto01u3agb155hg/subgraphs/sera-pro/1.0.9/gn | jq
Enter fullscreen mode Exit fullscreen mode

list

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ markets(first: 5) { id quoteToken { symbol } baseToken { symbol } latestPrice latestPriceIndex } }"}' \
  https://api.goldsky.com/api/public/project_cmicv6kkbhyto01u3agb155hg/subgraphs/sera-pro/1.0.9/gn | jq
Enter fullscreen mode Exit fullscreen mode

Order

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ openOrders(first: 10, where: { user: \"0xda6e605db8c3221f4b3706c1da9c4e28195045f5\" }) { id market { id } priceIndex isBid rawAmount status } }"}' \
  https://api.goldsky.com/api/public/project_cmicv6kkbhyto01u3agb155hg/subgraphs/sera-pro/1.0.9/gn | jq
Enter fullscreen mode Exit fullscreen mode

Depths

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ depths(first: 10, where: { market: \"0xd99802ee8f16d6ff929e27546de15d03fdcce4bd\", isBid:
Enter fullscreen mode Exit fullscreen mode

Features Included in This Sample

  • Market Info Retrieval (via Subgraph)
  • Base/Quote Token Balance Display
  • Order Book Depth Retrieval (Best Bid / Best Ask)
  • limitBid Execution with postOnly safety
  • Order Status Polling
  • Automatic claim (if claimable amount exists)
  • claim-only mode (to claim independently later)

File Structure

  • src/index.ts: The main execution flow (Orchestration).
  • src/lib/viem.ts: Viem client initialization, approve/place/claim/simulate logic.
  • src/utils/constants.ts: Addresses, ABIs, RPC URLs, and Chain configurations.
  • src/utils/helpers.ts: CLI parsing, GraphQL fetching, price index adjustments, etc.

Setup

Clone the repository and set up your environment variables. The tutorial code is located in the tutorial folder.

cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

Paste your private key from Metamask (or your preferred wallet).

PRIVATE_KEY=0xYOUR_PRIVATE_KEY
SEPOLIA_RPC_URL=https://0xrpc.io/sep
Enter fullscreen mode Exit fullscreen mode

Next, install the dependencies:

bun i
Enter fullscreen mode Exit fullscreen mode

Running the Demo

Execute the following command to start the trade lifecycle:

bun run dev
Enter fullscreen mode Exit fullscreen mode

If everything is configured correctly, you should see output like this:

Sera Protocol - Order Lifecycle Demo (Bun + TypeScript + viem)

Wallet: 0x51908F598A5e0d8F1A3bAbFa6DF76F9704daD072
RPC: https://0xrpc.io/sep
Market: MYRC/AUDD
Latest: index=20260, price=368386000000
Balances: MYRC=10000000, AUDD=9998000
Depth: bids=2, asks=1
Top of book: bestBidIndex=20260, bestAskIndex=20262
Your open orders (before): 2
Claim target: isBid=true, priceIndex=20260, orderIndex=1, claimable=0
Claimed proceeds: 0x6d78b2c3a6a9b1669fcd2b97a763cee0a5ddb315c5ae82cb73395cfd777cf421
Enter fullscreen mode Exit fullscreen mode

Code Deep Dive

1. lib/viem.ts

This file contains the functions for initializing Viem clients and interacting with smart contract methods.

import {
  createPublicClient,
  createWalletClient,
  http,
  type Address,
  type Hex,
  type PrivateKeyAccount,
} from "viem";
import {
  ERC20_ABI,
  ROUTER_ABI,
  ROUTER_ADDRESS,
  RPC_URL,
  UINT16_MAX,
  UINT64_MAX,
  sepolia,
} from "../utils/constants";
import type { OpenOrder } from "../utils/helpers";

/**
 * Initializes Viem clients.
 * The Public Client is used for reading chain data, 
 * while the Wallet Client is used for sending signed transactions.
 */
export function createViemClients(account: PrivateKeyAccount) {
  const publicClient = createPublicClient({
    chain: sepolia,
    transport: http(RPC_URL),
  });

  const walletClient = createWalletClient({
    account,
    chain: sepolia,
    transport: http(RPC_URL),
  });

  return { publicClient, walletClient };
}

/**
 * Utility function to approve ERC20 tokens if needed.
 */
export async function approveTokenIfNeeded(args: {
  publicClient: ReturnType<typeof createPublicClient>;
  walletClient: ReturnType<typeof createWalletClient>;
  account: PrivateKeyAccount;
  tokenAddress: Address;
  spender: Address;
  amount: bigint;
}): Promise<Hex | null> {
  const { publicClient, walletClient, account, tokenAddress, spender, amount } = args;

  const allowance = await publicClient.readContract({
    address: tokenAddress,
    abi: ERC20_ABI,
    functionName: "allowance",
    args: [account.address, spender],
  });

  if (allowance >= amount) {
    return null;
  }

  const approvalHash = await walletClient.writeContract({
    address: tokenAddress,
    abi: ERC20_ABI,
    functionName: "approve",
    args: [spender, amount],
    account,
    chain: sepolia,
  });

  await publicClient.waitForTransactionReceipt({ hash: approvalHash });
  return approvalHash;
}

/**
 * Function to place a Limit Bid.
 */
export async function placeLimitBid(args: {
  publicClient: ReturnType<typeof createPublicClient>;
  walletClient: ReturnType<typeof createWalletClient>;
  account: PrivateKeyAccount;
  market: Address;
  priceIndex: number;
  rawAmount: bigint;
}): Promise<Hex> {
  const { publicClient, walletClient, account, market, priceIndex, rawAmount } = args;

  // Validation
  if (priceIndex < 0 || priceIndex > UINT16_MAX) {
    throw new Error(`priceIndex out of uint16 range: ${priceIndex}`);
  }
  if (rawAmount <= 0n || rawAmount > UINT64_MAX) {
    throw new Error(`rawAmount out of uint64 range: ${rawAmount.toString()}`);
  }

  const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
  const callArgs = [
    {
      market,
      deadline,
      claimBounty: 0,
      user: account.address,
      priceIndex,
      rawAmount,
      postOnly: true, // Safety flag to ensure the order is placed on the book
      useNative: false,
      baseAmount: 0n,
    },
  ] as const;

  // Simulation check before execution
  try {
    await publicClient.simulateContract({
      address: ROUTER_ADDRESS,
      abi: ROUTER_ABI,
      functionName: "limitBid",
      args: callArgs,
      account,
      chain: sepolia,
      value: 0n,
    });
  } catch (error) {
    const reason = error instanceof Error ? error.message : String(error);
    if (reason.includes("0xe450d38c")) {
      throw new Error("Simulation failed: ERC20InsufficientBalance.");
    }
    throw new Error(`Simulation failed: ${reason}`);
  }

  const txHash = await walletClient.writeContract({
    address: ROUTER_ADDRESS,
    abi: ROUTER_ABI,
    functionName: "limitBid",
    args: callArgs,
    account,
    chain: sepolia,
    value: 0n,
  });

  const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
  console.log(`Order placed in block ${receipt.blockNumber}`);
  return txHash;
}
Enter fullscreen mode Exit fullscreen mode

2. index.ts

This is the core script that orchestrates the entire flow from fetching market info to executing the order.

/**
 * Main Execution Logic:
 * 1. Fetch Market Info
 * 2. Get Order Book Depth
 * 3. Check Existing User Orders
 * 4. Place a Limit Order
 * 5. Monitor Order Status via GraphQL
 * 6. Claim Profits (if available)
 */
async function main() {
  console.log("Sera Protocol - Order Lifecycle Demo
");
  const cli = parseCliOptions(process.argv.slice(2));

  const privateKey = requirePrivateKey(PRIVATE_KEY);
  const account = privateKeyToAccount(privateKey);
  const { publicClient, walletClient } = createViemClients(account);

  // 1. Get market info
  const market = await getMarketInfo(MARKET_ADDRESS);
  console.log(`Market: ${market.baseToken.symbol}/${market.quoteToken.symbol}`);

  // 2. Get order book depth
  const depth = await getOrderBook(MARKET_ADDRESS);
  console.log(`Top of book: bestBidIndex=${depth.bids[0]?.priceIndex}, bestAskIndex=${depth.asks[0]?.priceIndex}`);

  // 3. Place a limit order
  const priceIndex = resolvePostOnlyBidPriceIndex({
    desiredPriceIndex: cli.priceIndex ?? Number(market.latestPriceIndex) - 100,
    bids: depth.bids,
    asks: depth.asks,
  });

  const orderTx = await placeLimitBid({
    publicClient,
    walletClient,
    account,
    market: MARKET_ADDRESS,
    priceIndex,
    rawAmount: cli.rawAmount ?? 1000n,
  });
  console.log(`Order Transaction: ${orderTx}`);

  // 4. Monitor via GraphQL
  console.log("Monitoring order status...");
  // ... (Polling logic to check status)

  // 5. Claim proceeds
  // ... (Claim logic if rawFilledAmount > 0)
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

That’s all for this tutorial!

By completing this hands-on guide, you’ve mastered the fundamental lifecycle of trading on Sera Protocol—from querying market states to executing and claiming orders via code.

This foundation is crucial for anyone looking to build automated bots, arbitrage tools, or custom interfaces for the next generation of on-chain FX.

Happy building!

Top comments (0)