AXIS Docs

DevelopersContract reference

Events

Contract events emitted by AXIS: topics, data layout, ordering within a call and decoding from Stellar RPC.

Format#

  • The first topic is the event name as a Symbol (trade, swap, new, mod, skip, refresh, freeze, config). There is no other prefix topic. Asset addresses follow as extra topics where listed.
  • trade, swap, new and mod use the vec data format: the data is a vec of the fields in the order listed below. skip, freeze and config carry a single value. refresh carries no data (void).
  • There are no trade or swap IDs on-chain. Indexers derive them from the ledger, the transaction and the event position. The AXIS indexer uses the event position as the trade and swap ID.
  • A failed call emits nothing, so every AXIS event comes from a call that succeeded.
  • Nothing is emitted when an order expires. See Expiry.
  • Token contracts emit their own transfer and approve events in the same transactions. They are not AXIS events but they interleave with them (see Event ordering).
Event Topics Data Emitted by
trade ["trade", selling, buying] [order, taker, maker, sold, bought, left] trade, swap, crossfill
swap ["swap", selling, buying] [trader, sold, bought] swap
new ["new", selling, buying] [id, owner, price, amount, expires] trade with Limit
mod ["mod"] [id, price, amount, expires] update
skip ["skip"] u128 order ID trade, swap, crossfill
refresh ["refresh", base, quote] none requote, subsidize
freeze ["freeze"] bool freeze
config ["config"] Config constructor, delegate, set_oracle, set_floor, set_listing_min_days, set_ledger_time

Event reference#

trade#

Topics: ["trade", selling: Address, buying: Address], the assets sold and bought by the taker.

# Field Type Meaning
0 order u128 Filled order.
1 taker Address Account on the taking side (see below).
2 maker Address Owner of the filled order.
3 sold i128 Amount of selling the taker paid. The maker received it.
4 bought i128 Amount of buying the taker received. It came out of the order.
5 left i128 Order amount after the fill. 0 when the order was filled in full or the rest became dust, in which case the order was removed.

When: once per fill, in trade, swap and crossfill. A fill emits no mod: left is the order's new amount.

Semantics: the fill price seen by the taker is sold / bought. taker is the trader for trade and for the first hop of a swap, the AXIS contract for later swap hops, and the owner of the taker order for the maker fills of a crossfill. A crossfill also emits one trade for the taker order itself, with the caller as taker, the owner as maker, reversed topics ["trade", order.buying, order.selling], sold set to what the owner received at its own price and bought set to what the owner paid. It reads like a normal fill of that order. The caller's surplus shows only in the token transfer from the AXIS contract.

Size: about 320 bytes. With the maker's two token transfer events, a fill from a distinct maker costs about 792 bytes of the 16,384-byte event limit, hence 20 fills per call. See Resource limits and costs.

swap#

Topics: ["swap", selling: Address, buying: Address], the trader's input asset and the asset of the last step.

# Field Type Meaning
0 trader Address Account that called swap.
1 sold i128 Total amount of selling the trader paid.
2 bought i128 Total amount of buying the trader received.

When: once per successful swap, after the fills of every hop. The fills themselves are trade events.

Size: about 236 bytes.

new#

Topics: ["new", selling: Address, buying: Address], the assets of the new order.

# Field Type Meaning
0 id u128 Order ID.
1 owner Address Maker.
2 price i128 buying per 1 selling, 18 decimals.
3 amount i128 Amount of selling on offer.
4 expires u64 Expiration, UNIX seconds, 0 for none.

When: a Limit trade stores its remainder, after the fill events of the same trade.

Semantics: the order is always sell-equivalent. The remainder of a Buy trade appears as an order selling the trade's selling asset at ceil(10^36 / price). A new event can carry the ID of an expired order whose nonce was reused: the new order replaces it. Note the field order, price comes before amount.

Size: about 268 bytes.

mod#

Topics: ["mod"]. There are no asset topics: look the order up by ID.

# Field Type Meaning
0 id u128 Order ID.
1 price i128 New price, or the last price for a removal.
2 amount i128 New amount. 0 means the order was removed.
3 expires u64 New expiration, or the last expiration for a removal.

When: update changes or removes an order.

Semantics: with amount > 0 the order now has these values. This also revives an expired order. With amount = 0 the owner removed the order, and price and expires are the values it had. Fills and expiry never emit mod.

Size: about 148 bytes, which allows about 110 orders per update call.

skip#

Topics: ["skip"]. Data: a single u128, the order ID.

When: a listed order was not executed because its maker could not settle the fill: the backing left did not cover it, the maker could not receive the taker's asset, or the maker's asset could not be collected into the contract (trade and crossfill only, a swap fails instead). In a crossfill the event also flags the taker order when its owner has no backing or cannot receive. A skip always points at the order's owner, see Who is at fault.

Semantics: the order is left unchanged. Indexers recheck the maker's backing and routers stop proposing the maker's orders until the backing is restored (see Effective depth). IDs skipped silently emit nothing: missing, expired and duplicate IDs, orders of another pair, orders priced worse than the limit, fills that round to zero, and crossfill fills that would leave the taker order's owner short.

Size: about 84 bytes. Twenty fills leave room for 3 skip events, so routers budget listed orders, not only fills.

refresh#

Topics: ["refresh", base: Address, quote: Address], the market assets in canonical order. Data: none (void, which scValToNative decodes to null).

When: every successful oracle check of a market: requote on an existing market and every subsidize, including the one that opens the market.

Semantics: marks the time of the last verification. The first refresh of a pair announces a new market. The event does not say whether the listing changed: read the record with the market view.

Size: about 152 bytes.

freeze#

Topics: ["freeze"]. Data: a single bool, whether trading is blocked after the call.

When: every freeze call by the safety admin, also when the state does not change.

config#

Topics: ["config"]. Data: a single Config map (safety_admin, oracle, listing_min_days, market_listing_fee, min_trade_size, ledger_time).

When: the constructor, delegate, set_oracle, set_floor, set_listing_min_days and set_ledger_time. The data is the full configuration after the call.

Event ordering#

Events appear in the order the contract emits them, interleaved with the token events of the transfers.

Settlement of a fill#

The contract settles maker by maker, in its address order (the order of the encoded addresses, not the order of the listed IDs). For each maker:

  1. Token transfer of the maker leg: the maker's asset from the maker to the AXIS contract.
  2. Token transfer of the taker leg: the taker's asset to the maker, from the trader, from the AXIS contract on later swap hops, or from the taker order's owner in a crossfill.
  3. One trade per fill of this maker, in the order the IDs were listed.

A maker with several listed orders gets one transfer per leg for all of them. After the last maker, the contract forwards everything the makers delivered to the trader in one token transfer from the AXIS contract (in a trade and on the last hop of a swap, never in a crossfill). It then emits one skip per skipped order: first the orders refused while matching, in list order, then the orders of makers whose asset could not be collected.

Trades#

  1. Token approve when the call carries approve.
  2. The settlement events above, maker by maker.
  3. Token transfer from the AXIS contract to the trader, the forward of everything bought.
  4. The skip events.
  5. new when a Limit remainder is stored.

A FillOrKill trade that does not execute in full fails and emits nothing.

Swaps#

Each hop settles like a trade, then emits its own skip events. Every hop's makers deliver to the AXIS contract. On the first hop the trader is the taker. On later hops the AXIS contract is the taker and pays the makers out of what the previous hop delivered. The last hop ends with the forward of its output to the trader, and one swap event closes the call. A two-hop swap from XLM to EURC through USDC, with makers A and B on the first hop and maker C on the second, emits in this order:

# Emitted by Event
1 XLM token approve from the trader to AXIS (only with an in-call approval)
2 USDC token transfer from maker A to AXIS
3 XLM token transfer from the trader to maker A
4 AXIS trade (XLM, USDC), taker = trader, maker = A
5 USDC token transfer from maker B to AXIS
6 XLM token transfer from the trader to maker B
7 AXIS trade (XLM, USDC), taker = trader, maker = B
8 EURC token transfer from maker C to AXIS
9 USDC token transfer from AXIS to maker C
10 AXIS trade (USDC, EURC), taker = AXIS contract, maker = C
11 EURC token transfer from AXIS to the trader
12 AXIS swap (XLM, EURC)

The order of A and B follows their addresses. The network's own fee events are not shown.

Crossfills#

  1. Per maker, in address order: token transfer of the taker order's buying asset from the maker to AXIS, token transfer of its selling asset from the owner to the maker, then one trade per maker fill with the owner as taker.
  2. One skip per listed order whose maker could not settle.
  3. Token transfer from AXIS to the owner (the amount owed at the order's price), then from AXIS to the caller (the surplus, only when positive).
  4. One trade for the taker order, with the caller as taker and reversed topics.

When the owner of the taker order cannot back it or cannot receive, the call emits a single skip with the taker order ID and nothing else.

Updates#

  1. Token approve per entry in approvals, in list order.
  2. One mod per processed entry, in list order. Missing IDs emit nothing and a repeated ID emits once.

Expiry#

An order expires silently when the ledger time reaches expires. The contract compares expires with the ledger close time. The AXIS indexer checks it against its own clock on a timer, so expect a few seconds of skew. An expired order can still come back: update revives it (mod with amount > 0) or removes it (mod with amount = 0), and a new with the same ID replaces it. Keep expired orders until one of these happens or the entry is archived. See Order lifecycle and rent.

Decoding with Stellar RPC#

Fetch events with getEvents and decode them with scValToNative. Amounts and IDs decode to bigint, addresses to strings.

import {rpc, scValToNative} from '@stellar/stellar-sdk'

const AXIS = 'CA6P26K4QNNIMTYP22ILTSCXQQDEKNWJIZJPC34YEZYOLBNT7YUPX7XS'
const server = new rpc.Server('https://soroban-testnet.stellar.org')

function decodeAxisEvent(event) {
    const [name, ...topics] = event.topic.map(topic => scValToNative(topic))
    const data = scValToNative(event.value)
    switch (name) {
        case 'trade': {
            const [order, taker, maker, sold, bought, left] = data
            return {name, selling: topics[0], buying: topics[1], order, taker, maker, sold, bought, left}
        }
        case 'swap': {
            const [trader, sold, bought] = data
            return {name, selling: topics[0], buying: topics[1], trader, sold, bought}
        }
        case 'new': {
            const [id, owner, price, amount, expires] = data
            return {name, selling: topics[0], buying: topics[1], id, owner, price, amount, expires}
        }
        case 'mod': {
            const [id, price, amount, expires] = data
            return {name, id, price, amount, expires, removed: amount === 0n}
        }
        case 'skip':
            return {name, order: data}
        case 'refresh':
            return {name, base: topics[0], quote: topics[1]}
        case 'freeze':
            return {name, frozen: data}
        case 'config':
            return {name, config: data}
        default:
            return {name, topics, data}
    }
}

const {sequence} = await server.getLatestLedger()
const {events} = await server.getEvents({
    startLedger: sequence - 720,
    filters: [{type: 'contract', contractIds: [AXIS]}],
    limit: 200
})
for (const event of events) {
    console.log(event.ledger, event.txHash, decodeAxisEvent(event))
}

To fetch one event type, filter on the first topic. A filter matches events with exactly as many topics as it has entries, so trade needs three:

import {xdr} from '@stellar/stellar-sdk'

const tradesOnly = [{
    type: 'contract',
    contractIds: [AXIS],
    topics: [[xdr.ScVal.scvSymbol('trade').toXDR('base64'), '*', '*']]
}]

RPC servers keep events for a limited window. To follow the book continuously, embed the indexer or subscribe to the WebSocket API instead of polling raw events.