dynamic-waas-sdk 0.0.3

Rust SDK for Dynamic Wallet-as-a-Service — manage wallets from your backend.
Documentation
//! Generic MPC sign orchestration. Mirrors
//! `python/dynamic_wallet_sdk/wallet_client.py:_sign_mpc_message`.
//!
//! Flow:
//!   1. POST `/waas/{walletId}/signMessage` with `{message, isFormatted, ...}`.
//!   2. SSE delivers `room_created` with `roomId`.
//!   3. While the stream stays open, run `EcdsaSigner::sign` against the
//!      relay room.
//!   4. SSE delivers `ceremony_complete` (or just closes); we already have
//!      the signature from step 3.
//!
//! `is_formatted` semantics (load-bearing — see the Python doc string):
//!   true  → "this message is already hashed; pass it straight to the relay"
//!   false → "apply chain-specific formatting before passing to the relay"
//!
//! For EVM message signing the client passes
//! `keccak256(EIP-191-prefixed message)` with `is_formatted = true`, while
//! the server sees the EIP-191 prefixed hex with `server_is_formatted = false`.

use dynamic_waas_sdk_core::{
    api::{KeygenCompleteEvent, SignMessageReq},
    sse::{stream_sse_with_callback, SseEventData},
    Error, Result, ServerKeyShare,
};
use dynamic_waas_sdk_mpc::{EcdsaSignature, EcdsaSigner, MessageHash, RoomUuid, SecretShare};
use tracing::{debug, instrument};

use crate::client::DynamicWalletClient;

/// Options for [`run_sign_ecdsa`]. All fields required.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SignOpts {
    /// Wallet id from the cached `WalletProperties`.
    pub wallet_id: String,
    /// Pre-hashed message (32 bytes). For EVM, this is
    /// `keccak256(EIP-191-prefixed message)`.
    pub msg_hash: [u8; 32],
    /// Sent in the request body. May differ from `msg_hash` — e.g. for
    /// EVM message signing the body carries the EIP-191 prefixed hex
    /// while the relay gets the keccak256 hash.
    pub server_message: String,
    /// `is_formatted` flag the server sees. For EVM message signing,
    /// `false` (server applies its own formatting).
    pub server_is_formatted: bool,
    /// The client's MPC share for the wallet.
    pub secret_share: ServerKeyShare,
    /// BIP-32 derivation path (for ECDSA). Use the chain's
    /// `derivation_path` from `mpc_config`.
    pub derivation_path: Vec<u32>,
}

impl SignOpts {
    pub fn new(
        wallet_id: impl Into<String>,
        msg_hash: [u8; 32],
        server_message: impl Into<String>,
        server_is_formatted: bool,
        secret_share: ServerKeyShare,
        derivation_path: Vec<u32>,
    ) -> Self {
        Self {
            wallet_id: wallet_id.into(),
            msg_hash,
            server_message: server_message.into(),
            server_is_formatted,
            secret_share,
            derivation_path,
        }
    }
}

#[instrument(skip(client, opts), fields(wallet_id = %opts.wallet_id))]
pub async fn run_sign_ecdsa(
    client: &DynamicWalletClient,
    opts: SignOpts,
) -> Result<EcdsaSignature> {
    if !client.is_authenticated() {
        return Err(Error::Authentication(crate::AUTH_REQUIRED_MSG.into()));
    }

    let body = SignMessageReq {
        message: opts.server_message,
        is_formatted: opts.server_is_formatted,
        server_is_formatted: None,
        context: None,
    };
    let response = client
        .api()
        .sign_message_with_callback(&opts.wallet_id, &body)
        .await?;

    let host_url = client.base_mpc_relay_url().to_string();
    let msg_hash = opts.msg_hash;
    let derivation_path = opts.derivation_path.clone();
    let secret_share = SecretShare::from_string(opts.secret_share.secret_share);

    let (signature, _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 MPC sign");

            let signer = EcdsaSigner::new(host_url);
            let room = RoomUuid::new(event.room_id);
            let hash = MessageHash(msg_hash);
            let sig = signer
                .sign(&room, &secret_share, &hash, &derivation_path)
                .await?;
            Ok::<_, Error>(sig)
        })
        .await?;

    Ok(signature)
}