> For the complete documentation index, see [llms.txt](https://docs.sigilvault.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.sigilvault.xyz/rust/astrotrain.md).

# astrotrain

Canonical Rust HTTP client for the Sigil SP-facing surface. Two modules:

* **`astrotrain`** root — `AstrotrainClient` for `POST /sp/sign`.
* **`astrotrain::pairing`** — pair sessions, results, events, and revocation.

Chain-agnostic by design — no `alloy`, `solana-*`, or other crypto-suite deps. Use it directly when you need raw signing access, or pull in [`cliffjumper`](/rust/cliffjumper.md) (Ethereum) / [`blurr`](/rust/blurr.md) (Solana) for chain-specific signer traits built on top.

## Install

```toml
[dependencies]
astrotrain = "0.1"
tokio      = { version = "1", features = ["full"] }
```

## Configuration

You need three things:

|                    | Where it comes from                                                              |
| ------------------ | -------------------------------------------------------------------------------- |
| `octane_url`       | `https://api.sigilvault.xyz/octane` (prod) or `http://127.0.0.1:8124` (local)    |
| `api_key`          | Issued against your SP user in ironhide. Bearer auth on every call.              |
| `private_key_seed` | 32-byte Ed25519 seed, base64url-encoded (no padding). Sign envelope per request. |

The seed is **only** used for `astrotrain::sign` — it co-signs each signing request alongside octane and the device. Pair-session calls do not use it; they only need the API key.

## Pair handshake

```rust
use astrotrain::pairing::{
    self, CreatePairSessionRequest, RequestedScope, ScopeAccess,
};

let octane_url = "https://api.sigilvault.xyz/octane";
let api_key = std::env::var("SIGIL_API_KEY")?;

// 1. Mint a session.
let session = pairing::create_pair_session(
    octane_url,
    &api_key,
    CreatePairSessionRequest {
        sp_user_ref: "user-42".into(),
        requested_scopes: vec![
            RequestedScope { chain: "ethereum".into(), access: ScopeAccess::Sign },
            RequestedScope { chain: "polygon".into(),  access: ScopeAccess::Read },
        ],
        expires_in_secs: Some(300),
        return_url: None,
        client_meta: None,
    },
).await?;
// session = { session_id, pair_uri, expires_at, return_url? }

// 2. Show session.pair_uri to the user (QR or deeplink).
//    The user opens Sunstorm and approves.

// 3. Long-poll the result.
let result = pairing::await_pair_result(
    octane_url,
    &api_key,
    &session.session_id,
).await?;
// result = { pair_id, session_id, sp_user_ref, bindings, completed_at }

// 4. Persist result.pair_id against your user row.
```

`PairError` mirrors the same `kind`s as the TypeScript [`@sigil/astrotrain`](/typescript/astrotrain-ts.md) SDK — branch on it exhaustively:

```rust
match err {
    PairError::Network(_)        => /* DNS / refused / abort */,
    PairError::Http { status, body } => /* non-2xx from octane */,
    PairError::Deserialise(_)    => /* response shape didn't match */,
    PairError::MalformedSseFrame => /* SSE frame syntactically broken */,
    PairError::Rejected { reason, .. } => /* user said no */,
    PairError::Expired { .. }    => /* TTL elapsed */,
}
```

## Per-pair management

```rust
// Watch for revocations. Resolves once with the lifecycle event.
let event = pairing::subscribe_pair_events(octane_url, &api_key, pair_id).await?;
match event {
    PairLifecycleEvent::PairRevoked { revoked_at_ms, .. } => /* drop your local state */,
}

// Revoke from your side.
let outcome = pairing::revoke_pairing(octane_url, &api_key, pair_id).await?;
// outcome = { revoked: true|false, pair_id, revoked_at_ms? }
```

`revoke_pairing` is idempotent — second call returns `revoked: false` plus the unchanged row.

## Signing

```rust
use astrotrain::{AstrotrainClient, SigningAlgo};

let client = AstrotrainClient::new(
    "https://api.sigilvault.xyz/octane",
    &api_key,
    &private_key_seed_b64url,   // 32-byte Ed25519 seed, base64url
)?;

let resp = client.sign(
    "0xabc…",                    // device address
    b"transaction bytes",
    SigningAlgo::Secp256k1,      // or Ed25519
).await?;

// resp.device_signature  — base64url, what you submit on-chain
// resp.octane_signature  — base64url, octane's co-signature
// resp.sp_signature      — base64url, your own signature (for audit)
```

### What gets signed by whom

The `sign` call constructs a three-signature envelope:

1. **SP signature** — `astrotrain` does this locally with your seed. Signs `payload_bytes || address_bytes` with Ed25519.
2. **Octane signature** — the cloud co-signer signs the same envelope after applying its policy.
3. **Device signature** — the appliance signs the *payload* (not the envelope) after the user approves on-screen.

You verify the three together; chains don't care about the SP and octane signatures, only the device one. SP and octane signatures matter for audit and policy.

## When to drop down to astrotrain vs use the chain signers

* **Just need the appliance signature on raw bytes?** Use `AstrotrainClient::sign` directly.
* **Want to drop the signer into an alloy or ethers pipeline?** Use `cliffjumper` — it implements `alloy_signer::Signer`.
* **Same for Solana?** Use `blurr` — implements `solana_signer::Signer`.
* **Building a Rust SP that pairs but doesn't sign on-chain (e.g. read-only audit tooling)?** Use only the `astrotrain::pairing` module; you don't need a seed.

## Re-exports in chain signers

Both `cliffjumper` and `blurr` re-export `astrotrain::pairing`, so you can drive the pair handshake without adding `astrotrain` to your `Cargo.toml`:

```rust
use cliffjumper::pairing;   // same as astrotrain::pairing
use blurr::pairing;         // same module
```

If your code only does pair / revoke and never signs, depend on `astrotrain` directly to avoid pulling in the full alloy / solana toolchain.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.sigilvault.xyz/rust/astrotrain.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
