Skip to main content

Crate acta_maker_sdk

Crate acta_maker_sdk 

Source
Expand description

§acta-maker-sdk

Rust SDK for Acta’s Solana options RFQ venue. Makers use it to receive RFQs and send signed quotes. Vault operators use it to quote for a vault, check served transactions before signing, and run capital operations.

§Install

[dependencies]
acta-maker-sdk = { version = "0.8.0", features = ["ws-client"] }

Rust 1.89 or later. The default feature set is empty.

FeatureEnables
ws-clientMakerQuoteClient, MakerDataClient, ManagedWsHandle and the raw Tokio WsClient
httpRead-only HttpClient (reqwest 0.13 with rustls)
chainInstruction builders, derived addresses, checked account decoders, VaultManager, served-transaction verification
chain-rpcChainClient over Solana RPC; includes chain
vaultVaultOperatorClient and checked vault transaction signing; includes ws-client and chain
decimalrust_decimal conversions for Price, Strike and Quantity
test-helpersManagedWsHandle::test_handle and message injection

This release speaks WS protocol 1.1.0 (WS_PROTOCOL_VERSION). See CHANGELOG.md for what changed.

§Before you connect

Maker registration is not self-service. Acta registers the maker owner and its quote-signing key on chain; until then authentication fails with maker_not_registered.

  • The owner holds funds and signs maker transactions such as MakerDeposit and DepositFundsToPosition.
  • The quote-signing key authenticates the session and signs quotes. It can be the owner key.
  • A fill debits its premium from the owner’s premium balance, so deposit with MakerDeposit before quoting.

The Rust maker guide covers registration and environment setup.

§Quick start

The examples read a 64-byte Solana keypair JSON file:

export ACTA_KEYPAIR_PATH=/absolute/path/to/quote-signing-key.json
export ACTA_MAKER_OWNER=<owner base58>
cargo run --locked --features ws-client --example hello_auth
cargo run --locked --features ws-client,http --example managed_quote

ACTA_WS_URL defaults to DEVNET_WS. The quote examples send real signed quotes at a fixed price of 1_000_000_000, so use a test maker on devnet. They do not price, hedge or fund.

A managed quote loop connects, authenticates, recovers state, subscribes to RFQs and quotes in the recovered epoch:

use acta_maker_sdk::*;
use acta_maker_sdk::ws::{maker::MakerQuoteClient, managed::*, types::*};
use std::{sync::Arc, time::Duration};
static NONCES: AtomicNonceGenerator = AtomicNonceGenerator::new();

let signer = Arc::new(BytesSigner::from_keypair(&keypair)?);
let maker_owner = Address32::from_base58(&owner)?;
let config = ManagedWsConfig::new(
    DEVNET_WS,
    HelloData {
        protocol_version: WS_PROTOCOL_VERSION.to_string(),
        on_disconnect: OnDisconnect::CancelQuotes,
        client_name: "my-bot".to_string(),
        client_version: env!("CARGO_PKG_VERSION").to_string(),
    },
    signer.clone(),
    maker_owner,
)
.low_latency()
.with_initial_subscribe(SubscribeData {
    products: ProductSet::ALL,
    request_id: uuid::Uuid::new_v4(),
    channels: vec![WsChannel::Rfqs],
    underlying_mints: Vec::new(),
    quote_mints: Vec::new(),
});

let client = MakerQuoteClient::spawn(config)?;
let mut messages = client.subscribe_messages();
let epoch = client.wait_until_ready().await?;

while let Ok(msg) = messages.recv().await {
    match msg.message() {
        ServerMessage::RfqBroadcast(rfq) => {
            let min_valid_until = QuoteExpiry::after(min_validity).ok_or("clock")?;
            let valid_until =
                QuoteExpiry::after(min_validity + Duration::from_secs(60)).ok_or("clock")?;
            let quote = match RfqBinding::from_broadcast(rfq)?
                .quote()
                .maker_owner(maker_owner)
                .price(Price::new(1_000_000_000))
                .valid_until(valid_until)
                .min_valid_until(min_valid_until)
                .nonce(Nonce::new(NONCES.next_u64()?))
                .sign(signer.as_ref())
            {
                Ok(quote) => quote,
                Err(QuoteBuildError::ValidUntilAfterMarketExpiry { .. }) => continue,
                Err(error) => return Err(error.into()),
            };
            client.quote_in_epoch(quote, epoch).await?;
        }
        ServerMessage::QuoteAcknowledged(ack) => println!("accepted {:?}", ack.order_id),
        ServerMessage::QuoteOutbid(outbid) => println!("outbid on {}", outbid.rfq_id),
        ServerMessage::QuoteRejected(rejected) => println!("rejected {:?}", rejected.reason),
        ServerMessage::QuoteFilled(fill) => {
            println!("filled on {}, position {}", fill.market_pda, fill.position_pda)
        }
        ServerMessage::RfqClosed(closed) => println!("closed {:?}", closed.reason),
        _ => {}
    }
}

This sketch quotes only in its first epoch. managed_quote waits for each new epoch and applies recovery before it quotes again. A socket write is not an accepted quote; never replay an old signed quote after a reconnect.

QuoteFilled means the position opened on chain. Fund its settlement leg with DepositFundsToPosition if the product requires it. Acta’s keeper settles an in-the-money funded position to the owner’s ATA; no SDK call is needed.

§Examples

ExampleFeaturesPurpose
hello_authws-clientAuthenticate and close
managed_quotews-client, httpRecover state and send epoch-bound quotes
taker_buy_quotews-client, http, chain-rpcWrite taker buys from Maker PDA custody and keep it funded
quotews-client, httpRaw /maker handshake and one signed quote
spread_quotews-client, httpPrice and sign call spreads, put spreads and cash puts
discoveryws-clientRead quotes, positions, markets and summary on /maker/data
indicativews-clientAnswer unsigned indicative-price requests
loopback_latencyws-clientMeasure local managed-client latency
vault_operatorvault, httpVault session quoting both sides, with checked fill and swap signing
vault_capitalchain-rpcCapital-cycle crank
curator_ptchain-rpc, httpPlan a PT route and simulate it; never sends

Maker examples read ACTA_KEYPAIR_PATH, ACTA_MAKER_OWNER and ACTA_WS_URL. taker_buy_quote also reads ACTA_OWNER_KEYPAIR_PATH, ACTA_CUSTODY_MINT, ACTA_CUSTODY_TOKEN_PROGRAM, ACTA_CUSTODY_TARGET (atoms) and ACTA_RPC_URL. vault_operator also reads ACTA_VAULT_PDA. vault_capital reads ACTA_KEYPAIR_PATH, ACTA_VAULT_PDA and ACTA_RPC_URL (devnet by default). curator_pt reads RPC_URL, JUPITER_API_KEY, CURATOR_KEYPAIR, VAULT and AMOUNT_ATOMS, and takes buy, sell or redeem as its argument.

§Writing taker buys

A buy RFQ (side: buy) asks you to write the option the taker holds: the taker is the holder, you are the writer. Subscribe to WsChannel::TakerBuyRfqs; the channel is separate from Rfqs, so a maker that only sells never receives buys.

  • Custody. The collateral leaves your Maker PDA custody at open, not your wallet: the underlying for a call, the quote mint for a put, a spread or a cash put. The net premium lands in custody in the same transaction. Fund custody with ChainClient::maker_deposit (MakerDeposit) from your wallet ATA; a wSOL underlying must already be wrapped in that ATA. For native SOL, chain::build_maker_deposit_native_sol_ixs wraps and deposits in one go and chain::build_maker_withdraw_native_sol_ixs withdraws and unwraps.
  • Admission. The server checks custody before it sends you the RFQ. A short custody turns into RfqSkipped with maker_insufficient_balance and the RFQ never reaches quoting, so top up when you see the skip, after every fill, or on a timer.
  • Price. The taker pays your price; quote an ask. The order id signs the side, so a buy quote cannot fill a sell.
  • Settlement. The holder deposits strictly before expiry to exercise a physical buy. An unfunded in-the-money buy returns all collateral to you and settles as not_exercised; spreads and cash puts settle in cash with no deposit. PositionInfo names the roles writer (you) and holder (the buyer).

See examples/taker_buy_quote.rs.

§Rules that matter

  • Epochs. Ready means the SDK’s recovery reads arrived, not that your strategy applied them. quote_in_epoch, replace_in_epoch and batch_quotes_in_epoch return NotReady once the session moves to a new epoch.
  • Subscribe first. The first receiver gets a bounded backlog since spawn; later receivers start at subscription time.
  • Unknown is not failure. A timeout, gap, reconnect or Unknown order status does not prove an order has no exposure. Reconcile with GetMyQuotes or GetOrderStatus and never reuse its order ID.
  • Program ID. RfqBinding::from_broadcast checks the market against ACTA_PROGRAM_ID. For another deployment use from_broadcast_for_program with a locally configured ID; never take it from the RFQ.
  • Units. Price and Strike use the 1e9 PRICE_SCALE per underlying token. Quantity is underlying atoms. Signed quote expiry is whole Unix seconds; *_unix_ms fields are milliseconds.
  • Closed wire. Every field is present on every frame and absent facts are null. Decoding rejects unknown message types, enum values, error codes and missing fields. A rejected frame after Ready keeps the session up and reaches you as ManagedWsEvent::UnreadableFrame and a receiver gap: reconcile as after any gap. Match error codes, not messages.
  • Side. Every RFQ, quote, position and trade carries terms: OptionTerms and side. Makers subscribe to WsChannel::TakerBuyRfqs to receive buy RFQs, physical or spread, and quote them from custody. A vault sees both sides on its vaults channel: it bids on a sell and holds, and with SIGN_WRITER_QUOTES it asks on a buy and writes from its collateral leg. The buyer’s wallet signs that open (VaultOpenIntent::Buy); the vault signs only the quote. A vault never requests a buy as taker (taker_buy_unsupported).

§Where to start in the API

TaskStart with
Quote, replace, cancelMakerQuoteClient, RfqBinding, QuoteBuilder
Private reads and owner pushesMakerDataClient (/maker/data), all_pages
Track one quote to a resultws::maker::QuoteOutcomeTracker
Spreads and cash putsOptionTerms, spreads::SpreadTerms, build_spread_order_preimage
Premium and fee splitphysical_premium, SpreadTerms::premium
Recover positions from chainChainClient::maker_open_positions, maker_snapshot
Fund and confirmPositionSnapshot::fund_instructions, confirm_signatures
Vault owner actionschain::vault::VaultManager, ChainClient::vault_manager
Vault operator sessionVaultOperatorClient, verify_and_sign_sponsored_tx, verify_and_sign_swap
Buy from a vault (wallet side)VaultManager::open_position_for_wallet_holder, verify_vault_open with VaultOpenIntent::Buy
Decode position eventschain::events::decode_position_event (event version 2)
Vault capital cycleChainClient::unfunded_positions, pending_capital_requests
PT routingchain::vault::curator
Venue limits, tokens, markets, vaultsHttpClient

Read venue limits such as minimum quote validity from HttpClient::venue or WelcomeData rather than hardcoding them.

§Latency sample

Release build, loopback peer, low_latency, one Apple silicon machine. This is SDK overhead only; network and server time are extra.

Legp50p90p99
try_send to peer frame20 µs27 µs60 µs
peer send to session parse150 µs281 µs424 µs
session task to subscriber recv19 µs39 µs70 µs

§List paging

List calls collect every page for you. MakerDataClient::request_all and the HttpClient catalog methods go through paging::fetch_all. For a “load more” view use request_page and keep the returned cursor as an opaque value.

On the wire, WS lists take limit (at most 200) and Cursor::Start {} or Cursor::After { id }, and return next_cursor, null on the last page. REST lists take an optional limit (default 50, at most 200, 100 for vault requests) and an optional cursor: omit it for the first page, then pass next_cursor as is.

§Documentation

§License

MIT

Re-exports§

pub use error::ActaSdkError;
pub use network::DEVNET_API;
pub use network::DEVNET_WS;
pub use network::MAINNET_API;
pub use network::MAINNET_WS;
pub use quote::QuoteBuildError;
pub use quote::QuoteBuilder;
pub use quote::RfqBinding;
pub use types::PRICE_SCALE;
pub use types::QuoteExpiry;
pub use types::ids::Balance;
pub use types::ids::ChainId;
pub use types::ids::ChainPositionType;
pub use types::ids::ChainPositionTypeParseError;
pub use types::ids::ChainTerms;
pub use types::ids::Decimals;
pub use types::ids::DurationSeconds;
pub use types::ids::MarketId;
pub use types::ids::Nonce;
pub use types::ids::OptionTerms;
pub use types::ids::OrderId;
pub use types::ids::OrderVersion;
pub use types::ids::PositionRevision;
pub use types::ids::PositionType;
pub use types::ids::Price;
pub use types::ids::Quantity;
pub use types::ids::QuoteCount;
pub use types::ids::RfqVersion;
pub use types::ids::Slot;
pub use types::ids::Strike;
pub use types::ids::TakerSide;
pub use types::ids::TimeoutSeconds;
pub use types::ids::TradeCount;
pub use types::ids::UserId;
pub use types::ids::Volume;
pub use types::invite::REFERRAL_CODE_MAX_LEN;
pub use types::invite::REFERRAL_CODE_MIN_LEN;
pub use types::invite::RESERVED_CODES;
pub use types::invite::ReferralCode;
pub use types::invite::ReferralCodeFormatError;
pub use types::invite::TakerStatus;
pub use types::invite::is_reserved;
pub use types::vault::VaultDelegatePermissions;
pub use types::vault::VaultLeg;
pub use types::vault::VaultOperatorKind;
pub use types::vault::VaultStagingStep;
pub use types::vault::VaultSwapDirection;
pub use types::vault::VaultSwapMode;
pub use types::Address32;
pub use types::AddressError;
pub use types::PhysicalPremium;
pub use types::physical_premium;
pub use types::total_premium_quote_atoms;
pub use ws::client::PreparedClientMessage;
pub use ws::client::ServerClose;
pub use ws::client::WsClient;
pub use ws::client::WsTransportConfig;
pub use ws::error::WsClientError;
pub use ws::error::WsResult;
pub use ws::error::WsTransportConfigError;
pub use ws::maker::MakerClientError;
pub use ws::maker::MakerDataClient;
pub use ws::maker::MakerDataRequest;
pub use ws::maker::MakerDataRequestError;
pub use ws::maker::MakerQuoteClient;
pub use ws::maker::MakerResponse;
pub use ws::maker::PagedRequest;
pub use ws::managed::GapPolicy;
pub use ws::managed::MakerWsEndpoint;
pub use ws::managed::ManagedInbound;
pub use ws::managed::ManagedMessageReceiver;
pub use ws::managed::ManagedReceiveError;
pub use ws::managed::ManagedWsConfig;
pub use ws::managed::ManagedWsConfigError;
pub use ws::managed::ManagedWsError;
pub use ws::managed::ManagedWsEvent;
pub use ws::managed::ManagedWsHandle;
pub use ws::managed::ManagedWsState;
pub use ws::managed::ManagedWsTerminationReason;
pub use ws::managed::OutboundMessageError;
pub use ws::managed::SendAwaitError;
pub use ws::managed::SendTicket;
pub use ws::managed::WaitUntilReadyError;
pub use ws::managed::normalize_maker_data_ws_url;
pub use ws::managed::normalize_maker_ws_url;
pub use ws::managed::normalize_maker_ws_url_for_endpoint;
pub use ws::managed::spawn_managed_ws;
pub use ws::market_decimals::MarketDecimals;
pub use ws::market_decimals::TokenDecimals;
pub use ws::types::ClientMessage;
pub use ws::types::ServerMessage;
pub use ws::vault::FleetListing;
pub use ws::vault::VaultFleet;
pub use ws::vault::VaultFleetConfig;
pub use ws::vault::SaleDesk;
pub use ws::vault::SaleError;
pub use ws::vault::SaleOutcome;
pub use ws::vault::SaleStage;
pub use ws::vault::SignedSale;
pub use ws::vault::VaultSale;
pub use ws::vault::SendGate;
pub use ws::vault::Ungated;
pub use ws::vault::VaultClientError;
pub use ws::vault::VaultOperatorClient;
pub use ws::vault::VaultOperatorConfig;
pub use ws::vault::VaultProjection;
pub use ws::vault::VaultProjectionReceiver;
pub use ws::vault::VaultRequestError;
pub use ws::vault::VaultTxError;
pub use nonce::AtomicNonceGenerator;
pub use nonce::NonceError;
pub use nonce::NonceGenerator;
pub use orders::BytesSigner;
pub use orders::ORDER_DOMAIN_TAG;
pub use orders::ORDER_ID_LEN;
pub use orders::ORDER_PREIMAGE_LEN;
pub use orders::OrderError;
pub use orders::OrderPreimageArgs;
pub use orders::SignerLike;
pub use orders::build_order_preimage;
pub use orders::compute_order_id;
pub use orders::hash_order_preimage;
pub use orders::order_id_hex;
pub use orders::order_preimage_hex;
pub use orders::sign_order_id_base58;
pub use orders::sign_order_id_bytes;
pub use orders::sign_order_id_from_base58_keypair;
pub use orders::sign_order_id_with_async_signer;
pub use orders::sign_order_id_with_signer;
pub use orders::sign_order_id_with_signer_base58;
pub use orders::verify_order_id_signature_base58;
pub use orders::verify_order_id_signature_bytes;
pub use signing::AsyncSignerLike;
pub use signing::SigningError;
pub use signing::SigningFuture;
pub use wire::WireError;
pub use wire::decode_base58_32;
pub use wire::decode_base58_64;
pub use wire::decode_hex_32;
pub use wire::encode_base58;
pub use wire::encode_hex;
pub use types::InvalidProductSet;
pub use types::ProductSet;

Modules§

chain
error
http
Read-only client for the public Acta HTTP API.
io_observer
Optional process-local observations at transport boundaries.
network
Public Acta API origins. Append the WebSocket path (/maker, /maker/data, /vault) or pass the origin to normalize_maker_ws_url.
nonce
orders
paging
Shared continuation handling for HTTP and WebSocket lists.
quote
Quote assembly: RfqBinding decodes identities and resolves offered terms; QuoteBuilder derives the signed preimage and QuoteMessage from the same terms, price, expiry and nonce.
signing
spreads
Cash spreads use quote atoms for collateral and payoff; prices use 1e9.
types
Domain values shared by WebSocket and chain APIs.
wire
ws

Constants§

ACTA_PROGRAM_ID
The same on devnet and mainnet; /api/v1/venue program_id is authoritative.
WS_PROTOCOL_VERSION