cdk-spilman 0.17.6

Standalone Spilman payment channels library for Cashu
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
use super::*;

// ============================================================================
// SWAP-TO-FUNDING FUNCTIONS
// ============================================================================
// These functions allow creating channel funding from existing wallet tokens
// (via swap) instead of minting fresh tokens.

/// Compute channel parameters from a Cashu token
///
/// Given a token string (cashuA.../cashuB...), computes the channel capacity,
/// funding token nominal amount, and change amount. Also builds the channel
/// parameters ready for use.
///
/// # Arguments
/// * `token_string` - The Cashu token (cashuA... or cashuB...)
/// * `receiver_pubkey_hex` - Receiver's public key (hex)
/// * `sender_pubkey_hex` - Sender's public key (hex)
/// * `channel_secret_hex` - Pre-computed ECDH channel secret (32 bytes, hex)
/// * `expiry_timestamp` - Unix timestamp for channel expiry (refund becomes available)
/// * `keyset_info_json` - Keyset info from mint (JSON)
/// * `maximum_amount_for_one_output` - Max amount per output from server policy
///
/// # Returns
/// JSON with:
/// - `capacity`: Channel capacity (final value after all fees)
/// - `funding_token_amount`: Nominal value of the funding token
/// - `input_value`: Total value of input proofs
/// - `mint_url`: Mint URL from the token
/// - `params_json`: Serialized channel params for use in later functions
/// - `proofs_json`: The parsed proofs from the token (for create_funding_swap)
pub fn compute_channel_from_token(
    token_string: &str,
    receiver_pubkey_hex: &str,
    sender_pubkey_hex: &str,
    channel_secret_hex: &str,
    expiry_timestamp: u64,
    keyset_info_json: &str,
    maximum_amount_for_one_output: u64,
) -> Result<String, String> {
    // Parse the token
    let token: Token = token_string
        .parse()
        .map_err(|e| format!("Failed to parse token: {}", e))?;

    // Get total value from token (doesn't need keyset info)
    let input_value: u64 = token
        .value()
        .map_err(|e| format!("Failed to get token value: {}", e))?
        .into();

    // Get mint URL
    let mint_url = token
        .mint_url()
        .map_err(|e| format!("Failed to get mint URL: {}", e))?;

    // Get unit from token
    let unit = token.unit().unwrap_or(CurrencyUnit::Sat);

    // Parse keyset info
    let keyset_info = parse_keyset_info_from_json(keyset_info_json)?;

    // Parse proofs using keyset info
    // We need to create a KeySetInfo (nut02) for the token's proofs() method
    let nut02_keyset_info = cashu::nuts::KeySetInfo {
        id: keyset_info.keyset_id,
        unit: unit.clone(),
        active: true,
        input_fee_ppk: keyset_info.input_fee_ppk,
        final_expiry: None,
    };
    let proofs = token
        .proofs(&[nut02_keyset_info])
        .map_err(|e| format!("Failed to parse proofs: {}", e))?;

    // Assert all proofs are from the same keyset
    for proof in &proofs {
        if proof.keyset_id != keyset_info.keyset_id {
            return Err(format!(
                "All proofs must be from the same keyset. Expected {}, got {}",
                keyset_info.keyset_id, proof.keyset_id
            ));
        }
    }

    let max_amt = maximum_amount_for_one_output;

    // Step 1: funding_token_amount = forward_fees(input_value) - value after swap's input fees
    // This is the nominal value of the funding token after swapping wallet proofs
    let funding_token_amount = keyset_info
        .deterministic_value_after_fees(input_value, max_amt)
        .map_err(|e| format!("Failed to compute funding_token_amount: {}", e))?;

    // Step 2: capacity = forward(forward(funding_token_amount)) - value after both close stages
    let v2 = keyset_info
        .deterministic_value_after_fees(funding_token_amount, max_amt)
        .map_err(|e| format!("Failed to compute v2: {}", e))?;
    let capacity = keyset_info
        .deterministic_value_after_fees(v2, max_amt)
        .map_err(|e| format!("Failed to compute capacity: {}", e))?;

    // Parse sender pubkey
    let sender_pubkey: PublicKey = sender_pubkey_hex
        .parse()
        .map_err(|e| format!("Invalid sender pubkey: {}", e))?;

    // Parse receiver pubkey
    let receiver_pubkey: PublicKey = receiver_pubkey_hex
        .parse()
        .map_err(|e| format!("Invalid receiver pubkey: {}", e))?;

    // Parse channel secret
    let channel_secret_bytes = hex::decode(channel_secret_hex)
        .map_err(|e| format!("Invalid channel secret hex: {}", e))?;
    if channel_secret_bytes.len() != 32 {
        return Err(format!(
            "Channel secret must be 32 bytes, got {}",
            channel_secret_bytes.len()
        ));
    }
    let mut channel_secret = [0u8; 32];
    channel_secret.copy_from_slice(&channel_secret_bytes);

    // Create channel parameters with pre-computed channel secret
    let params = ChannelParameters::new(
        sender_pubkey,
        receiver_pubkey,
        mint_url.to_string(),
        unit,
        capacity,
        funding_token_amount,
        expiry_timestamp,
        unix_time(),
        keyset_info.clone(),
        max_amt,
        channel_secret,
    )
    .map_err(|e| format!("Failed to create channel params: {}", e))?;

    // Serialize proofs
    let proofs_json =
        serde_json::to_string(&proofs).map_err(|e| format!("Failed to serialize proofs: {}", e))?;

    // Serialize params
    let params_json = params.get_channel_id_params_json();

    // Build result
    let result = serde_json::json!({
        "capacity": capacity,
        "funding_token_amount": funding_token_amount,
        "input_value": input_value,
        "mint_url": mint_url.to_string(),
        "params_json": params_json,
        "proofs_json": proofs_json
    });

    Ok(result.to_string())
}

/// Create a swap request for funding a channel from existing proofs
///
/// Takes input proofs and creates a swap request with deterministic
/// funding outputs (2-of-2 locked).
///
/// # Arguments
/// * `params_json` - Channel params JSON (from compute_channel_from_token)
/// * `channel_secret_hex` - Pre-computed ECDH channel secret (32 bytes, hex)
/// * `keyset_info_json` - Keyset info (JSON)
/// * `input_proofs_json` - Input proofs from the token (JSON array)
///
/// # Returns
/// JSON with:
/// - `swap_request_json`: The swap request to send to mint (JSON)
/// - `funding_secrets_json`: Secrets for unblinding funding outputs (JSON array)
/// - `funding_count`: Number of funding outputs
pub fn create_funding_swap(
    params_json: &str,
    channel_secret_hex: &str,
    keyset_info_json: &str,
    input_proofs_json: &str,
) -> Result<String, String> {
    // Parse keyset info
    let keyset_info = parse_keyset_info_from_json(keyset_info_json)?;

    // Parse channel secret
    let channel_secret_bytes = hex::decode(channel_secret_hex)
        .map_err(|e| format!("Invalid channel secret hex: {}", e))?;
    if channel_secret_bytes.len() != 32 {
        return Err(format!(
            "Channel secret must be 32 bytes, got {}",
            channel_secret_bytes.len()
        ));
    }
    let mut channel_secret = [0u8; 32];
    channel_secret.copy_from_slice(&channel_secret_bytes);

    // Create ChannelParameters from JSON with pre-computed channel secret
    let params = ChannelParameters::from_json_with_channel_secret(
        params_json,
        keyset_info.clone(),
        channel_secret,
    )
    .map_err(|e| format!("Failed to create ChannelParameters: {}", e))?;

    // Parse input proofs
    let input_proofs: Vec<Proof> = serde_json::from_str(input_proofs_json)
        .map_err(|e| format!("Failed to parse input proofs: {}", e))?;

    // Get the funding token nominal amount
    let funding_token_nominal = params
        .get_total_funding_token_amount()
        .map_err(|e| format!("Failed to compute funding token amount: {}", e))?;

    // Create deterministic funding outputs
    let funding_outputs = DeterministicOutputsForOneContext::new(
        "funding".to_string(),
        funding_token_nominal,
        params,
    )
    .map_err(|e| format!("Failed to create funding outputs: {}", e))?;

    // Get funding blinded messages
    let funding_blinded_messages = funding_outputs
        .get_blinded_messages(None)
        .map_err(|e| format!("Failed to get funding blinded messages: {}", e))?;

    // Get funding secrets with blinding
    let funding_secrets = funding_outputs
        .get_secrets_with_blinding()
        .map_err(|e| format!("Failed to get funding secrets: {}", e))?;

    // Create swap request
    let swap_request = SwapRequest::new(input_proofs, funding_blinded_messages);

    // Serialize swap request
    let swap_request_json = serde_json::to_string(&swap_request)
        .map_err(|e| format!("Failed to serialize swap request: {}", e))?;

    // Serialize funding secrets
    let funding_secrets_json: Vec<serde_json::Value> = funding_secrets
        .iter()
        .map(|swb| {
            serde_json::json!({
                "secret": swb.secret.to_string(),
                "blinding_factor": swb.blinding_factor.to_secret_hex(),
                "amount": swb.amount
            })
        })
        .collect();

    let funding_secrets_str = serde_json::to_string(&funding_secrets_json)
        .map_err(|e| format!("Failed to serialize funding secrets: {}", e))?;

    // Build result
    let result = serde_json::json!({
        "swap_request_json": swap_request_json,
        "funding_secrets_json": funding_secrets_str,
        "funding_count": funding_secrets.len()
    });

    Ok(result.to_string())
}

/// Complete a funding swap by unblinding the mint's response
///
/// Takes the mint's swap response and unblinding the funding proofs.
/// Also verifies DLEQ proofs on all signatures.
///
/// # Arguments
/// * `swap_response_json` - Mint's swap response (JSON with "signatures" array)
/// * `funding_secrets_json` - Funding secrets from create_funding_swap (JSON array)
/// * `keyset_info_json` - Keyset info (JSON)
///
/// # Returns
/// JSON with:
/// - `funding_proofs_json`: Funding proofs for channel (JSON array)
#[cfg(feature = "wallet")]
pub fn complete_funding_swap(
    swap_response_json: &str,
    funding_secrets_json: &str,
    keyset_info_json: &str,
) -> Result<String, String> {
    // Parse keyset info
    let keyset_info = parse_keyset_info_from_json(keyset_info_json)?;
    let keys = keyset_info.active_keys.clone();

    // Parse swap response to get signatures
    let response: serde_json::Value = serde_json::from_str(swap_response_json)
        .map_err(|e| format!("Failed to parse swap response: {}", e))?;

    let signatures_raw = response["signatures"]
        .as_array()
        .ok_or("Missing 'signatures' in swap response")?;

    // Parse funding secrets
    let funding_secrets_raw: Vec<serde_json::Value> = serde_json::from_str(funding_secrets_json)
        .map_err(|e| format!("Failed to parse funding secrets: {}", e))?;

    let funding_count = funding_secrets_raw.len();

    // Verify signature count matches
    if signatures_raw.len() != funding_count {
        return Err(format!(
            "Signature count mismatch: expected {}, got {}",
            funding_count,
            signatures_raw.len()
        ));
    }

    // Helper to parse and verify signatures
    let parse_signatures = |sigs: &[serde_json::Value]| -> Result<Vec<BlindSignature>, String> {
        let mut result = Vec::new();
        for (i, sig) in sigs.iter().enumerate() {
            let amount = sig["amount"]
                .as_u64()
                .ok_or_else(|| format!("Missing 'amount' in signature {}", i))?;
            let id_str = sig["id"]
                .as_str()
                .ok_or_else(|| format!("Missing 'id' in signature {}", i))?;
            let c_str = sig["C_"]
                .as_str()
                .ok_or_else(|| format!("Missing 'C_' in signature {}", i))?;

            let keyset_id: Id = id_str
                .parse()
                .map_err(|e| format!("Invalid keyset id in signature {}: {}", i, e))?;
            let c = PublicKey::from_str(c_str)
                .map_err(|e| format!("Invalid C_ in signature {}: {}", i, e))?;

            // Parse DLEQ - required for Spilman channels
            let dleq_obj = sig["dleq"].as_object().ok_or_else(|| {
                format!(
                    "Missing 'dleq' in signature {} - DLEQ proofs are required",
                    i
                )
            })?;
            let e_str = dleq_obj
                .get("e")
                .and_then(|v| v.as_str())
                .ok_or_else(|| format!("Missing 'e' in dleq for signature {}", i))?;
            let s_str = dleq_obj
                .get("s")
                .and_then(|v| v.as_str())
                .ok_or_else(|| format!("Missing 's' in dleq for signature {}", i))?;
            let e = SecretKey::from_hex(e_str)
                .map_err(|e| format!("Invalid dleq.e in signature {}: {}", i, e))?;
            let s = SecretKey::from_hex(s_str)
                .map_err(|e| format!("Invalid dleq.s in signature {}: {}", i, e))?;
            let dleq = BlindSignatureDleq { e, s };

            result.push(BlindSignature {
                amount: Amount::from(amount),
                keyset_id,
                c,
                dleq: Some(dleq),
            });
        }
        Ok(result)
    };

    // Helper to parse secrets
    let parse_secrets =
        |secrets: &[serde_json::Value]| -> Result<(Vec<Secret>, Vec<SecretKey>), String> {
            let mut result_secrets = Vec::new();
            let mut result_rs = Vec::new();
            for (i, swb) in secrets.iter().enumerate() {
                let secret_str = swb["secret"]
                    .as_str()
                    .ok_or_else(|| format!("Missing 'secret' in secrets {}", i))?;
                let blinding_factor_hex = swb["blinding_factor"]
                    .as_str()
                    .ok_or_else(|| format!("Missing 'blinding_factor' in secrets {}", i))?;

                let secret: Secret = secret_str
                    .parse()
                    .map_err(|e| format!("Invalid secret {}: {}", i, e))?;
                let r = SecretKey::from_hex(blinding_factor_hex)
                    .map_err(|e| format!("Invalid blinding factor {}: {}", i, e))?;

                result_secrets.push(secret);
                result_rs.push(r);
            }
            Ok((result_secrets, result_rs))
        };

    // Parse funding signatures and secrets
    let funding_blind_sigs = parse_signatures(signatures_raw)?;
    let (funding_secrets, funding_rs) = parse_secrets(&funding_secrets_raw)?;

    // Construct funding proofs (includes DLEQ verification)
    #[cfg(feature = "wallet")]
    let funding_proofs =
        dhke_construct_proofs(funding_blind_sigs, funding_rs, funding_secrets, &keys).map_err(
            |e| {
                format!(
                    "Failed to construct funding proofs (DLEQ verification failed?): {}",
                    e
                )
            },
        )?;

    #[cfg(not(feature = "wallet"))]
    let funding_proofs: Vec<Proof> = Vec::new(); // Stub for non-wallet builds
    #[cfg(not(feature = "wallet"))]
    let _ = (funding_blind_sigs, funding_rs, funding_secrets, keys); // suppress unused warnings

    // Serialize results
    let funding_proofs_json = serde_json::to_string(&funding_proofs)
        .map_err(|e| format!("Failed to serialize funding proofs: {}", e))?;

    let result = serde_json::json!({
        "funding_proofs_json": funding_proofs_json
    });

    Ok(result.to_string())
}