Skip to content

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

avo-rust-sdk

Unofficial Rust client for the Avo Portfolio (Market) API on Solana.

⚠️ This is a community-maintained, unofficial SDK. It is not affiliated with, maintained by, or endorsed by Avo (avo.so). It is a Rust port of the official TypeScript SDK, @avodotso/market-sdk.

Built for market-makers: bootstrap an agent, run rebalances, push NAV updates — over plain HTTP, through one client. It is not an on-chain SDK; every call goes to the Avo REST API, which signs the on-chain instructions server-side.

The single entry point is AvoMarketClient, which implements the MarketClient trait covering the full market-maker journey: create → operate → close.


1. Installation

# Cargo.toml
[dependencies]
avo-rust-sdk = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

A Tokio runtime is required — the client is built on reqwest (hyper + Tokio) and uses Tokio primitives internally. Other runtimes (async-std / smol) need a compatibility shim such as async-compat.

Or:

cargo add avo-rust-sdk tokio

The library re-exports the one solana type it exposes as avo_rust_sdk::Pubkey, so self-custodial callers need no solana crate of their own. Only the create-market flow needs solana-sdk (for Keypair) and solana-client (to broadcast) — see the examples.


2. Quick start

Operate a market — custodial (bearer, recommended)

You hold a bearer token (from finalize_create_market / register_market); the API holds the agent's signing key encrypted at rest. You never touch a private key.

use avo_rust_sdk::client::{AvoMarketAgent, AvoMarketClient, MarketClient, MarketClientConfig};
use avo_rust_sdk::types::{NavRequest, RebalanceRequest, WeightInput};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = AvoMarketClient::new(
        MarketClientConfig::new()
            .with_avo_api("https://portfolio-contract-api-service-dev.up.railway.app")
            .with_agent(AvoMarketAgent::Bearer {
                token: std::env::var("AGENT_BEARER")?,
                pubkey: None,
            }),
    );

    // Live valuation of every vault in the market (50 bps pricing slippage).
    let value = client.get_value(Some(50)).await?;
    println!("{value:#?}");

    // Rebalance to a new target weight vector (bps sum to 10_000).
    client
        .rebalance(
            RebalanceRequest::builder()
                .weights(vec![
                    WeightInput::builder().asset("base").weight_bps(0).build(),
                    WeightInput::builder()
                        .asset("So11111111111111111111111111111111111111112")
                        .weight_bps(10_000)
                        .build(),
                ])
                .slippage_bps(100)
                .deadline_secs(180)
                .build(),
        )
        .await?;

    // Push fresh NAV on chain (omit total_value_base to let the API quote live).
    client.update_nav(NavRequest::builder().build()).await?;
    Ok(())
}

Operate a market — self-custodial (Sig auth)

You hold the agent's Ed25519 secret key; the SDK mints short-lived signed tokens on every call window.

use avo_rust_sdk::auth::LocalSigner;
use avo_rust_sdk::client::{AvoMarketAgent, AvoMarketClient, MarketClientConfig};
use avo_rust_sdk::Pubkey; // re-exported — no solana crate needed

let signer = LocalSigner::new(pubkey /* Pubkey */, seed /* [u8; 32] */);
let client = AvoMarketClient::new(
    MarketClientConfig::new()
        .with_avo_api("https://…")
        .with_agent(AvoMarketAgent::SelfCustodial { pubkey, signer: Box::new(signer) }),
);

External signers (KMS / HSM / hardware wallet) plug in by implementing the Signer trait instead of LocalSigner.

More use cases → examples/

Run any with cargo run --example <name>:

Example What it shows
healthz, list_markets, market_snapshot, agent_identity Zero-auth public reads
mint_token, external_signer, error_handling Auth & error mechanics (offline)
operate_bearer, operate_sig Operate a market in each auth mode + rebalance
create_market, create_market_manual Create a market (permissionless flow, one-shot & step-by-step)
register_market Register a market you built on-chain yourself

3. Method reference

All methods are on the MarketClient trait (AvoMarketClient is the concrete impl), except create_market_with_shadow_portfolio, which is an inherent method on AvoMarketClient.

Bootstrap (no auth)

Method Endpoint
prepare_create_market(req) POST /v1/markets/create/prepare
finalize_create_market(req) POST /v1/markets/create/finalize
register_market(req) POST /v1/markets/register
create_market_with_shadow_portfolio(req, &signer, submit) one-shot prepare → sign → finalize

Public reads (no auth)

Method Endpoint
healthz() GET /healthz
list_markets(req) GET /v1/markets
get_market_events(pda, req) GET /v1/markets/{pda}/events
get_nav_history(pda, req) GET /v1/markets/{pda}/nav-history
get_identity(pubkey) GET /v1/agents/identity/{pubkey}
attach_identity(req) POST /v1/agents/identity/attach

Agent ops (authenticated)

Method Endpoint
get_market() GET /v1/market
get_assets() GET /v1/assets
get_value(slippage_bps) GET /v1/value
get_quote(req) GET /v1/quote
update_nav(req) POST /v1/nav
rebalance(req) POST /v1/rebalance
simulate_rebalance(req) POST /v1/rebalance/simulate
add_asset(req) POST /v1/assets
remove_asset(mint, req) DELETE /v1/assets/{mint}
get_own_identity() GET /v1/agents/identity/{self}

Requests use a builder (SomeRequest::builder().field(..).build()); optional fields are omitted from the wire body when unset. Responses expose new(..) + Default. Atomic-unit amounts are rust_decimal::Decimal (quoted decimal strings on the wire).


4. Auth model

Authenticated methods work in two mutually-exclusive modes, selected by the AvoMarketAgent you pass to MarketClientConfig::with_agent. Both resolve to the same agent identity server-side and grant identical access.

pub enum AvoMarketAgent {
    // Custodial: the API holds the key; you hold a one-time bearer.
    Bearer { token: String, pubkey: Option<Pubkey> },
    // Self-custodial: you hold the key; the SDK mints signed tokens per call.
    SelfCustodial { pubkey: Pubkey, signer: Box<dyn Signer + Send + Sync> },
}
  • Bearer stamps a static Authorization: Bearer <token>.
  • Sig mints Authorization: Sig <pubkey>.<expiresAtMs>.<sigBase58> over a canonical message, caching the token until ~30s before expiry. TTL defaults to 600s; override with MarketClientConfig::with_token_ttl_secs.

External signers. Implement the async Signer trait for KMS / HSM / hardware wallets — the SDK hands you the message bytes and you return a 64-byte Ed25519 signature. LocalSigner is the in-process implementation.

Helpers (mint tokens / build attach messages outside the client): avo_rust_sdk::auth::{mint_token, build_attach_message, canonical_auth_message}.

Gotcha: the agent pubkey must be registered with the API (by creating or registering a market) before Sig/Bearer auth works — otherwise the API returns BAD_AUTH / "Agent pubkey not registered". See docs/api-discovery.md.


5. Errors

Every fallible method returns Result<T, AvoError> (aliased AvoResult<T>):

pub enum AvoError {
    // A structured API/SDK error (HTTP status + machine code).
    Sdk { status: u16, code: String, message: String, details: Option<serde_json::Value> },
    // A transport failure (connection refused, DNS, non-JSON body).
    Transport { message: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
}

Branch with the accessors err.status() -> Option<u16>, err.code() -> Option<&str>, err.is_transport() -> bool:

match client.rebalance(req).await {
    Ok(res) => { /* … */ }
    Err(e) if e.is_transport() => eprintln!("network: {e}"),
    Err(e) => eprintln!("api error [{:?}] {:?}: {e}", e.status(), e.code()),
}

Codes the SDK raises itself (everything else comes from the API's error envelope, e.g. BAD_AUTH, RATE_LIMITED):

Code When
SERVICE_UNCONFIGURED No API base URL was configured.
AGENT_NOT_CONFIGURED A protected method was called on an unauthenticated client (or get_own_identity without a known pubkey).
INVALID_FUNDING_SIGNATURE The funding-tx signer returned an empty signature.
HTTP_<status> A non-2xx response with no parseable error envelope.

6. Versioning

Semantic versioning. The crate version tracks the upstream @avodotso/market-sdk version it ports0.1.5 here mirrors upstream 0.1.5. A scheduled CI workflow (.github/workflows/upstream-watch.yml) checks npm daily and opens a sync issue when upstream publishes a new version.


7. License

Licensed under the ISC License, matching the upstream SDK.


8. Author

Maintained by @pr0m3th3usEx.

Not affiliated with Avo. For the official product, see avo.so.

About

Avo.so Rust SDK

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages