Skip to main content

dynamic_waas_sdk/
sign.rs

1//! Generic MPC sign orchestration. Mirrors
2//! `python/dynamic_wallet_sdk/wallet_client.py:_sign_mpc_message`.
3//!
4//! Flow:
5//!   1. POST `/waas/{walletId}/signMessage` with `{message, isFormatted, ...}`.
6//!   2. SSE delivers `room_created` with `roomId`.
7//!   3. While the stream stays open, run `EcdsaSigner::sign` against the
8//!      relay room.
9//!   4. SSE delivers `ceremony_complete` (or just closes); we already have
10//!      the signature from step 3.
11//!
12//! `is_formatted` semantics (load-bearing — see the Python doc string):
13//!   true  → "this message is already hashed; pass it straight to the relay"
14//!   false → "apply chain-specific formatting before passing to the relay"
15//!
16//! For EVM message signing the client passes
17//! `keccak256(EIP-191-prefixed message)` with `is_formatted = true`, while
18//! the server sees the EIP-191 prefixed hex with `server_is_formatted = false`.
19
20use dynamic_waas_sdk_core::{
21    api::{KeygenCompleteEvent, SignMessageReq},
22    sse::{stream_sse_with_callback, SseEventData},
23    Error, Result, ServerKeyShare,
24};
25use dynamic_waas_sdk_mpc::{EcdsaSignature, EcdsaSigner, MessageHash, RoomUuid, SecretShare};
26use tracing::{debug, instrument};
27
28use crate::client::DynamicWalletClient;
29
30/// Options for [`run_sign_ecdsa`]. All fields required.
31#[derive(Debug, Clone)]
32#[non_exhaustive]
33pub struct SignOpts {
34    /// Wallet id from the cached `WalletProperties`.
35    pub wallet_id: String,
36    /// Pre-hashed message (32 bytes). For EVM, this is
37    /// `keccak256(EIP-191-prefixed message)`.
38    pub msg_hash: [u8; 32],
39    /// Sent in the request body. May differ from `msg_hash` — e.g. for
40    /// EVM message signing the body carries the EIP-191 prefixed hex
41    /// while the relay gets the keccak256 hash.
42    pub server_message: String,
43    /// `is_formatted` flag the server sees. For EVM message signing,
44    /// `false` (server applies its own formatting).
45    pub server_is_formatted: bool,
46    /// The client's MPC share for the wallet.
47    pub secret_share: ServerKeyShare,
48    /// BIP-32 derivation path (for ECDSA). Use the chain's
49    /// `derivation_path` from `mpc_config`.
50    pub derivation_path: Vec<u32>,
51}
52
53impl SignOpts {
54    pub fn new(
55        wallet_id: impl Into<String>,
56        msg_hash: [u8; 32],
57        server_message: impl Into<String>,
58        server_is_formatted: bool,
59        secret_share: ServerKeyShare,
60        derivation_path: Vec<u32>,
61    ) -> Self {
62        Self {
63            wallet_id: wallet_id.into(),
64            msg_hash,
65            server_message: server_message.into(),
66            server_is_formatted,
67            secret_share,
68            derivation_path,
69        }
70    }
71}
72
73#[instrument(skip(client, opts), fields(wallet_id = %opts.wallet_id))]
74pub async fn run_sign_ecdsa(
75    client: &DynamicWalletClient,
76    opts: SignOpts,
77) -> Result<EcdsaSignature> {
78    if !client.is_authenticated() {
79        return Err(Error::Authentication(crate::AUTH_REQUIRED_MSG.into()));
80    }
81
82    let body = SignMessageReq {
83        message: opts.server_message,
84        is_formatted: opts.server_is_formatted,
85        server_is_formatted: None,
86        context: None,
87    };
88    let response = client
89        .api()
90        .sign_message_with_callback(&opts.wallet_id, &body)
91        .await?;
92
93    let host_url = client.base_mpc_relay_url().to_string();
94    let msg_hash = opts.msg_hash;
95    let derivation_path = opts.derivation_path.clone();
96    let secret_share = SecretShare::from_string(opts.secret_share.secret_share);
97
98    let (signature, _ceremony_data) =
99        stream_sse_with_callback(response, "room_created", move |trigger| async move {
100            let event: KeygenCompleteEvent = match trigger {
101                SseEventData::Json(v) => serde_json::from_value(v).map_err(Error::from)?,
102                SseEventData::Raw(s) => {
103                    return Err(Error::Sse(format!(
104                        "room_created payload was not JSON: {s}"
105                    )))
106                }
107            };
108            debug!(room_id = %event.room_id, "running MPC sign");
109
110            let signer = EcdsaSigner::new(host_url);
111            let room = RoomUuid::new(event.room_id);
112            let hash = MessageHash(msg_hash);
113            let sig = signer
114                .sign(&room, &secret_share, &hash, &derivation_path)
115                .await?;
116            Ok::<_, Error>(sig)
117        })
118        .await?;
119
120    Ok(signature)
121}