dynamic-waas-sdk 0.0.3

Rust SDK for Dynamic Wallet-as-a-Service — manage wallets from your backend.
Documentation
//! Export-private-key orchestration for both ECDSA and `ExportableEd25519`.
//!
//! Flow (mirrors `python/dynamic_wallet_sdk/wallet_client.py:_export_private_key_mpc`):
//!   1. Compute `export_id` locally via `signer.export_id(share)`.
//!   2. POST `/waas/{walletId}/privateKey/export` with the `export_id` (SSE).
//!   3. SSE delivers `room_created` with the roomId.
//!   4. While the stream stays open, run `export_full_private_key`.
//!   5. SSE delivers `ceremony_complete` (or just closes).
//!
//! For ECDSA: returns the xpriv string. The chain client then calls
//! `derive_private_key_from_xpriv(xpriv, derivation_path)` to get the
//! 32-byte EVM private key.
//!
//! For Ed25519: returns the 32-byte raw Ed25519 private key directly
//! (the engine decodes its hex output for us).

use dynamic_waas_sdk_core::{
    api::{ExportKeyReq, KeygenCompleteEvent},
    sse::{stream_sse_with_callback, SseEventData},
    Error, Result, ServerKeyShare,
};
use dynamic_waas_sdk_mpc::{EcdsaSigner, Ed25519Signer, KeygenId, RoomUuid, SecretShare};
use tracing::{debug, instrument};

use crate::client::DynamicWalletClient;

/// Export an ECDSA wallet's xpriv. Caller is the only "exporter" — server
/// + other client parties cooperate via the relay but receive no key
/// material.
#[instrument(skip(client, share), fields(wallet_id))]
pub async fn run_export_ecdsa(
    client: &DynamicWalletClient,
    wallet_id: &str,
    share: ServerKeyShare,
) -> Result<String> {
    if !client.is_authenticated() {
        return Err(Error::Authentication(crate::AUTH_REQUIRED_MSG.into()));
    }

    let signer = EcdsaSigner::new(client.base_mpc_relay_url().to_string());
    let secret_share = SecretShare::from_string(share.secret_share);
    let export_id = signer.export_id(&secret_share)?;
    let export_id_str = export_id.as_str().to_owned();

    let body = ExportKeyReq {
        export_id: export_id_str.clone(),
        address_type: None,
    };
    let response = client.api().export_key(wallet_id, &body).await?;

    let host_url = client.base_mpc_relay_url().to_string();
    let secret_share_for_cb = secret_share.clone();
    let export_id_for_cb = KeygenId::new(export_id_str);

    let (xpriv_opt, _ceremony_data) =
        stream_sse_with_callback(response, "room_created", move |trigger| async move {
            let event: KeygenCompleteEvent = match trigger {
                SseEventData::Json(v) => serde_json::from_value(v).map_err(Error::from)?,
                SseEventData::Raw(s) => {
                    return Err(Error::Sse(format!(
                        "room_created payload was not JSON: {s}"
                    )))
                }
            };
            debug!(room_id = %event.room_id, "running ECDSA export");

            let signer = EcdsaSigner::new(host_url);
            let room = RoomUuid::new(event.room_id);
            let result = signer
                .export_full_private_key(&room, &secret_share_for_cb, &export_id_for_cb)
                .await?;
            Ok::<_, Error>(result)
        })
        .await?;

    xpriv_opt.ok_or_else(|| {
        Error::InvalidArgument(
            "ECDSA export ceremony returned no xpriv (non-exporter party)".into(),
        )
    })
}

/// Export an Ed25519 (Solana) wallet's private key. Returns the raw
/// bytes the engine delivers — Solana hands back a 64-byte expanded keypair
/// (`seed || pubkey`). Chain clients extract the seed if they want the
/// 32-byte form.
#[instrument(skip(client, share), fields(wallet_id))]
pub async fn run_export_ed25519(
    client: &DynamicWalletClient,
    wallet_id: &str,
    share: ServerKeyShare,
) -> Result<Vec<u8>> {
    if !client.is_authenticated() {
        return Err(Error::Authentication(crate::AUTH_REQUIRED_MSG.into()));
    }

    let signer = Ed25519Signer::new(client.base_mpc_relay_url().to_string());
    let secret_share = SecretShare::from_string(share.secret_share);
    let export_id = signer.export_id(&secret_share)?;
    let export_id_str = export_id.as_str().to_owned();

    let body = ExportKeyReq {
        export_id: export_id_str.clone(),
        address_type: None,
    };
    let response = client.api().export_key(wallet_id, &body).await?;

    let host_url = client.base_mpc_relay_url().to_string();
    let secret_share_for_cb = secret_share.clone();
    let export_id_for_cb = KeygenId::new(export_id_str);

    let (privkey_opt, _ceremony_data) =
        stream_sse_with_callback(response, "room_created", move |trigger| async move {
            let event: KeygenCompleteEvent = match trigger {
                SseEventData::Json(v) => serde_json::from_value(v).map_err(Error::from)?,
                SseEventData::Raw(s) => {
                    return Err(Error::Sse(format!(
                        "room_created payload was not JSON: {s}"
                    )))
                }
            };
            debug!(room_id = %event.room_id, "running Ed25519 export");

            let signer = Ed25519Signer::new(host_url);
            let room = RoomUuid::new(event.room_id);
            let result = signer
                .export_full_private_key(&room, &secret_share_for_cb, &export_id_for_cb)
                .await?;
            Ok::<_, Error>(result)
        })
        .await?;

    privkey_opt.ok_or_else(|| {
        Error::InvalidArgument(
            "Ed25519 export ceremony returned no private key (non-exporter party)".into(),
        )
    })
}