Skip to main content

dynamic_waas_sdk/
keygen_ed25519.rs

1//! Ed25519 (`ExportableEd25519`) keygen orchestration. SVM equivalent of
2//! `keygen.rs::run_keygen` for ECDSA.
3//!
4//! Flow is identical at the SSE / API layer:
5//!   1. `init_keygen` (sync, `Ed25519Signer`) — generates 32-byte secret + base58 `keygen_id`
6//!   2. POST /waas/create (SSE)
7//!   3. SSE `keygen_complete` → run `Ed25519Signer.receive_key` against the relay
8//!   4. SSE `ceremony_complete` → carries `walletId`
9//!   5. `derive_pubkey` (sync) — 32-byte raw Ed25519 pubkey
10//!   6. `mark_key_shares_as_backed_up` (location: external)
11
12use std::sync::Arc;
13
14use dynamic_waas_sdk_core::{
15    api::{CreateWalletKeygenReq, KeygenCompleteEvent},
16    sse::{stream_sse_keygen, SseEventData},
17    Error, Result, ServerKeyShare, ThresholdSignatureScheme, WalletProperties,
18};
19use dynamic_waas_sdk_mpc::{Ed25519Signer, KeygenId, RoomUuid, SecretShare};
20use tracing::{debug, instrument};
21
22use crate::client::DynamicWalletClient;
23use crate::mpc_config::{threshold_wire, MpcSchemeConfig};
24
25#[derive(Debug)]
26pub struct KeygenOutputEd25519 {
27    pub wallet_properties: WalletProperties,
28    pub external_server_key_shares: Vec<ServerKeyShare>,
29    pub raw_public_key: [u8; 32],
30}
31
32#[derive(Debug, Clone)]
33#[non_exhaustive]
34pub struct KeygenOptsEd25519 {
35    pub chain_name: String,
36    pub threshold_signature_scheme: ThresholdSignatureScheme,
37    pub derivation_path: Vec<u32>,
38    /// See [`KeygenOpts::back_up_to_dynamic`](crate::KeygenOpts).
39    pub back_up_to_dynamic: bool,
40    /// See [`KeygenOpts::password`](crate::KeygenOpts).
41    pub password: Option<String>,
42}
43
44impl KeygenOptsEd25519 {
45    pub fn new(
46        chain_name: impl Into<String>,
47        threshold_signature_scheme: ThresholdSignatureScheme,
48        derivation_path: Vec<u32>,
49    ) -> Self {
50        Self {
51            chain_name: chain_name.into(),
52            threshold_signature_scheme,
53            derivation_path,
54            back_up_to_dynamic: true,
55            password: None,
56        }
57    }
58
59    #[must_use]
60    pub fn with_back_up_to_dynamic(mut self, value: bool) -> Self {
61        self.back_up_to_dynamic = value;
62        self
63    }
64
65    #[must_use]
66    pub fn with_password(mut self, password: impl Into<String>) -> Self {
67        self.password = Some(password.into());
68        self
69    }
70}
71
72#[instrument(skip(client), fields(chain = %opts.chain_name))]
73#[allow(clippy::too_many_lines)]
74pub async fn run_keygen_ed25519(
75    client: &DynamicWalletClient,
76    opts: KeygenOptsEd25519,
77) -> Result<KeygenOutputEd25519> {
78    if !client.is_authenticated() {
79        return Err(Error::Authentication(crate::AUTH_REQUIRED_MSG.into()));
80    }
81
82    crate::keygen::validate_password_for_backup(opts.password.as_deref(), opts.back_up_to_dynamic)?;
83
84    let scheme_cfg = MpcSchemeConfig::from(opts.threshold_signature_scheme);
85
86    // 1. Client init keygen.
87    let signer = Ed25519Signer::new(client.base_mpc_relay_url().to_string());
88    let mut init_results = Vec::with_capacity(scheme_cfg.client_threshold as usize);
89    for _ in 0..scheme_cfg.client_threshold {
90        init_results.push(signer.init_keygen()?);
91    }
92    let client_keygen_ids: Vec<String> = init_results
93        .iter()
94        .map(|r| r.keygen_id.as_str().to_owned())
95        .collect();
96
97    // 2. POST /waas/create — opens SSE stream.
98    let body = CreateWalletKeygenReq {
99        chain: opts.chain_name.clone(),
100        client_keygen_ids: client_keygen_ids.clone(),
101        threshold_signature_scheme: threshold_wire(opts.threshold_signature_scheme).to_string(),
102        derivation_path: Some(opts.derivation_path.clone()),
103        address_type: None,
104    };
105    let response = client.api().create_wallet_keygen(&body).await?;
106
107    // 3+4+5. Stream stays open during MPC ceremony.
108    let host_url = client.base_mpc_relay_url().to_string();
109    let init_results = Arc::new(init_results);
110    let init_results_for_cb = Arc::clone(&init_results);
111    let client_keygen_ids_for_cb = client_keygen_ids.clone();
112    let scheme_n = scheme_cfg.n;
113    let scheme_t = scheme_cfg.t;
114
115    let (mpc_results, ceremony_data) = stream_sse_keygen(response, move |trigger| async move {
116        let event: KeygenCompleteEvent = match trigger {
117            SseEventData::Json(v) => serde_json::from_value(v).map_err(Error::from)?,
118            SseEventData::Raw(s) => {
119                return Err(Error::Sse(format!(
120                    "keygen_complete payload was not JSON: {s}"
121                )))
122            }
123        };
124        debug!(room_id = %event.room_id, "running MPC receive_key");
125
126        let signer = Ed25519Signer::new(host_url);
127        let room = RoomUuid::new(event.room_id);
128
129        let mut results = Vec::with_capacity(init_results_for_cb.len());
130        for (i, init) in init_results_for_cb.iter().enumerate() {
131            let other_external_ids: Vec<KeygenId> = client_keygen_ids_for_cb
132                .iter()
133                .enumerate()
134                .filter(|(j, _)| *j != i)
135                .map(|(_, id)| KeygenId::new(id.clone()))
136                .collect();
137            let server_ids: Vec<KeygenId> = event
138                .server_keygen_ids
139                .iter()
140                .map(|s| KeygenId::new(s.clone()))
141                .collect();
142            let all_others: Vec<KeygenId> =
143                server_ids.into_iter().chain(other_external_ids).collect();
144
145            let result = signer
146                .receive_key(&room, scheme_n, scheme_t, &init.keygen_secret, &all_others)
147                .await?;
148            results.push(result);
149        }
150        Ok::<_, Error>(results)
151    })
152    .await?;
153
154    // 6. derive pubkey.
155    let first = mpc_results
156        .first()
157        .ok_or(Error::Mpc(dynamic_waas_sdk_mpc::MpcError::Unknown))?;
158    let signer = Ed25519Signer::new(client.base_mpc_relay_url().to_string());
159    let raw_pubkey = signer.derive_pubkey(&first.secret_share)?;
160
161    let mut wallet_id = String::new();
162    if let Some(SseEventData::Json(v)) = &ceremony_data {
163        if let Some(id) = v.get("walletId").and_then(|x| x.as_str()) {
164            id.clone_into(&mut wallet_id);
165        }
166    }
167    let mut wp = WalletProperties::new(
168        opts.chain_name.clone(),
169        wallet_id.clone(),
170        String::new(), // chain client fills account_address
171    )
172    .with_threshold(opts.threshold_signature_scheme)
173    .with_derivation_path(opts.derivation_path.clone());
174
175    let key_shares: Vec<ServerKeyShare> = mpc_results
176        .into_iter()
177        .enumerate()
178        .map(|(i, kr)| {
179            ServerKeyShare::new(client_keygen_ids[i].clone(), kr.secret_share.into_string())
180        })
181        .collect();
182
183    // Activate the wallet. See `keygen.rs` for the dual-path rationale —
184    // same logic, but with Ed25519Signer::export_id for the keygen_id.
185    if !wallet_id.is_empty() {
186        let signer = Ed25519Signer::new(client.base_mpc_relay_url().to_string());
187        let keygen_id = signer
188            .export_id(&SecretShare::from_string(
189                key_shares[0].secret_share.clone(),
190            ))?
191            .into_string();
192        if opts.back_up_to_dynamic {
193            let password = opts.password.as_deref().unwrap_or("");
194            let backup_info = crate::backup::run_backup_dynamic(
195                client,
196                &wallet_id,
197                &key_shares,
198                &keygen_id,
199                password,
200            )
201            .await?;
202            wp.external_server_key_shares_backup_info = Some(backup_info);
203        } else {
204            crate::backup::run_mark_external_no_backup(client, &wallet_id, &key_shares, &keygen_id)
205                .await?;
206        }
207    }
208
209    Ok(KeygenOutputEd25519 {
210        wallet_properties: wp,
211        external_server_key_shares: key_shares,
212        raw_public_key: raw_pubkey,
213    })
214}