Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

seal-sdk-rs gives you a Seal client that works with any Sui framework. The crate focuses on developer experience. It ships safe defaults, but you can replace every layer: HTTP transport, Sui client, signer, and cache. Use the full sui_sdk stack or a lighter experimental client. The API stays simple so you can control how requests are built, signed, cached, and executed.

Key features:

  • Modular design: BaseSealClient accepts generic types for the HTTP client, Sui RPC adapter, caches, and error types. You can mix and match implementations without forking the crate.
  • Helpful defaults: The provided SealClient variants combine sui_sdk::SuiClient, reqwest, and different cache strategies (including moka) so most projects can start quickly.
  • Parallel performance: Batch helpers such as encrypt_multiple_bytes reuse fetched metadata and run remote calls in parallel when possible. This keeps round trips to Mysten key servers short.
  • Friendly helpers: Conversion traits, generic object IDs/addresses, and BCS serializers simplify data handling. Advanced users can still manage serialization manually when they need to.

Quick Start

This guide uses the crate with the default features. In this mode the SealClient specialization is active, combining sui_sdk::SuiClient, reqwest, and the no-op cache adapters.

Install

Add the crate to your project:

[dependencies]
seal-sdk-rs = { git = "https://github.com/gfusee/seal-sdk-rs", tag = "0.0.5" }

Info: The examples build a WalletContext from a normal Sui CLI config (WalletContext::new("<path to the config file>")). This approach is easy, but not required. Any signer that implements the crate’s Signer trait can call SessionKey::new, so you can bring your own signing logic if you prefer.

Example setup

The snippets follow the flow of the integration tests with fewer moving parts. Both examples rely on the same inputs:

  • One key server identified by setup.key_server_id.
  • A Seal package deployed at setup.approve_package_id.
  • A wallet context that can sign personal messages.

All helpers return Result<_, SealClientError>, so the ? operator propagates any failure.

Encrypting a string

#![allow(unused)]
fn main() {
use seal_sdk_rs::error::SealClientError;
use seal_sdk_rs::native_sui_sdk::client::seal_client::SealClient;
use sui_sdk::SuiClientBuilder;

struct DemoSetup {
    approve_package_id: seal_sdk_rs::generic_types::ObjectID,
    key_server_id: seal_sdk_rs::generic_types::ObjectID,
}

async fn encrypt_message(
    setup: &DemoSetup,
) -> Result<seal_sdk_rs::crypto::EncryptedObject, SealClientError> {
    let sui_client = SuiClientBuilder::default()
        .build("https://fullnode.testnet.sui.io:443")
        .await?;

    let client = SealClient::new(sui_client);
    let (encrypted, recovery_key) = client
        .encrypt_bytes(
            setup.approve_package_id,
            b"my_id".to_vec(),
            1,
            vec![seal_sdk_rs::base_client::KeyServerConfig::new(setup.key_server_id, None)],
            b"hello from seal".to_vec(),
        )
        .await?;

    drop(recovery_key);
    Ok(encrypted)
}
}

Decrypting the ciphertext

Assume the package at setup.approve_package_id contains a wildcard module with a seal_approve function that approves requests for my_id. Use the EncryptedObject returned by encrypt_message to rebuild the approval transaction and recover the original string.

#![allow(unused)]
fn main() {
use seal_sdk_rs::error::SealClientError;
use seal_sdk_rs::native_sui_sdk::client::seal_client::SealClient;
use seal_sdk_rs::session_key::SessionKey;
use sui_sdk::SuiClientBuilder;
use sui_sdk::wallet_context::WalletContext;
use sui_types::Identifier;
use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder;
use std::str::FromStr;

struct DemoSetup {
    approve_package_id: seal_sdk_rs::generic_types::ObjectID,
    key_server_id: seal_sdk_rs::generic_types::ObjectID,
}

async fn decrypt_message(
    setup: &DemoSetup,
    encrypted: seal_sdk_rs::crypto::EncryptedObject,
) -> Result<(), SealClientError> {
    let sui_client = SuiClientBuilder::default()
        .build("https://fullnode.testnet.sui.io:443")
        .await?;
    let client = SealClient::new(sui_client);

    let mut wallet = WalletContext::new("<path to the config file>").unwrap();
    let session_key = SessionKey::new(
        setup.approve_package_id,
        5,
        &mut wallet,
    )
    .await?;

    let mut builder = ProgrammableTransactionBuilder::new();
    let id_arg = builder.pure(b"my_id".to_vec())?;
    builder.programmable_move_call(
        setup.approve_package_id.into(),
        Identifier::from_str("wildcard")?,
        Identifier::from_str("seal_approve")?,
        vec![],
        vec![id_arg],
    );
    let approve_ptb = builder.finish();

    let plaintext = client
        .decrypt_object_bytes(
            &bcs::to_bytes(&encrypted)?,
            approve_ptb,
            &session_key,
            std::collections::HashMap::new(),
        )
        .await?;

    assert_eq!(plaintext, b"hello from seal");
    Ok(())
}
}

Concepts & Architecture

This chapter explains how the SDK fits together: what powers BaseSealClient, how the ready-made specializations behave, and which operational details matter when you run the client in production.

BaseSealClient

BaseSealClient (see src/base_client.rs) is the generic core. It exposes six type parameters that let you decide which pieces to plug in:

  • key-server info cache implementation
  • derived-keys cache implementation
  • Sui RPC error type
  • Sui client implementation
  • HTTP error type
  • HTTP client implementation

You can supply any types that implement the required traits:

  • SealCache defines a simple async cache API.
  • SuiClient outlines the Sui RPC calls the client needs.
  • HttpClient asks for a single post method to talk to Seal key servers.

Because the generics stay abstract, you can swap components without editing the rest of the crate—use a mock HTTP client in tests, replace the cache with a shared service, or point to a different Sui SDK version.

Specializations

src/native_sui_sdk/client offers ready-to-use type aliases:

  • SealClient uses sui_sdk::SuiClient, reqwest::Client, and the NoCache adapters. The client and native-sui-sdk features enable it by default.
  • SealClientLeakingCache adds Arc<Mutex<HashMap<...>>> caches. These caches never evict, so use them only for short-lived tools.
  • SealClientMokaCache (behind the moka-client feature) relies on moka::future::Cache, giving you configurable eviction for long-lived services.

If you need something else, create your own alias that wires the right HTTP client, Sui client, and caches into BaseSealClient.

Caching strategies

Caching is optional but useful. The client can cache two kinds of data:

  • key-server metadata fetched from Sui
  • derived keys fetched from the Seal servers

NoCache skips caching. The HashMap and moka adapters show how to keep results in memory. To integrate a different cache (Redis, a database, etc.), implement SealCache.

Session keys (JWT analogy)

SessionKey lives in src/session_key.rs. Instead of signing every decrypt request with a wallet, you sign once to mint a short-lived key. Think of it like a JWT:

  1. A signer that implements Signer creates the session key.
  2. During the TTL window, decrypt calls use that key without asking the wallet again.

Handle the session key like a bearer token: keep it safe in memory and drop it when you no longer need it.

Independent and committee key servers

Seal key servers come in two flavors. An independent server holds the full master secret and serves key requests directly. A committee distributes the secret across multiple participants via threshold cryptography; an aggregator collects partial responses and returns the combined result.

The ServerType enum lets you tell the two apart programmatically:

use seal_sdk_rs::base_client::ServerType;

match &info.server_type {
    ServerType::Independent { url } => { /* server URL is available */ }
    ServerType::Committee { .. }    => { /* needs an external aggregator URL */ }
}

You can retrieve a key server’s metadata (name, public key, server type) at any time with get_key_server_info:

let info = client.get_key_server_info(key_server_id).await?;
println!("name: {}, type: {:?}", info.name, info.server_type);

For encryption and decryption, the SDK handles both types transparently through KeyServerConfig. Pass aggregator_url: None for independent servers and aggregator_url: Some(url) for committees. During decryption, provide a HashMap<ObjectID, String> mapping committee key server IDs to their aggregator URLs. See the Committee Servers chapter for details and examples.

Recovery keys and operational security

Every encrypt helper returns (EncryptedObject, [u8; 32]). The second value is an emergency recovery key. Store it if you want a break-glass option when key servers go offline. Drop it if you never want a single authority to decrypt all payloads without the key-server quorum.

Supported Sui SDKs and bridging types

Today you can choose between two Sui SDK families:

  • MystenLabs/sui is mature but heavy. It pulls in a large dependency graph and build toolchain.
  • MystenLabs/sui-rust-sdk is lightweight but still experimental.

seal-sdk-rs already bridges both worlds. src/generic_types.rs defines ObjectID and SuiAddress, and the BCSSerializableProgrammableTransaction trait hides differences between the SDKs. Conversions run in both directions and all types support serde.

The built-in specializations (SealClient, SealClientLeakingCache, SealClientMokaCache) currently target MystenLabs/sui and use JSON-RPC. gRPC support is on the roadmap because the JSON-RPC endpoints have started their phase-out. When the lightweight SDK stabilizes, new specializations can land without changing the overall design.

Feature flags overview

Cargo.toml exposes several public features:

  • default = client, native-tls, native-sui-sdk
  • client enables the HTTP layer (reqwest + http).
  • native-tls switches reqwest to native TLS. Disable it if you want to opt into rustls manually.
  • native-sui-sdk pulls in sui_sdk, sui_types, sui_keys, and shared_crypto, plus the Sui-specific adapters.
  • moka-client adds the moka cache specialization.

Disable the defaults if you want to bring your own implementations and re-enable only the pieces you need.

Committee Servers

Seal supports two key server modes: independent and committee. This chapter explains how they differ and how the SDK handles each.

Independent vs committee

An independent key server is a single operator that holds the master secret and responds to key requests directly. The on-chain KeyServer object stores the server URL, and the SDK calls that URL to fetch derived keys.

A committee key server distributes the master secret across multiple participants using threshold cryptography (MPC). No single member holds the complete secret. An aggregator service coordinates key requests: it fans out to individual members, collects partial responses until the threshold is met, combines them, and returns the result. From the SDK’s perspective, the aggregator is the single endpoint to call.

On-chain representation

Both modes use the same KeyServer Move object. The difference lies in the KeyServerV2 dynamic field’s ServerType enum:

  • ServerType::Independent { url } stores the server URL directly.
  • ServerType::Committee { version, threshold, partial_key_servers } stores the threshold and a vector of PartialKeyServer entries (each with their own URL, partial public key, and party ID). There is no single URL on-chain for committees because the aggregator endpoint is provided by the caller.

The SDK reads the V2 dynamic field and falls back to V1 for older key servers.

Querying key server metadata

Before encrypting or decrypting, you can inspect a key server’s metadata with get_key_server_info. The returned KeyServerInfo includes a server_type field — a ServerType enum that mirrors the on-chain Move type:

use seal_sdk_rs::base_client::ServerType;

let info = client.get_key_server_info(key_server_id).await?;

match &info.server_type {
    ServerType::Independent { url } => {
        println!("Independent server at {url}");
    }
    ServerType::Committee { version, threshold, partial_key_servers } => {
        println!("Committee v{version}, threshold {threshold}");
        println!("{} partial key servers", partial_key_servers.len());
    }
}

This is useful when your application needs to decide at runtime whether an aggregator URL is required, or when you want to display server details to the user.

Querying committee details

If you need to fan out requests to individual partial key servers (e.g. to build your own aggregator), use get_committee_info. It returns Some(ServerType::Committee { .. }) for committee servers and None for independent ones:

use seal_sdk_rs::base_client::ServerType;

let committee_info = client.get_committee_info(key_server_id).await?;

match committee_info {
    Some(ServerType::Committee { version, threshold, partial_key_servers }) => {
        println!("Committee v{version}, threshold: {threshold}");
        for member in &partial_key_servers {
            println!(
                "  Party {}: {} at {}",
                member.party_id, member.name, member.url
            );
        }
    }
    _ => {
        println!("Not a committee server");
    }
}

The partial public key bytes (partial_pk) are raw BLS12-381 G2 elements that can be used to verify each member’s partial key response before aggregation.

KeyServerConfig

KeyServerConfig wraps a key server’s object ID with an optional aggregator URL:

use seal_sdk_rs::base_client::KeyServerConfig;

// Independent server: no aggregator URL needed.
let independent = KeyServerConfig::new(key_server_id, None);

// Committee server: provide the aggregator URL.
let committee = KeyServerConfig::new(
    key_server_id,
    Some("https://aggregator.example.com".to_string()),
);

During encryption, the aggregator URL has no effect; only the key server’s on-chain public key matters. During decryption, the SDK uses the aggregator URL (when present) instead of the on-chain server URL to fetch derived keys.

Encrypting with a committee

Encryption works the same way for both modes. The SDK fetches the public key from the on-chain KeyServer object and encrypts locally:

#![allow(unused)]
fn main() {
use seal_sdk_rs::base_client::KeyServerConfig;
use seal_sdk_rs::error::SealClientError;
use seal_sdk_rs::generic_types::ObjectID;
use seal_sdk_rs::native_sui_sdk::client::seal_client::SealClient;
use sui_sdk::SuiClientBuilder;

async fn encrypt_with_committee(
    package_id: ObjectID,
    key_server_id: ObjectID,
    aggregator_url: String,
) -> Result<seal_sdk_rs::crypto::EncryptedObject, SealClientError> {
    let sui_client = SuiClientBuilder::default()
        .build("https://fullnode.testnet.sui.io:443")
        .await?;
    let client = SealClient::new(sui_client);

    let key_server = KeyServerConfig::new(key_server_id, Some(aggregator_url));

    let (encrypted, _recovery_key) = client
        .encrypt_bytes(
            package_id,
            b"my_id".to_vec(),
            1,
            vec![key_server],
            b"secret data".to_vec(),
        )
        .await?;

    Ok(encrypted)
}
}

Decrypting with a committee

During decryption, pass a map of key server object IDs to their aggregator URLs. The SDK routes the key fetch request to the aggregator instead of the on-chain URL:

#![allow(unused)]
fn main() {
use seal_sdk_rs::error::SealClientError;
use seal_sdk_rs::generic_types::ObjectID;
use seal_sdk_rs::native_sui_sdk::client::seal_client::SealClient;
use seal_sdk_rs::session_key::SessionKey;
use std::collections::HashMap;
use sui_sdk::SuiClientBuilder;
use sui_sdk::wallet_context::WalletContext;
use sui_types::Identifier;
use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder;
use std::str::FromStr;

async fn decrypt_with_committee(
    package_id: ObjectID,
    key_server_id: ObjectID,
    aggregator_url: String,
    encrypted: seal_sdk_rs::crypto::EncryptedObject,
) -> Result<Vec<u8>, SealClientError> {
    let sui_client = SuiClientBuilder::default()
        .build("https://fullnode.testnet.sui.io:443")
        .await?;
    let client = SealClient::new(sui_client);

    let mut wallet = WalletContext::new("<path to config>").unwrap();
    let session_key = SessionKey::new(package_id, 5, &mut wallet).await?;

    let mut builder = ProgrammableTransactionBuilder::new();
    let id_arg = builder.pure(b"my_id".to_vec())?;
    builder.programmable_move_call(
        package_id.into(),
        Identifier::from_str("wildcard")?,
        Identifier::from_str("seal_approve")?,
        vec![],
        vec![id_arg],
    );

    let aggregator_urls = HashMap::from([
        (key_server_id, aggregator_url),
    ]);

    let plaintext = client
        .decrypt_object_bytes(
            &bcs::to_bytes(&encrypted)?,
            builder.finish(),
            &session_key,
            aggregator_urls,
        )
        .await?;

    Ok(plaintext)
}
}

For independent servers, pass HashMap::new() (or omit the key server from the map) and the SDK will use the on-chain URL as before.

Mixing independent and committee servers

You can encrypt with multiple key servers where some are independent and others are committee-based. During decryption, only include the committee servers in the aggregator URL map; independent servers will automatically use their on-chain URL:

let aggregator_urls = HashMap::from([
    (committee_key_server_id, "https://aggregator.example.com".to_string()),
    // independent_key_server_id is NOT in the map, so its on-chain URL is used.
]);

let plaintext = client
    .decrypt_object_bytes(&encrypted_bytes, ptb, &session_key, aggregator_urls)
    .await?;

Error handling

When the aggregator is unreachable or returns an error, the SDK treats it the same as a failed independent server: the response is excluded from the threshold count. If too few servers respond, decryption fails with SealClientError::InsufficientKeys.

Advanced Topics

This chapter shows how to plug in custom clients, caches, and transports, plus how to pick the right feature flags and error strategy when you deploy the SDK.

Custom Sui client

You can run BaseSealClient with a different version of the Sui SDK. Implement the SuiClient trait for the client type you want to use and update your Cargo.toml to point at that version.

The built-in implementation tries the V2 dynamic field first (key 2) and falls back to V1 (key 1). V2 supports both independent and committee key servers via a ServerType enum. For committee servers the on-chain URL is empty because the aggregator URL is provided externally through KeyServerConfig.

If you write your own implementation, handle both versions to stay compatible with older and newer key servers. See the default implementation in src/native_sui_sdk/client/sui_client.rs for reference.

Compile seal-sdk-rs against your chosen dependency version and the new implementation becomes active.

Custom HTTP client

HttpClient defines a single method. Implement it for your preferred transport (hyper, surf, a custom blocking client, etc.):

#![allow(unused)]
fn main() {
use std::collections::HashMap;

async fn post<S: ToString + Send + Sync>(
    &self,
    url: &str,
    headers: HashMap<String, String>,
    body: S,
) -> Result<PostResponse, Self::PostError>;
}

Seal key servers only expect HTTP POST requests, so you do not need anything else.

Custom caching

To plug in your own cache, implement SealCache. The key method, try_get_with, either returns a cached value or runs the provided future to populate the cache:

#![allow(unused)]
fn main() {
use std::future::Future;
use std::sync::Arc;

async fn try_get_with<Fut, Error>(
    &self,
    key: Self::Key,
    init: Fut,
) -> Result<Self::Value, Arc<Error>>
where
    Fut: Future<Output = Result<Self::Value, Error>> + Send,
    Error: Send + Sync + 'static;
}

Whenever possible, add request coalescing so you collapse duplicate misses into one in-flight future. This reduces unnecessary parallel calls, keeps you away from Seal server rate limits, and lightens the load on Sui RPC endpoints.

Error handling strategies

Public helpers return Result<_, SealClientError>. Examples and tests sometimes use anyhow::Result<_>. Both styles support the ? operator, so you can bubble errors up or wrap them in your own enums for richer diagnostics.

Deployment considerations

  • Feature gating: Disable the default feature set when you want a custom stack, then re-enable only what you need (for example, --no-default-features --features client,moka-client).
  • Parallel behavior: Encrypt and decrypt helpers send requests in parallel. Keep an eye on quotas for key servers and Sui RPC. Caches plus coalescing help control the traffic.
  • Recovery keys: Decide whether to store the [u8; 32] recovery key that encrypt helpers return. Dropping it removes a potential backdoor. Storing it gives you an emergency path if key servers become unavailable.