metaflux-client 0.1.0

Rust SDK for the MetaFlux derivatives L1 — REST + WebSocket, EIP-712 signing, and typed builders for the full signed-action surface (orders, TWAP, margin, vaults, staking, spot/Earn).
Documentation

metaflux-client

crates.io docs.rs CI license

Rust SDK for the MetaFlux derivatives L1 — REST + WebSocket, EIP-712 signing, and typed builders for the node's full signed-action surface.

Installation

Published on crates.io:

[dependencies]
metaflux-client = "0.1"

or cargo add metaflux-client. The crate is imported as metaflux_client.

A short alias crate, metaflux, re-exports the entire API — metaflux = "0.1" and use metaflux::... work identically.

What it does

  • REST /info / /exchange / /explorer — snake_case JSON, plain-integer numerics (sizes / prices on fixed-point planes), market_id rather than coin.
  • WebSocket subscriptions — reconnect with backoff + heartbeat.
  • EIP-712 signing — secp256k1 with deterministic (RFC-6979) nonces.

The /exchange surface is fully typed. Every signed action the node accepts has a first-class request type and an Exchange method, including:

  • Orders — submit / cancel / cancel-by-cloid / modify, plus batched variants, schedule-cancel and cancel-all.
  • TWAP — sliced orders and cancellation.
  • Leverage & margin — update leverage, isolated-margin adjust / top-up, portfolio-margin enroll.
  • Vaults — create, transfer, modify, follower withdraw.
  • Staking — delegate / undelegate, claim rewards, link staking user.
  • Spot & Earn — spot CLOB orders, leveraged spot margin, Earn lending.
  • Account & agents — display name, referrer, agent approval, builder-fee approval, multisig conversion, abstraction settings, priority bids.
  • Encrypted orders — threshold-encrypted, MEV-resistant submissions.
  • MetaBridge — cross-collateral withdrawals to other chains.

Quick start

use metaflux_client::{
    Client,
    types::order::{Order, OrderKind, Side, TimeInForce},
    wallet::Wallet,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Build a wallet from a 32-byte secp256k1 private key.
    let priv_key_hex = std::env::var("MTF_PRIVATE_KEY")?;
    let wallet = Wallet::from_hex(&priv_key_hex)?;
    println!("wallet address: 0x{}", hex::encode(wallet.address()));

    // 2. Construct the client.
    let client = Client::new("https://api.mtf.exchange")?;

    // 3. Build + sign + submit a limit order. The node assigns the oid and
    //    returns it in the response — the submit shape never declares one.
    let order = Order {
        owner: wallet.address(),
        market: metaflux_client::types::MarketId(1), // BTC-PERP
        side: Side::Bid,
        kind: OrderKind::Limit,
        size: 1_000, // 0.001 BTC if size_decimals = 6
        limit_px: 5_000_000_000_000, // $50,000.0000 in tick units
        tif: TimeInForce::Gtc,
        stp_mode: metaflux_client::types::order::StpMode::CancelOldest,
        reduce_only: false,
        cloid: None,
        builder: None,
        position_side: None,
    };

    let resp = client.exchange().submit_order(&wallet, &order).await?;
    println!("submitted: {resp:?}");
    Ok(())
}

Spot trading

The spot CLOB (v0 = IOC limit only, limit_px > 0 on the 1e8 price plane) is a separate book from the perp engine, keyed by a numeric pair id. Discover pairs via spot_meta(), trade with spot_order / spot_cancel, and read balances back with spot_clearinghouse_state(address):

use metaflux_client::types::{
    order::Side,
    spot::{SpotCancel, SpotOrder},
};

// `client` and `wallet` as in the Quick start above.

// 1. Discover pairs. `name` is derived as "{base}/{quote}" from the token
//    registry; `id` is the numeric pair id.
let meta = client.rest().info().spot_meta().await?;
let pair = meta
    .pairs
    .iter()
    .find(|p| p.name == "BTC/USDC")
    .expect("pair listed");

// 2. Place an IOC limit spot order (signed, POST /exchange).
let order = SpotOrder::ioc_limit(pair.id, Side::Bid, 1_000, 5_000_000_000);
let resp = client.exchange().spot_order(&wallet, &order).await?;
println!("spot order: {resp:?}");

// 3. Read balances back.
let bals = client
    .rest()
    .info()
    .spot_clearinghouse_state(wallet.address())
    .await?;
for b in &bals.balances {
    println!("{} ({}) = {}", b.name, b.asset, b.balance);
}

// 4. Cancel a resting order by oid.
client
    .exchange()
    .spot_cancel(&wallet, &SpotCancel { pair: pair.id, oid: 12345 })
    .await?;

On the WebSocket trades / candles / fills channels, spot prints carry the numeric pair id as the coin label (e.g. "101"), not the display name — use spot_meta() to map id to its "{base}/{quote}" name.

Spot margin & Earn (devnet preview)

Leveraged spot borrows quote from the Earn lending pool. It is available on devnet (preview): the full deposit → borrow → leveraged-buy → close loop works, but forced-liquidation settlement is not yet wired and per-pair maintenance ratios are still being calibrated — don't treat it as production-ready. All six actions are sender-authorized (the signer is the actor) and return the 202 Accepted admission envelope, not a synchronous oid; observe committed state via /info spot_margin_state / earn_state. Decimal amounts (amount / borrow / shares) are passed as strings; size / limit_px are integers on the raw-lot / 1e8 planes.

use metaflux_client::types::spot::{
    EarnDeposit, SpotMarginClose, SpotMarginDeposit, SpotMarginOpen,
};

// `client`, `wallet`, `pair` as above.

// Supply side: a lender funds the pool (asset = the pair's quote token id).
client
    .exchange()
    .earn_deposit(&wallet, &EarnDeposit { asset: pair.quote, amount: "5000".into() })
    .await?;

// Borrow side: post collateral, then open a leveraged long.
client
    .exchange()
    .spot_margin_deposit(&wallet, &SpotMarginDeposit { pair: pair.id, amount: "100".into() })
    .await?;
client
    .exchange()
    .spot_margin_open(
        &wallet,
        &SpotMarginOpen { pair: pair.id, size: 200, limit_px: 200_000_000, borrow: "400".into() },
    )
    .await?;

// Close the position (sells the held base, repays principal + interest to the
// Earn pool, returns the remainder; a partial fill keeps the account open).
client
    .exchange()
    .spot_margin_close(&wallet, &SpotMarginClose { pair: pair.id, limit_px: 200_000_000 })
    .await?;

Read the committed state over POST /info with { "type": "spot_margin_state", "user": "0x.." } (collateral / borrowed / base_held / current_debt per margin account) and { "type": "earn_state", "user": "0x.." } (per-pool supplied / borrowed / idle / share_value, plus your shares when user is supplied).

Module overview

Module Purpose
wallet secp256k1 keypair management + EIP-712 signing (RFC-6979 deterministic nonces)
rest RestClient/info, /exchange, /explorer endpoints
ws WsClient — subscriptions with reconnect-with-backoff
types Domain types: orders, TWAP, margin, vaults, staking, spot / Earn

Examples

Runnable examples live under examples/:

  • submit_limit_order.rs — fetches markets(), signs a limit order, posts to /exchange.
  • stream_trades.rs — opens a WS connection, subscribes to BTC-PERP trades, prints first 10.
  • create_vault.rs — creates a vault, queries its state.

Run with cargo run --example <name>. Examples expect MTF_PRIVATE_KEY env var.

Versioning

Pre-1.0: minor bumps may break. We will follow strict SemVer once we tag v1.0. The wire schema is governed by the node, not this SDK — the SDK re-exposes wire types verbatim, so wire-breaking changes upstream cascade.

License

MIT — see LICENSE.