Interact with OISY TRADE
Walkthrough of the core OISY TRADE flows against the staging canister, using only the icp CLI:
- List trading pairs
- Approve and deposit
- Place a limit order
- Check order status
- Withdraw
The walkthrough uses two configurable identities — one for buy-side operations, one for sell-side — so the flow can exercise both halves of a matched trade. Run every command below from the same shell: steps share exported variables (BUYER_IDENTITY, SELLER_IDENTITY, BASE_LEDGER, OISY_TRADE, …).
Prerequisites
icpCLI installed and onPATH- Run these commands from the project root so
--environment stagingresolves theoisy_tradecanister name (defined inicp.yaml) - Two identities configured via
icp identity neworicp identity import— one acts as the buyer, one as the seller. They may be the same identity if you just want to see the commands run end-to-end against yourself; using two distinct identities lets you demonstrate an actual matched trade.
Identities
Export a buyer and a seller identity. OISY TRADE keys internal balances and order ownership off the caller principal, so every signing call targets one of these two.
export BUYER_IDENTITY=<buyer-name>
export SELLER_IDENTITY=<seller-name>
icp identity principal --identity "$BUYER_IDENTITY"
icp identity principal --identity "$SELLER_IDENTITY"
Limit Orders 101
- Trading Pair — two token ledgers: the base (what you’re buying or selling) and the quote (what prices are denominated in).
SOL / ETHmeans trading SOL priced in ETH. - Quantity — amount of the base token, in its base units (e.g. 1 SOL = 109 units at 9 decimals). Must be a positive multiple of
lot_size. - Price — quote base units per one whole base token (i.e. per
10^base_decimalsbase units). Must be a positive multiple oftick_size. A fill ofquantityatpricesettlesprice × quantity / 10^base_decimalsquote-token base units. - Side —
Buy(bid) reservesprice × quantity / 10^base_decimalsof the quote token from your free balance;Sell(ask) reservesquantityof the base token. The order fills when a crossing order arrives, otherwise it rests in the book until canceled. - Notional — an order’s value,
price × quantity / 10^base_decimalsquote base units, must clear the pair’smin_notionaland stay under its optionalmax_notional.
List trading pairs
get_trading_pairs returns every listed pair along with its tick_size (price must be a positive multiple) and lot_size (quantity must be a positive multiple, in base-token base units).
icp canister call oisy_trade get_trading_pairs '()' --environment staging --query --identity anonymous
Pick a pair
Copy the base and quote ledger principals from the output above and export them. The pair parameters below follow Binance’s SOLETH market for the ckDevnetSOL / ckSepoliaETH pair (see the admin guide for the derivation); confirm them against the get_trading_pairs output and adjust if you picked a different pair.
export BASE_LEDGER=la34w-haaaa-aaaar-qb5na-cai # ckDevnetSOL (base_decimals = 9)
export QUOTE_LEDGER=apia6-jaaaa-aaaar-qabma-cai # ckSepoliaETH (quote_decimals = 18)
export OISY_TRADE=proc5-daaaa-aaaar-qb5va-cai # staging OISY TRADE canister
# Pair parameters, from Binance SOLETH (tickSize 0.00001 ETH/SOL, stepSize 0.001 SOL):
export TICK_SIZE=10_000_000_000_000 # 0.00001 × 10^18 = 10^13
export LOT_SIZE=1_000_000 # 0.001 × 10^9 = 10^6
# This walkthrough trades 0.1 SOL at 0.05 ETH/SOL:
export PRICE=50_000_000_000_000_000 # 0.05 ETH/SOL = 5000 × tick_size
export QUANTITY=100_000_000 # 0.1 SOL = 100 × lot_size
The pair’s notional bounds (admin-set, from Binance SOLETH’s NOTIONAL filter) are min_notional = 0.001 ETH = 1_000_000_000_000_000 and max_notional = 9_000_000 ETH = 9_000_000_000_000_000_000_000_000 quote base units. Our order’s notional is PRICE × QUANTITY / 10^9 = 5_000_000_000_000_000 (0.005 ETH), comfortably between them.
Approve and deposit
Every limit order reserves one side of the pair before the matching engine can fill it:
- Sell orders reserve
quantitybase tokens → the seller needs base in their on-OISY-TRADE free balance - Buy orders reserve
price × quantity / 10^base_decimalsquote tokens → the buyer needs quote in their on-OISY-TRADE free balance
So which identity approves and deposits which token depends on the side it plans to trade.
deposit moves tokens by calling icrc2_transfer_from on the token ledger, so OISY TRADE must first be approved to spend on the caller’s behalf. Two fees are charged to the on-ledger balance:
icrc2_approvecharges the ledger fee onceicrc2_transfer_from(triggered later bydeposit) charges the ledger fee again, on top of the amount transferred
So to deposit N, approve at least N + ledger_fee and hold at least N + 2 × ledger_fee on the ledger. Fees vary widely per ledger — this walkthrough assumes ckDevnetSOL = 50 base units (≈ 5 × 10-8 SOL) and ckSepoliaETH = 10_000_000_000 (= 10-8 ETH). If the live icrc1_fee values below differ from these, adjust the literal amount fields in the approve / deposit calls accordingly before running them:
# Seller — base ledger (needs base to sell)
icp token "$BASE_LEDGER" balance --network ic --identity "$SELLER_IDENTITY"
icp canister call "$BASE_LEDGER" icrc1_fee '()' --query --identity anonymous --network ic
# Buyer — quote ledger (needs quote to buy)
icp token "$QUOTE_LEDGER" balance --network ic --identity "$BUYER_IDENTITY"
icp canister call "$QUOTE_LEDGER" icrc1_fee '()' --query --identity anonymous --network ic
Then approve and deposit — run each subsection as the appropriate identity. icrc2_approve calls the token ledger directly (--network ic); deposit targets OISY TRADE on staging (--environment staging).
Seller — approve + deposit base
Deposits QUANTITY base-token base units (= 100 × LOT_SIZE = 0.1 SOL; a base unit is the token’s smallest indivisible unit, not to be confused with the base token in the trading pair), enough for one sell at quantity = $QUANTITY. Approval is QUANTITY + ckDevnetSOL_fee = 100_000_000 + 50.
icp canister call "$BASE_LEDGER" icrc2_approve --args-file /dev/stdin --network ic --identity "$SELLER_IDENTITY" <<EOF
(
record {
spender = record { owner = principal "$OISY_TRADE"; subaccount = null };
amount = 100_000_050 : nat
}
)
EOF
icp canister call oisy_trade deposit --args-file /dev/stdin --environment staging --identity "$SELLER_IDENTITY" <<EOF
(
record {
token_id = record { ledger_id = principal "$BASE_LEDGER" };
amount = 100_000_000 : nat
}
)
EOF
Buyer — approve + deposit quote
Deposits the order’s notional price × quantity / 10^base_decimals = PRICE × QUANTITY / 10^9 = 5_000_000_000_000_000 quote-base-units (= 0.005 ETH), enough for one buy at price = $PRICE, quantity = $QUANTITY. Approval is notional + ckSepoliaETH_fee = 5_000_000_000_000_000 + 10_000_000_000. The buyer’s ckSepoliaETH on-ledger balance must be at least notional + 2 × fee = 5_000_020_000_000_000 (≈ 0.005 ETH) to cover both the icrc2_approve fee and the icrc2_transfer_from fee on top of the deposit itself.
icp canister call "$QUOTE_LEDGER" icrc2_approve --args-file /dev/stdin --network ic --identity "$BUYER_IDENTITY" <<EOF
(
record {
spender = record { owner = principal "$OISY_TRADE"; subaccount = null };
amount = 5_000_010_000_000_000 : nat
}
)
EOF
icp canister call oisy_trade deposit --args-file /dev/stdin --environment staging --identity "$BUYER_IDENTITY" <<EOF
(
record {
token_id = record { ledger_id = principal "$QUOTE_LEDGER" };
amount = 5_000_000_000_000_000 : nat
}
)
EOF
Candid’s record literals use
{/}, which collide with bash variable-expansion rules. Passing the candid via--args-file /dev/stdinand a heredoc sidesteps every shell-quoting pitfall — the unquotedEOFterminator still expands$VARinside the body.
Check on-OISY-TRADE balances
OISY TRADE tracks balance per (caller, token):
free— available for new orders or withdrawalreserved— locked by open orders
# Seller's base balance
icp canister call oisy_trade get_balances --args-file /dev/stdin --environment staging --query --identity "$SELLER_IDENTITY" <<EOF
(opt vec { variant { ById = record { ledger_id = principal "$BASE_LEDGER" } } })
EOF
# Buyer's quote balance
icp canister call oisy_trade get_balances --args-file /dev/stdin --environment staging --query --identity "$BUYER_IDENTITY" <<EOF
(opt vec { variant { ById = record { ledger_id = principal "$QUOTE_LEDGER" } } })
EOF
Place a limit order
price must be a positive multiple of TICK_SIZE; quantity a positive multiple of LOT_SIZE.
Submission is asynchronous: the call returns an order ID immediately, the matching engine processes it on the next canister tick. Two resting orders with crossing prices fill as soon as they meet.
Sell (as seller)
icp canister call oisy_trade add_limit_order --args-file /dev/stdin --environment staging --identity "$SELLER_IDENTITY" <<EOF
(
record {
pair = record { base = principal "$BASE_LEDGER"; quote = principal "$QUOTE_LEDGER" };
side = variant { Sell };
price = $PRICE : nat;
quantity = $QUANTITY : nat
}
)
EOF
Paste the returned 32-char hex order ID:
export SELL_ORDER_ID=<paste-the-order-id-here>
Buy (as buyer)
Same price/quantity, so this order crosses the seller’s resting ask and fills both.
icp canister call oisy_trade add_limit_order --args-file /dev/stdin --environment staging --identity "$BUYER_IDENTITY" <<EOF
(
record {
pair = record { base = principal "$BASE_LEDGER"; quote = principal "$QUOTE_LEDGER" };
side = variant { Buy };
price = $PRICE : nat;
quantity = $QUANTITY : nat
}
)
EOF
export BUY_ORDER_ID=<paste-the-order-id-here>
Check order status
icp canister call oisy_trade get_my_orders --args-file /dev/stdin --environment staging --query --identity "$SELLER_IDENTITY" <<EOF
(opt record { filter = variant { ById = "$SELL_ORDER_ID" } })
EOF
icp canister call oisy_trade get_my_orders --args-file /dev/stdin --environment staging --query --identity "$BUYER_IDENTITY" <<EOF
(opt record { filter = variant { ById = "$BUY_ORDER_ID" } })
EOF
get_my_orders returns the caller’s orders; the ById filter selects a single order by id and returns it (or an empty vector if the caller does not own it). Each returned order carries a status field:
| Status | Meaning |
|---|---|
Pending | Accepted, awaiting the matching engine |
Open | Resting in the order book |
Filled | Fully filled |
Canceled | Canceled |
Both orders should reach Filled once the matching engine has ticked. The engine typically processes within a few seconds — if you see Pending, wait a moment and re-run the query.
Re-check balances to confirm the trade settled. The pre-trade balances from §4 (seller’s base, buyer’s quote) should now both read 0 — fees are charged on the asset each side receives, so the spent side nets to zero either way.
Each side pays a trading fee in the asset it receives, at the maker rate if its order was resting or the taker rate if it crossed. Here the seller’s sell rested (maker) and the buyer’s buy crossed (taker); fees round up, in the protocol’s favor:
- seller’s quote
free=notional − ⌈maker_bps × notional / 10_000⌉ - buyer’s base
free=quantity − ⌈taker_bps × quantity / 10_000⌉
This walkthrough assumes the pair is configured with maker_fee_bps = 0 and taker_fee_bps = 20 (0.2%):
| Side | Gross | Fee | Net free |
|---|---|---|---|
| Seller (quote, maker 0 bps) | 5_000_000_000_000_000 | 0 | 5_000_000_000_000_000 |
| Buyer (base, taker 20 bps) | 100_000_000 | 200_000 | 99_800_000 |
A pair’s fee rates are returned by get_trading_pairs, and the fees the canister has collected are queryable with get_fee_balances.
# Seller's quote (credited by the fill)
icp canister call oisy_trade get_balances --args-file /dev/stdin --environment staging --query --identity "$SELLER_IDENTITY" <<EOF
(opt vec { variant { ById = record { ledger_id = principal "$QUOTE_LEDGER" } } })
EOF
# Buyer's base (credited by the fill)
icp canister call oisy_trade get_balances --args-file /dev/stdin --environment staging --query --identity "$BUYER_IDENTITY" <<EOF
(opt vec { variant { ById = record { ledger_id = principal "$BASE_LEDGER" } } })
EOF
Withdraw
Debits amount from your on-OISY-TRADE free balance and sends amount − ledger_fee to your principal on the ledger. amount must be strictly greater than the current ledger fee — otherwise the call returns AmountTooSmall; re-check icrc1_fee if unsure. Only free funds are eligible; reserved funds (locked by open orders) aren’t withdrawable until the order fills or is canceled.
After the matched trade in §5 the seller now holds quote and the buyer now holds base. Each withdraws what they received:
Seller withdraws quote
Withdraws the seller’s full 5_000_000_000_000_000 quote-base-units balance; after the ckSepoliaETH fee (10_000_000_000), the seller receives 4_999_990_000_000_000 on-ledger.
icp canister call oisy_trade withdraw --args-file /dev/stdin --environment staging --identity "$SELLER_IDENTITY" <<EOF
(
record {
token_id = record { ledger_id = principal "$QUOTE_LEDGER" };
amount = 5_000_000_000_000_000 : nat
}
)
EOF
Buyer withdraws base
Withdraws the buyer’s full 99_800_000 base-base-units balance (the 100_000_000 quantity minus the 200_000 taker fee); after the ckDevnetSOL fee (50), the buyer receives 99_799_950 on-ledger.
icp canister call oisy_trade withdraw --args-file /dev/stdin --environment staging --identity "$BUYER_IDENTITY" <<EOF
(
record {
token_id = record { ledger_id = principal "$BASE_LEDGER" };
amount = 99_800_000 : nat
}
)
EOF