DevelopersContract reference
Types
Argument and record types of the AXIS contract, the order ID formula, the on-chain order encoding and the units of amounts and prices.
The definitions below are adapted from the contract source (order.rs, trade.rs, market.rs, pricing.rs). On the wire every type
is an XDR ScVal:
- Integer enums (
TradeDirection,OrderKind) areu32values. - Structs are
mapvalues keyed by the field names as symbols. Option<T>is the value itself orvoid.Vec<T>and tuples (the return values) arevecvalues.i128,u128andu64decode tobigintwithscValToNativefrom@stellar/stellar-sdk,u32to a number andAddressto aG...orC...string.
The JS client keeps the contract's snake_case field names in decoded records such as Config (listing_min_days,
market_listing_fee, min_trade_size) and uses camelCase in its own argument objects (liveUntil for live_until, sellingAmount for
selling_amount).
Enums#
TradeDirection#
#[repr(i16)]
pub enum TradeDirection {
// Sell a fixed amount of asset
Sell = 1,
// Buy a fixed amount of asset
Buy = 2,
}
| Variant | Value | Meaning |
|---|---|---|
Sell |
1 |
The trader spends a fixed amount of selling. price is the minimum buying per 1 selling. |
Buy |
2 |
The trader acquires a fixed amount of buying. price is the maximum selling per 1 buying. |
OrderKind#
#[repr(i16)]
pub enum OrderKind {
// Execute trade, create a limit order if not executed in full
Limit = 1,
// Execute trade without creating a limit order
Fill = 2,
// Execute trade, fail if it cannot be executed in full
FillOrKill = 3,
}
| Variant | Value | Meaning |
|---|---|---|
Limit |
1 |
Match the listed orders, then store the remainder as an order. Needs a market, and a fresh cached price while the minimum order value is enabled. |
Fill |
2 |
Match what the listed orders allow and stop (immediate-or-cancel). A market order in trader terms. |
FillOrKill |
3 |
Fill-or-Kill: match in full or fail with NotFilled (709). Nothing moves on failure. |
There are no separate immediate-or-cancel or post-only flags.
Trading types#
Order#
pub struct Order {
// Unique order identifier, derived from the owner and the client nonce
pub id: u128,
// Selling token address
pub selling: Address,
// Buying token address
pub buying: Address,
// Amount left to sell
pub amount: i128,
// Maker address
pub owner: Address,
// Order price (`buying` per 1 `selling`, 18 decimals)
pub price: i128,
// Expiration timestamp (0 = no expiration)
pub expires: u64,
}
| Field | Type | Unit | Meaning |
|---|---|---|---|
id |
u128 |
See order ID derivation. | |
selling |
Address |
Token the maker sells. | |
buying |
Address |
Token the maker receives. | |
amount |
i128 |
base units of selling |
Amount left to sell. |
owner |
Address |
Maker. | |
price |
i128 |
18 decimals | buying base units per 1 selling base unit, times 10^18. |
expires |
u64 |
UNIX seconds | 0 means no expiration. Otherwise the order is expired once expires <= now in ledger time. |
The order view returns this struct. Orders are always stored sell-equivalent: a Buy remainder becomes an order
selling the trade's selling asset (see trade). The ledger entry uses a smaller
positional encoding.
Approval#
pub struct Approval {
// Token the allowance is granted on
pub asset: Address,
// Absolute allowance amount
pub amount: i128,
// Ledger sequence the allowance lives until
pub live_until: u32,
}
| Field | Type | Unit | Meaning |
|---|---|---|---|
asset |
Address |
Token to approve, normally the selling asset of the call. |
|
amount |
i128 |
base units of asset |
New allowance of the AXIS contract. Absolute, not a delta. 0 revokes. |
live_until |
u32 |
ledger sequence | Expiration of the allowance entry. |
The contract calls approve(trader, AXIS, amount, live_until) on the token as a sub-invocation the trader signs. For
a Stellar Asset Contract, live_until may not exceed the network maximum entry TTL (about 180 days ahead) and may not
be in the past when amount is positive. The allowance reads as zero after it expires and has to be granted again. See
Settlement and allowances.
OrderUpdate#
pub struct OrderUpdate {
// Order ID
pub id: u128,
// New amount to sell, 0 removes the order
pub amount: i128,
// New order price (ignored when the order is removed)
pub price: i128,
// New expiration timestamp, 0 = no expiration (ignored when the order is removed)
pub expires: u64,
}
| Field | Type | Unit | Meaning |
|---|---|---|---|
id |
u128 |
Order to change. Missing IDs are skipped. | |
amount |
i128 |
base units of the order's selling |
New amount. 0 removes the order. |
price |
i128 |
18 decimals | New price. Ignored for a removal. |
expires |
u64 |
UNIX seconds | New expiration, 0 for none. Ignored for a removal. |
The selling and buying assets of an order never change.
TradeStep#
pub struct TradeStep {
// Asset to buy at this step
pub asset: Address,
// Maker order IDs to match
pub orders: Vec<u128>,
}
| Field | Type | Meaning |
|---|---|---|
asset |
Address |
Asset bought at this hop. The hop sells the previous step's asset, or selling for the first step. |
orders |
Vec<u128> |
Maker orders to match at this hop, in order. Orders of another pair are skipped. |
Market and configuration types#
Market#
pub struct Market {
// Base asset, the first of the pair in canonical order
pub base: MarketSide,
// Quote asset, the second of the pair in canonical order
pub quote: MarketSide,
// Creation timestamp
pub created: u64,
}
| Field | Type | Unit | Meaning |
|---|---|---|---|
base |
MarketSide |
Base asset, the first in canonical order. | |
quote |
MarketSide |
Quote asset, the second in canonical order. | |
created |
u64 |
UNIX seconds | When subsidize opened the market. |
There is one market per unordered pair. Canonical order compares the encoded ScAddress bytes of the two assets,
which is not the order of the C... strings. On Testnet, EURC (CCUU...) sorts before CETES (CC72...). The JS
helpers canonicalPair and compareAssets reproduce this order.
MarketSide#
pub struct MarketSide {
// Token contract address
pub asset: Address,
// Whether the asset is quoted by the oracle (with token decimals the valuation can handle)
pub listed: bool,
// Token decimals (fetched only for listed assets)
pub decimals: u32,
}
| Field | Type | Meaning |
|---|---|---|
asset |
Address |
Token contract. |
listed |
bool |
Whether the oracle quoted the asset at the last check (subsidize or requote) and its token decimals plus the oracle decimals stay within 37. |
decimals |
u32 |
Token decimals, read only for listed assets. 0 when not listed. |
Config#
pub struct Config {
// Account allowed to freeze the contract and change the configuration
pub safety_admin: Address,
// Reflector Beam price oracle contract address
pub oracle: Address,
// Days of price feeds a new market must buy, zero opens markets without a fee
pub listing_min_days: u32,
// Amount of XRF stroops burned by the market creator to provision the oracle price
// feeds for the listed market assets: the oracle's daily fee times `listing_min_days`
pub market_listing_fee: i128,
// Minimum value a trade must sell, in USD with 7 decimals (1 USD = `MIN_TRADE_SIZE_UNIT`),
// zero disables the limit
pub min_trade_size: i128,
// Expected average ledger close time in seconds
pub ledger_time: u32,
}
| Field | Type | Unit | Meaning | Testnet value |
|---|---|---|---|---|
safety_admin |
Address |
Holder of the six safety admin calls. | GBDCULE53LUPK4XHUCXBI35MAZFQHENMZ3JRKAJS2PPYBV646M6XKVHG |
|
oracle |
Address |
Reflector Beam oracle. | CDEIQLTE3Y3XQMYPSZFJC2OXIS67F4MURGP73NCE2C2NZZ22RNSPV7ZI |
|
listing_min_days |
u32 |
Days | Days of price feeds the listing fee buys, counted per asset. 90 at deployment, 0 to 255, set with set_listing_min_days. 0 opens markets without a fee. |
90 |
market_listing_fee |
i128 |
XRF base units | Oracle daily per-asset fee times listing_min_days, derived, never set directly. |
180000000000 (18,000 XRF) |
min_trade_size |
i128 |
USD, 7 decimals | Minimum order value. 0 disables it. |
10000 (0.001 USD) |
ledger_time |
u32 |
Seconds | Expected ledger close time, used to convert every lifetime the contract sets into ledgers. 5 at deployment, 1 to 20, set with set_ledger_time. |
5 |
The minimum order value applies to Limit trades (the whole amount) and to updates, valued at the cached oracle
price. See Minimum order value for the valuation rule and its errors.
Order ID derivation#
Order IDs are chosen by the client through a u64 nonce and computed off-chain. There is no on-chain view for them.
id = u128::from_be_bytes(sha256(xdr(ScVec[owner: Address, nonce: u64]))[0..16])
The hashed bytes are the XDR of the ScVal vector [owner, nonce]. The first 16 bytes of the SHA-256 digest, read
big-endian, form the full 128-bit ID. Consequences:
- IDs are not sequential and do not include the pair. The same nonce collides on every pair.
- A nonce must be unique among the owner's live orders, otherwise
tradefails withOrderExists(711). Once an order is removed or expired its nonce can be reused. A new order under the ID of a removed order starts a fresh entry at the network minimum lifetime. One under the ID of an expired order overwrites the old entry, whose lifetime is then extended as for anupdate. - The ID is known before the transaction is simulated, so clients can declare the new order entry in the footprint.
The JS client exports orderId. Its sell and buy methods generate the nonce themselves as
(Date.now() << 22) | 22 random bits.
import {orderId} from '@axis-markets/client'
const id = orderId('GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI', 12345n)
console.log(id) // 104716339259087188276464058217096427616n
The same computation with @stellar/stellar-sdk only:
import {hash, nativeToScVal} from '@stellar/stellar-sdk'
function computeOrderId(owner, nonce) {
const payload = nativeToScVal([owner, nonce], {type: ['address', 'u64']}).toXDR()
const digest = hash(payload) // SHA-256
let id = 0n
for (const byte of digest.subarray(0, 16)) {
id = (id << 8n) | BigInt(byte)
}
return id
}
Test vectors from the client test suite:
| Owner | Nonce | Order ID |
|---|---|---|
GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI |
0 |
87503119175631750522483667960259619256 |
GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI |
12345 |
104716339259087188276464058217096427616 |
GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI |
18446744073709551615 |
239482216881556756373704228044123948254 |
CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC |
777 |
260602579416723494792706352430735587842 |
Order storage encoding#
An order is a persistent contract data entry of the AXIS contract. The key is the raw u128 order ID. The value is a
positional Vec without the ID:
[owner: Address, selling: Address, buying: Address, amount: i128, price: i128, expires: u64]
expires is appended only when it is not zero, so an order without expiration has five elements. An order without
expiration takes about 172 to 176 bytes of value and about 250 bytes of ledger entry, and expires adds 12 bytes. The positional form keeps rent and write fees low (see
Order lifecycle and rent).
Read an order entry directly over RPC, for example to inspect its TTL:
import {rpc, scValToNative} from '@stellar/stellar-sdk'
import {orderLedgerKey} from '@axis-markets/client/footprint'
const AXIS = 'CA6P26K4QNNIMTYP22ILTSCXQQDEKNWJIZJPC34YEZYOLBNT7YUPX7XS'
const server = new rpc.Server('https://soroban-testnet.stellar.org')
const id = 137718931818025766751180795007231811965n
const {entries} = await server.getLedgerEntries(orderLedgerKey(AXIS, id))
if (entries.length) {
const [owner, selling, buying, amount, price, expires = 0n] = scValToNative(entries[0].val.contractData.val)
console.log({owner, selling, buying, amount, price, expires, liveUntil: entries[0].liveUntilLedgerSeq})
}
A raw read ignores expiration. The order view returns None once expires <= now, and matching skips the order.
Storage keys#
| Entry | Storage | Key | Value | Lifetime |
|---|---|---|---|---|
| Order | persistent | u128 order ID |
positional Vec |
Network minimum TTL at creation (about 120 days on mainnet, 7 days on Testnet). update extends it, and so does a new order written over an expired entry. |
| Market | persistent | DataKey::Market(base, quote) |
Market |
Extended to 120 days when fewer than 30 days are left whenever the contract reads it, and to 121 days when fewer than 120 are left on every Limit trade and every update that changes an order on it. |
| Price cache | temporary | asset Address |
PriceCache |
3 days, set on every write. |
| Configuration | instance | DataKey::Config |
Config |
Contract instance TTL. |
| Oracle decimals | instance | DataKey::OracleDecimals |
u32 |
Contract instance TTL. |
| Frozen flag | instance | DataKey::Frozen |
bool |
Absent until the first freeze call (reads as false). |
pub enum DataKey {
// Contract configuration (instance)
Config,
// Price oracle decimals (instance)
OracleDecimals,
// Withdrawal-only mode flag (instance)
Frozen,
// Market record for the canonically ordered asset pair (persistent)
Market(Address, Address),
}
pub struct PriceCache {
// Price in oracle decimals
pub price: i128,
// Price record timestamp (in seconds)
pub timestamp: u64,
// Price decimals of the oracle the price was read from
pub decimals: u32,
}
DataKey variants encode as vectors that start with the variant name: ["Config"], ["OracleDecimals"],
["Frozen"] and ["Market", base, quote] with the assets in canonical order. Build a market key like this:
import {Address, xdr} from '@stellar/stellar-sdk'
import {canonicalPair} from '@axis-markets/client'
function marketLedgerKey(contractId, x, y) {
const [base, quote] = canonicalPair(x, y)
return xdr.LedgerKey.contractData(new xdr.LedgerKeyContractData({
contract: Address.fromString(contractId).toScAddress(),
key: xdr.ScVal.scvVec([
xdr.ScVal.scvSymbol('Market'),
Address.fromString(base).toScVal(),
Address.fromString(quote).toScVal()
]),
durability: xdr.ContractDataDurability.persistent
}))
}
A cached price is usable while its oracle timestamp is at most 72 hours old and it was cached with the current
oracle's price decimals.
While the minimum order value is enabled, Limit trades and updates read the cache entries of both assets of the
pair, listed or not, so their footprint does not depend on the listing flags. Only requote and subsidize write the
cache (see Price cache). The TTL rules of every entry are in
TTL policy.
Price and amount units#
Amounts are i128 integer base units of a token. All Testnet assets (XLM, USDC, EURC, CETES) have 7 decimals, so 1
token is 10,000,000 base units ("stroops").
Prices are i128 fixed point numbers: buying base units per 1 selling base unit, times 10^18 (PRECISION in the
contract, PRICE_SCALE in the JS client). The accepted range is 1 to 10^36, anything else fails with
InvalidPrice (705). The contract never adjusts for token decimals. For a human price P in buying tokens per
selling token:
price = P * 10^(18 + buyingDecimals - sellingDecimals)
| Order | Human price | price argument |
|---|---|---|
| Sell XLM for USDC (7 and 7 decimals) | 0.25 USDC per XLM | 250000000000000000 |
Buy XLM with USDC, Buy direction, selling = USDC |
at most 0.25 USDC per XLM | 250000000000000000 |
| Sell USDC for XLM | 4 XLM per USDC | 4000000000000000000 |
| Sell a 6-decimal token for a 7-decimal token | 2 per token | 20000000000000000000 |
The same price seen from the other side of the market is floor(10^36 / price) (invertPrice in the JS client). The
contract rounds the other way, ceil(10^36 / price), when it stores a Buy remainder. An amount is
dust when it is worth less than one base unit of the counter asset at the order price,
floor(amount * price / 10^18) == 0. See Prices and rounding for the fill formulas.
Other units:
| Value | Type | Unit |
|---|---|---|
expires, created |
u64 |
UNIX seconds (ledger close time) |
price cache timestamp |
u64 |
UNIX seconds (time of the oracle record) |
live_until |
u32 |
Ledger sequence, about 5 seconds per ledger and 17,280 ledgers per day |
ledger_time |
u32 |
Seconds per ledger, the contract's conversion factor for lifetimes |
listing_min_days |
u32 |
Days of price feeds per listed asset |
min_trade_size |
i128 |
USD with 7 decimals (1 USD = 10,000,000) |
market_listing_fee, subsidize amount |
i128 |
XRF base units (7 decimals) |
Price cache price |
i128 |
USD in the oracle's decimals |