#!/usr/bin/env python3
"""
Sera Protocol - Order Lifecycle Demo
This script demonstrates the complete order lifecycle on Sera Protocol:
1. Query available markets (GraphQL)
2. Get current order book depth (GraphQL)
3. Place a limit order (Smart Contract)
4. Monitor order status (GraphQL)
5. Claim proceeds when filled (Smart Contract)
"""
import os
import sys
import time
import requests
from decimal import Decimal
from typing import Optional, Dict, Any, List
from dotenv import load_dotenv
from web3 import Web3
from eth_account import Account
# =============================================================================
# CONFIGURATION
# =============================================================================
load_dotenv()
PRIVATE_KEY = os.getenv("PRIVATE_KEY")
RPC_URL = os.getenv("SEPOLIA_RPC_URL", "https://0xrpc.io/sep")
# Sera Protocol contract addresses (Sepolia testnet)
ROUTER_ADDRESS = "0x82bfe1b31b6c1c3d201a0256416a18d93331d99e"
# EURC/XSGD market - a live stablecoin pair with active trading
MARKET_ADDRESS = "0x2e4a11c7711c6a69ac973cbc40a9b16d14f9aa7e"
# GraphQL API endpoint
SUBGRAPH_URL = "https://api.goldsky.com/api/public/project_cmicv6kkbhyto01u3agb155hg/subgraphs/sera-pro/1.0.9/gn"
# Minimal ABIs
ROUTER_ABI = [
{
"name": "limitBid",
"type": "function",
"stateMutability": "payable",
"inputs": [{
"name": "params",
"type": "tuple",
"components": [
{"name": "market", "type": "address"},
{"name": "deadline", "type": "uint64"},
{"name": "claimBounty", "type": "uint32"},
{"name": "user", "type": "address"},
{"name": "priceIndex", "type": "uint16"},
{"name": "rawAmount", "type": "uint64"},
{"name": "postOnly", "type": "bool"},
{"name": "useNative", "type": "bool"},
{"name": "baseAmount", "type": "uint256"},
]
}],
"outputs": [{"name": "", "type": "uint256"}]
}
]
ERC20_ABI = [
{"name": "approve", "type": "function", "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": [{"name": "", "type": "bool"}]},
{"name": "allowance", "type": "function", "stateMutability": "view", "inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}]},
]
# =============================================================================
# GRAPHQL HELPERS
# =============================================================================
def query_subgraph(query: str, variables: Optional[Dict] = None) -> Dict[str, Any]:
"""Execute a GraphQL query."""
response = requests.post(
SUBGRAPH_URL,
json={"query": query, "variables": variables or {}},
headers={"Content-Type": "application/json"}
)
result = response.json()
if "errors" in result:
raise Exception(f"GraphQL Error: {result['errors'][0]['message']}")
return result["data"]
def get_market_info(market_id: str) -> Dict[str, Any]:
"""Fetch market information."""
query = """
query GetMarket($id: ID!) {
market(id: $id) {
id
quoteToken { id symbol decimals }
baseToken { id symbol decimals }
quoteUnit
minPrice
tickSpace
latestPrice
latestPriceIndex
}
}
"""
return query_subgraph(query, {"id": market_id.lower()})["market"]
def get_order_book(market_id: str) -> Dict[str, List]:
"""Fetch order book depth."""
query = """
query GetDepth($market: String!) {
bids: depths(
where: { market: $market, isBid: true, rawAmount_gt: "0" }
orderBy: priceIndex, orderDirection: desc, first: 10
) { priceIndex price rawAmount }
asks: depths(
where: { market: $market, isBid: false, rawAmount_gt: "0" }
orderBy: priceIndex, orderDirection: asc, first: 10
) { priceIndex price rawAmount }
}
"""
return query_subgraph(query, {"market": market_id.lower()})
def get_user_orders(user: str, market_id: str) -> List[Dict]:
"""Fetch user's orders."""
query = """
query GetOrders($user: String!, $market: String!) {
openOrders(
where: { user: $user, market: $market }
orderBy: createdAt, orderDirection: desc, first: 20
) {
id priceIndex isBid rawAmount rawFilledAmount claimableAmount status orderIndex
}
}
"""
return query_subgraph(query, {"user": user.lower(), "market": market_id.lower()})["openOrders"]
# =============================================================================
# CONTRACT HELPERS
# =============================================================================
def approve_token(w3, account, token_address, spender, amount):
"""Approve token spending."""
token = w3.eth.contract(address=Web3.to_checksum_address(token_address), abi=ERC20_ABI)
if token.functions.allowance(account.address, Web3.to_checksum_address(spender)).call() >= amount:
return None
tx = token.functions.approve(Web3.to_checksum_address(spender), amount).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address, 'pending'),
"gas": 100000,
"gasPrice": int(w3.eth.gas_price * 1.2)
})
signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
w3.eth.wait_for_transaction_receipt(tx_hash)
return tx_hash.hex()
def place_limit_bid(w3, account, market, price_index, raw_amount):
"""Place a limit bid order."""
router = w3.eth.contract(address=Web3.to_checksum_address(ROUTER_ADDRESS), abi=ROUTER_ABI)
params = (
Web3.to_checksum_address(market),
int(time.time()) + 3600, # deadline
0, # claimBounty
account.address,
price_index,
raw_amount,
True, # postOnly
False, # useNative
0 # baseAmount
)
tx = router.functions.limitBid(params).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address, 'pending'),
"gas": 500000,
"gasPrice": int(w3.eth.gas_price * 1.2),
"value": 0
})
signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
return receipt
# =============================================================================
# MAIN
# =============================================================================
def main():
print("Sera Protocol - Order Lifecycle Demo\n")
# Setup
w3 = Web3(Web3.HTTPProvider(RPC_URL))
account = Account.from_key(PRIVATE_KEY)
print(f"Wallet: {account.address}")
# 1. Get market info
market = get_market_info(MARKET_ADDRESS)
print(f"Market: {market['baseToken']['symbol']}/{market['quoteToken']['symbol']}")
# 2. Get order book
depth = get_order_book(MARKET_ADDRESS)
print(f"Bids: {len(depth['bids'])}, Asks: {len(depth['asks'])}")
# 3. Check existing orders
orders = get_user_orders(account.address, MARKET_ADDRESS)
print(f"Your orders: {len(orders)}")
# 4. Place a limit order
price_index = max(1, int(market["latestPriceIndex"]) - 100)
raw_amount = 1000
approve_token(w3, account, market["quoteToken"]["id"], ROUTER_ADDRESS, raw_amount * int(market["quoteUnit"]))
receipt = place_limit_bid(w3, account, MARKET_ADDRESS, price_index, raw_amount)
print(f"Order placed in block {receipt['blockNumber']}")
# 5. Verify
time.sleep(3)
updated_orders = get_user_orders(account.address, MARKET_ADDRESS)
print(f"Updated orders: {len(updated_orders)}")
if __name__ == "__main__":
main()