@bennyblader/ddk-rn 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1093 @@
1
+ #![allow(unused_imports, unused_variables)]
2
+ #![allow(clippy::too_many_arguments)]
3
+ use bitcoin::consensus::{Decodable, Encodable};
4
+ use bitcoin::hashes::Hash;
5
+ use bitcoin::sighash::{EcdsaSighashType, SighashCache};
6
+ use bitcoin::transaction::Version;
7
+ use bitcoin::{
8
+ Amount, OutPoint, ScriptBuf, Sequence, Transaction as BtcTransaction, TxIn, TxOut as BtcTxOut,
9
+ Txid, Witness,
10
+ };
11
+ use bitcoin::{PubkeyHash, WPubkeyHash};
12
+ use dlc::{
13
+ self, dlc_input::DlcInputInfo as RustDlcInputInfo, DlcTransactions as RustDlcTransactions,
14
+ OracleInfo as DlcOracleInfo, PartyParams as DlcPartyParams, Payout as DlcPayout,
15
+ TxInputInfo as DlcTxInputInfo,
16
+ };
17
+ use secp256k1_zkp::{
18
+ ecdsa::Signature as EcdsaSignature, Message, PublicKey, Secp256k1, SecretKey, XOnlyPublicKey,
19
+ };
20
+ use secp256k1_zkp::{schnorr::Signature as SchnorrSignature, EcdsaAdaptorSignature};
21
+ use std::str::FromStr;
22
+
23
+ uniffi::include_scaffolding!("ddk_ffi");
24
+
25
+ pub fn version() -> String {
26
+ env!("CARGO_PKG_VERSION").to_string()
27
+ }
28
+
29
+ /// Minimum value that can be included in a transaction output. Under this value,
30
+ /// outputs are discarded
31
+ /// See: https://github.com/discreetlogcontracts/dlcspecs/blob/master/Transactions.md#change-outputs
32
+ const DUST_LIMIT: u64 = 1000;
33
+
34
+ /// The witness size of a P2WPKH input
35
+ /// See: <https://github.com/discreetlogcontracts/dlcspecs/blob/master/Transactions.md#fees>
36
+ pub const P2WPKH_WITNESS_SIZE: usize = 107;
37
+
38
+ // Error type implementation
39
+ #[derive(Debug, thiserror::Error)]
40
+ pub enum DLCError {
41
+ #[error("Invalid signature")]
42
+ InvalidSignature,
43
+ #[error("Invalid public key")]
44
+ InvalidPublicKey,
45
+ #[error("Invalid transaction")]
46
+ InvalidTransaction,
47
+ #[error("Insufficient funds")]
48
+ InsufficientFunds,
49
+ #[error("Invalid argument")]
50
+ InvalidArgument,
51
+ #[error("Serialization error")]
52
+ SerializationError,
53
+ #[error("Secp256k1 error: {0}")]
54
+ Secp256k1Error(String),
55
+ #[error("Miniscript error")]
56
+ MiniscriptError,
57
+ #[error("Network error")]
58
+ NetworkError,
59
+ }
60
+
61
+ impl From<dlc::Error> for DLCError {
62
+ fn from(err: dlc::Error) -> Self {
63
+ match err {
64
+ dlc::Error::Secp256k1(_) => DLCError::Secp256k1Error(err.to_string()),
65
+ dlc::Error::InvalidArgument => DLCError::InvalidArgument,
66
+ dlc::Error::Miniscript(_) => DLCError::MiniscriptError,
67
+ dlc::Error::P2wpkh(_) => DLCError::InvalidTransaction,
68
+ dlc::Error::InputsIndex(_) => DLCError::InvalidArgument,
69
+ }
70
+ }
71
+ }
72
+
73
+ impl From<secp256k1_zkp::Error> for DLCError {
74
+ fn from(err: secp256k1_zkp::Error) -> Self {
75
+ DLCError::Secp256k1Error(err.to_string())
76
+ }
77
+ }
78
+
79
+ // UniFFI struct definitions (as defined in UDL)
80
+ #[derive(Clone)]
81
+ pub struct Transaction {
82
+ pub version: i32,
83
+ pub lock_time: u32,
84
+ pub inputs: Vec<TxInput>,
85
+ pub outputs: Vec<TxOutput>,
86
+ pub raw_bytes: Vec<u8>,
87
+ }
88
+
89
+ #[derive(Clone)]
90
+ pub struct TxInput {
91
+ pub txid: String,
92
+ pub vout: u32,
93
+ pub script_sig: Vec<u8>,
94
+ pub sequence: u32,
95
+ pub witness: Vec<Vec<u8>>,
96
+ }
97
+
98
+ #[derive(Clone)]
99
+ pub struct TxOutput {
100
+ pub value: u64,
101
+ pub script_pubkey: Vec<u8>,
102
+ }
103
+
104
+ #[derive(Clone)]
105
+ pub struct TxInputInfo {
106
+ pub txid: String,
107
+ pub vout: u32,
108
+ pub script_sig: Vec<u8>,
109
+ pub max_witness_length: u32,
110
+ pub serial_id: u64,
111
+ }
112
+
113
+ #[derive(Clone)]
114
+ pub struct DlcOutcome {
115
+ pub local_payout: u64,
116
+ pub remote_payout: u64,
117
+ }
118
+
119
+ #[derive(Clone)]
120
+ pub struct Payout {
121
+ pub offer: u64,
122
+ pub accept: u64,
123
+ }
124
+
125
+ #[derive(Clone)]
126
+ pub struct DlcInputInfo {
127
+ pub fund_tx: Transaction,
128
+ pub fund_vout: u32,
129
+ pub local_fund_pubkey: Vec<u8>,
130
+ pub remote_fund_pubkey: Vec<u8>,
131
+ pub fund_amount: u64,
132
+ pub max_witness_len: u32,
133
+ pub input_serial_id: u64,
134
+ pub contract_id: Vec<u8>,
135
+ }
136
+
137
+ #[derive(Clone)]
138
+ pub struct PartyParams {
139
+ pub fund_pubkey: Vec<u8>,
140
+ pub change_script_pubkey: Vec<u8>,
141
+ pub change_serial_id: u64,
142
+ pub payout_script_pubkey: Vec<u8>,
143
+ pub payout_serial_id: u64,
144
+ pub inputs: Vec<TxInputInfo>,
145
+ pub input_amount: u64,
146
+ pub collateral: u64,
147
+ pub dlc_inputs: Vec<DlcInputInfo>,
148
+ }
149
+
150
+ #[derive(Clone)]
151
+ pub struct DlcTransactions {
152
+ pub fund: Transaction,
153
+ pub cets: Vec<Transaction>,
154
+ pub refund: Transaction,
155
+ pub funding_script_pubkey: Vec<u8>,
156
+ }
157
+
158
+ #[derive(Clone)]
159
+ pub struct AdaptorSignature {
160
+ pub signature: Vec<u8>,
161
+ pub proof: Vec<u8>,
162
+ }
163
+
164
+ #[derive(Clone)]
165
+ pub struct ChangeOutputAndFees {
166
+ pub change_output: TxOutput,
167
+ pub fund_fee: u64,
168
+ pub cet_fee: u64,
169
+ }
170
+
171
+ #[derive(Clone)]
172
+ pub struct OracleInfo {
173
+ pub public_key: Vec<u8>,
174
+ pub nonces: Vec<Vec<u8>>,
175
+ }
176
+
177
+ // Conversion helpers
178
+ pub fn btc_tx_to_transaction(tx: &BtcTransaction) -> Transaction {
179
+ use bitcoin::consensus::Encodable;
180
+ let mut raw_bytes = Vec::new();
181
+ tx.consensus_encode(&mut raw_bytes).unwrap();
182
+
183
+ Transaction {
184
+ version: tx.version.0,
185
+ lock_time: tx.lock_time.to_consensus_u32(),
186
+ inputs: tx
187
+ .input
188
+ .iter()
189
+ .map(|input| TxInput {
190
+ txid: input.previous_output.txid.to_string(),
191
+ vout: input.previous_output.vout,
192
+ script_sig: input.script_sig.to_bytes(),
193
+ sequence: input.sequence.0,
194
+ witness: input.witness.iter().map(|w| w.to_vec()).collect(),
195
+ })
196
+ .collect(),
197
+ outputs: tx
198
+ .output
199
+ .iter()
200
+ .map(|output| TxOutput {
201
+ value: output.value.to_sat(),
202
+ script_pubkey: output.script_pubkey.to_bytes(),
203
+ })
204
+ .collect(),
205
+ raw_bytes,
206
+ }
207
+ }
208
+
209
+ pub fn plz_work() -> String {
210
+ "heyhowareya".to_string()
211
+ }
212
+
213
+ pub fn transaction_to_btc_tx(tx: &Transaction) -> Result<BtcTransaction, DLCError> {
214
+ use bitcoin::consensus::Decodable;
215
+ BtcTransaction::consensus_decode(&mut &tx.raw_bytes[..])
216
+ .map_err(|_| DLCError::SerializationError)
217
+ }
218
+
219
+ pub fn dlc_input_info_to_rust(input: &DlcInputInfo) -> Result<RustDlcInputInfo, DLCError> {
220
+ let btc_tx = transaction_to_btc_tx(&input.fund_tx)?;
221
+ let local_fund_pubkey =
222
+ PublicKey::from_slice(&input.local_fund_pubkey).map_err(|_| DLCError::InvalidPublicKey)?;
223
+ let remote_fund_pubkey =
224
+ PublicKey::from_slice(&input.remote_fund_pubkey).map_err(|_| DLCError::InvalidPublicKey)?;
225
+ let contract_id: [u8; 32] = input
226
+ .contract_id
227
+ .as_slice()
228
+ .try_into()
229
+ .map_err(|_| DLCError::InvalidArgument)?;
230
+ Ok(RustDlcInputInfo {
231
+ fund_tx: btc_tx,
232
+ fund_vout: input.fund_vout,
233
+ local_fund_pubkey,
234
+ remote_fund_pubkey,
235
+ fund_amount: Amount::from_sat(input.fund_amount),
236
+ max_witness_len: input.max_witness_len as usize,
237
+ input_serial_id: input.input_serial_id,
238
+ contract_id,
239
+ })
240
+ }
241
+
242
+ /// Convert UniFFI TxInputInfo to rust-dlc TxInputInfo
243
+ pub fn tx_input_info_to_rust(input: &TxInputInfo) -> Result<DlcTxInputInfo, DLCError> {
244
+ let txid = Txid::from_str(&input.txid).map_err(|_| DLCError::InvalidArgument)?;
245
+ Ok(DlcTxInputInfo {
246
+ outpoint: OutPoint {
247
+ txid,
248
+ vout: input.vout,
249
+ },
250
+ max_witness_len: input.max_witness_length as usize,
251
+ redeem_script: ScriptBuf::from(input.script_sig.clone()),
252
+ serial_id: input.serial_id,
253
+ })
254
+ }
255
+
256
+ /// Convert UniFFI PartyParams to rust-dlc PartyParams
257
+ pub fn party_params_to_rust(params: &PartyParams) -> Result<DlcPartyParams, DLCError> {
258
+ let fund_pubkey =
259
+ PublicKey::from_slice(&params.fund_pubkey).map_err(|_| DLCError::InvalidPublicKey)?;
260
+
261
+ let inputs: Result<Vec<_>, _> = params.inputs.iter().map(tx_input_info_to_rust).collect();
262
+
263
+ let dlc_inputs: Result<Vec<_>, _> = params
264
+ .dlc_inputs
265
+ .iter()
266
+ .map(dlc_input_info_to_rust)
267
+ .collect();
268
+
269
+ Ok(DlcPartyParams {
270
+ fund_pubkey,
271
+ change_script_pubkey: ScriptBuf::from(params.change_script_pubkey.clone()),
272
+ change_serial_id: params.change_serial_id,
273
+ payout_script_pubkey: ScriptBuf::from(params.payout_script_pubkey.clone()),
274
+ payout_serial_id: params.payout_serial_id,
275
+ inputs: inputs?,
276
+ dlc_inputs: dlc_inputs?,
277
+ input_amount: Amount::from_sat(params.input_amount),
278
+ collateral: Amount::from_sat(params.collateral),
279
+ })
280
+ }
281
+
282
+ /// Convert rust-dlc DlcTransactions to UniFFI DlcTransactions
283
+ pub fn rust_dlc_transactions_to_uniffi(dlc_txs: RustDlcTransactions) -> DlcTransactions {
284
+ DlcTransactions {
285
+ fund: btc_tx_to_transaction(&dlc_txs.fund),
286
+ cets: dlc_txs.cets.iter().map(btc_tx_to_transaction).collect(),
287
+ refund: btc_tx_to_transaction(&dlc_txs.refund),
288
+ funding_script_pubkey: dlc_txs.funding_script_pubkey.to_bytes(),
289
+ }
290
+ }
291
+
292
+ /// Create a funding script pubkey for DLC transactions
293
+ pub fn create_fund_tx_locking_script(
294
+ local_fund_pubkey: Vec<u8>,
295
+ remote_fund_pubkey: Vec<u8>,
296
+ ) -> Result<Vec<u8>, DLCError> {
297
+ let local_pk =
298
+ PublicKey::from_slice(&local_fund_pubkey).map_err(|_| DLCError::InvalidPublicKey)?;
299
+ let remote_pk =
300
+ PublicKey::from_slice(&remote_fund_pubkey).map_err(|_| DLCError::InvalidPublicKey)?;
301
+
302
+ let script = dlc::make_funding_redeemscript(&local_pk, &remote_pk);
303
+ Ok(script.to_bytes())
304
+ }
305
+
306
+ /// Create complete DLC transactions
307
+ pub fn create_dlc_transactions(
308
+ outcomes: Vec<DlcOutcome>,
309
+ local_params: PartyParams,
310
+ remote_params: PartyParams,
311
+ refund_locktime: u32,
312
+ fee_rate: u64,
313
+ fund_lock_time: u32,
314
+ cet_lock_time: u32,
315
+ fund_output_serial_id: u64,
316
+ ) -> Result<DlcTransactions, DLCError> {
317
+ // Convert UniFFI types to rust-dlc types
318
+ let rust_local_params = party_params_to_rust(&local_params)?;
319
+ let rust_remote_params = party_params_to_rust(&remote_params)?;
320
+
321
+ // Convert outcomes to payouts
322
+ let payouts: Vec<DlcPayout> = outcomes
323
+ .iter()
324
+ .map(|outcome| DlcPayout {
325
+ offer: Amount::from_sat(outcome.local_payout),
326
+ accept: Amount::from_sat(outcome.remote_payout),
327
+ })
328
+ .collect();
329
+
330
+ // Use rust-dlc library to create transactions
331
+ let dlc_txs = dlc::create_dlc_transactions(
332
+ &rust_local_params,
333
+ &rust_remote_params,
334
+ &payouts,
335
+ refund_locktime,
336
+ fee_rate,
337
+ fund_lock_time,
338
+ cet_lock_time,
339
+ fund_output_serial_id,
340
+ )
341
+ .map_err(DLCError::from)?;
342
+
343
+ // Convert back to UniFFI types
344
+ Ok(rust_dlc_transactions_to_uniffi(dlc_txs))
345
+ }
346
+
347
+ /// Create spliced DLC transactions
348
+ pub fn create_spliced_dlc_transactions(
349
+ outcomes: Vec<DlcOutcome>,
350
+ local_params: PartyParams,
351
+ remote_params: PartyParams,
352
+ refund_locktime: u32,
353
+ fee_rate: u64,
354
+ fund_lock_time: u32,
355
+ cet_lock_time: u32,
356
+ fund_output_serial_id: u64,
357
+ ) -> Result<DlcTransactions, DLCError> {
358
+ // Convert UniFFI types to rust-dlc types
359
+ let rust_local_params = party_params_to_rust(&local_params)?;
360
+ let rust_remote_params = party_params_to_rust(&remote_params)?;
361
+
362
+ // Convert outcomes to payouts
363
+ let payouts: Vec<DlcPayout> = outcomes
364
+ .iter()
365
+ .map(|outcome| DlcPayout {
366
+ offer: Amount::from_sat(outcome.local_payout),
367
+ accept: Amount::from_sat(outcome.remote_payout),
368
+ })
369
+ .collect();
370
+
371
+ // Use rust-dlc library to create spliced transactions
372
+ let dlc_txs = dlc::create_spliced_dlc_transactions(
373
+ &rust_local_params,
374
+ &rust_remote_params,
375
+ &payouts,
376
+ refund_locktime,
377
+ fee_rate,
378
+ fund_lock_time,
379
+ cet_lock_time,
380
+ fund_output_serial_id,
381
+ )
382
+ .map_err(DLCError::from)?;
383
+
384
+ // Convert back to UniFFI types
385
+ Ok(rust_dlc_transactions_to_uniffi(dlc_txs))
386
+ }
387
+
388
+ /// Create a single CET
389
+ pub fn create_cet(
390
+ local_output: TxOutput,
391
+ local_payout_serial_id: u64,
392
+ remote_output: TxOutput,
393
+ remote_payout_serial_id: u64,
394
+ fund_tx_id: String,
395
+ fund_vout: u32,
396
+ lock_time: u32,
397
+ ) -> Result<Transaction, DLCError> {
398
+ let txid = Txid::from_str(&fund_tx_id).map_err(|_| DLCError::InvalidArgument)?;
399
+
400
+ let local_btc_output = BtcTxOut {
401
+ value: Amount::from_sat(local_output.value),
402
+ script_pubkey: ScriptBuf::from(local_output.script_pubkey),
403
+ };
404
+
405
+ let remote_btc_output = BtcTxOut {
406
+ value: Amount::from_sat(remote_output.value),
407
+ script_pubkey: ScriptBuf::from(remote_output.script_pubkey),
408
+ };
409
+
410
+ let fund_tx_input = TxIn {
411
+ previous_output: OutPoint {
412
+ txid,
413
+ vout: fund_vout,
414
+ },
415
+ script_sig: ScriptBuf::new(),
416
+ sequence: Sequence::ZERO,
417
+ witness: Witness::new(),
418
+ };
419
+
420
+ let btc_tx = dlc::create_cet(
421
+ local_btc_output,
422
+ local_payout_serial_id,
423
+ remote_btc_output,
424
+ remote_payout_serial_id,
425
+ &fund_tx_input,
426
+ lock_time,
427
+ );
428
+
429
+ Ok(btc_tx_to_transaction(&btc_tx))
430
+ }
431
+
432
+ /// Create multiple CETs
433
+ pub fn create_cets(
434
+ fund_tx_id: String,
435
+ fund_vout: u32,
436
+ local_final_script_pubkey: Vec<u8>,
437
+ remote_final_script_pubkey: Vec<u8>,
438
+ outcomes: Vec<DlcOutcome>,
439
+ lock_time: u32,
440
+ local_serial_id: u64,
441
+ remote_serial_id: u64,
442
+ ) -> Result<Vec<Transaction>, DLCError> {
443
+ let txid = Txid::from_str(&fund_tx_id).map_err(|_| DLCError::InvalidArgument)?;
444
+
445
+ let fund_tx_input = TxIn {
446
+ previous_output: OutPoint {
447
+ txid,
448
+ vout: fund_vout,
449
+ },
450
+ script_sig: ScriptBuf::new(),
451
+ sequence: Sequence::ZERO,
452
+ witness: Witness::new(),
453
+ };
454
+
455
+ let local_script = bitcoin::Script::from_bytes(&local_final_script_pubkey);
456
+ let remote_script = bitcoin::Script::from_bytes(&remote_final_script_pubkey);
457
+
458
+ let payouts: Vec<DlcPayout> = outcomes
459
+ .iter()
460
+ .map(|outcome| DlcPayout {
461
+ offer: Amount::from_sat(outcome.local_payout),
462
+ accept: Amount::from_sat(outcome.remote_payout),
463
+ })
464
+ .collect();
465
+
466
+ let btc_txs = dlc::create_cets(
467
+ &fund_tx_input,
468
+ local_script,
469
+ local_serial_id,
470
+ remote_script,
471
+ remote_serial_id,
472
+ &payouts,
473
+ lock_time,
474
+ );
475
+
476
+ Ok(btc_txs.iter().map(btc_tx_to_transaction).collect())
477
+ }
478
+
479
+ /// Create a refund transaction
480
+ pub fn create_refund_transaction(
481
+ local_final_script_pubkey: Vec<u8>,
482
+ remote_final_script_pubkey: Vec<u8>,
483
+ local_amount: u64,
484
+ remote_amount: u64,
485
+ lock_time: u32,
486
+ fund_tx_id: String,
487
+ fund_vout: u32,
488
+ ) -> Result<Transaction, DLCError> {
489
+ let txid = Txid::from_str(&fund_tx_id).map_err(|_| DLCError::InvalidArgument)?;
490
+
491
+ let local_output = BtcTxOut {
492
+ value: Amount::from_sat(local_amount),
493
+ script_pubkey: ScriptBuf::from(local_final_script_pubkey),
494
+ };
495
+
496
+ let remote_output = BtcTxOut {
497
+ value: Amount::from_sat(remote_amount),
498
+ script_pubkey: ScriptBuf::from(remote_final_script_pubkey),
499
+ };
500
+
501
+ let funding_input = TxIn {
502
+ previous_output: OutPoint {
503
+ txid,
504
+ vout: fund_vout,
505
+ },
506
+ script_sig: ScriptBuf::new(),
507
+ sequence: Sequence::ENABLE_LOCKTIME_NO_RBF,
508
+ witness: Witness::new(),
509
+ };
510
+
511
+ let btc_tx =
512
+ dlc::create_refund_transaction(local_output, remote_output, funding_input, lock_time);
513
+
514
+ Ok(btc_tx_to_transaction(&btc_tx))
515
+ }
516
+
517
+ /// Check if a transaction output is dust
518
+ pub fn is_dust_output(output: TxOutput) -> bool {
519
+ output.value < DUST_LIMIT
520
+ }
521
+
522
+ /// Get change output and fees for a party
523
+ pub fn get_change_output_and_fees(
524
+ params: PartyParams,
525
+ fee_rate: u64,
526
+ ) -> Result<ChangeOutputAndFees, DLCError> {
527
+ let rust_params = party_params_to_rust(&params)?;
528
+ let total_collateral = Amount::from_sat(params.collateral * 2); // Assume bilateral
529
+
530
+ let (change_output, fund_fee, cet_fee) = rust_params
531
+ .get_change_output_and_fees(total_collateral, fee_rate, Amount::ZERO)
532
+ .map_err(DLCError::from)?;
533
+
534
+ let uniffi_output = TxOutput {
535
+ value: change_output.value.to_sat(),
536
+ script_pubkey: change_output.script_pubkey.to_bytes(),
537
+ };
538
+
539
+ Ok(ChangeOutputAndFees {
540
+ change_output: uniffi_output,
541
+ fund_fee: fund_fee.to_sat(),
542
+ cet_fee: cet_fee.to_sat(),
543
+ })
544
+ }
545
+
546
+ /// Get total input virtual size for fee calculation
547
+ pub fn get_total_input_vsize(inputs: Vec<TxInputInfo>) -> u32 {
548
+ // Simplified calculation: P2WPKH inputs are ~148 vbytes each
549
+ inputs.len() as u32 * 148
550
+ }
551
+
552
+ /// Verify a fund transaction signature
553
+ pub fn verify_fund_tx_signature(
554
+ fund_tx: Transaction,
555
+ signature: Vec<u8>,
556
+ pubkey: Vec<u8>,
557
+ txid: String,
558
+ vout: u32,
559
+ input_amount: u64,
560
+ ) -> Result<bool, DLCError> {
561
+ let btc_tx = transaction_to_btc_tx(&fund_tx)?;
562
+ let pk = PublicKey::from_slice(&pubkey).map_err(|_| DLCError::InvalidPublicKey)?;
563
+ let input_txid = Txid::from_str(&txid).map_err(|_| DLCError::InvalidArgument)?;
564
+
565
+ // Find the input index
566
+ let input_index = btc_tx
567
+ .input
568
+ .iter()
569
+ .position(|input| {
570
+ input.previous_output.txid == input_txid && input.previous_output.vout == vout
571
+ })
572
+ .ok_or(DLCError::InvalidArgument)?;
573
+
574
+ // Create a simple P2WPKH script for verification
575
+ let wpkh = WPubkeyHash::hash(&pk.serialize());
576
+ let script = bitcoin::ScriptBuf::new_p2wpkh(&wpkh);
577
+
578
+ // Parse signature
579
+ let sig = if signature.len() == 64 {
580
+ EcdsaSignature::from_compact(&signature).map_err(|_| DLCError::InvalidSignature)?
581
+ } else {
582
+ // Remove sighash type byte and parse DER
583
+ let sig_bytes = &signature[..signature.len() - 1];
584
+ EcdsaSignature::from_der(sig_bytes).map_err(|_| DLCError::InvalidSignature)?
585
+ };
586
+
587
+ let secp = Secp256k1::verification_only();
588
+ match dlc::verify_tx_input_sig(
589
+ &secp,
590
+ &sig,
591
+ &btc_tx,
592
+ input_index,
593
+ &script,
594
+ Amount::from_sat(input_amount),
595
+ &pk,
596
+ ) {
597
+ Ok(()) => Ok(true),
598
+ Err(_) => Ok(false),
599
+ }
600
+ }
601
+
602
+ // ============================================================================
603
+ // SIGNING AND SIGNATURE FUNCTIONS (using rust-dlc library)
604
+ // ============================================================================
605
+
606
+ /// Get raw signature for a fund transaction input
607
+ pub fn get_raw_funding_transaction_input_signature(
608
+ funding_transaction: Transaction,
609
+ privkey: Vec<u8>,
610
+ prev_tx_id: String,
611
+ prev_tx_vout: u32,
612
+ value: u64,
613
+ ) -> Result<Vec<u8>, DLCError> {
614
+ let btc_tx = transaction_to_btc_tx(&funding_transaction)?;
615
+ let sk = SecretKey::from_slice(&privkey).map_err(|_| DLCError::InvalidArgument)?;
616
+ let prev_txid = Txid::from_str(&prev_tx_id).map_err(|_| DLCError::InvalidArgument)?;
617
+
618
+ // Find the input index
619
+ let input_index = btc_tx
620
+ .input
621
+ .iter()
622
+ .position(|input| {
623
+ input.previous_output.txid == prev_txid && input.previous_output.vout == prev_tx_vout
624
+ })
625
+ .ok_or(DLCError::InvalidArgument)?;
626
+
627
+ // Create P2WPKH script for signing
628
+ let pk = PublicKey::from_secret_key(&Secp256k1::new(), &sk);
629
+ let wpkh = WPubkeyHash::hash(&pk.serialize());
630
+ let script = bitcoin::ScriptBuf::new_p2wpkh(&wpkh);
631
+
632
+ let secp = Secp256k1::signing_only();
633
+ let sig = dlc::util::get_raw_sig_for_tx_input(
634
+ &secp,
635
+ &btc_tx,
636
+ input_index,
637
+ &script,
638
+ Amount::from_sat(value),
639
+ &sk,
640
+ )
641
+ .map_err(DLCError::from)?;
642
+
643
+ Ok(sig.serialize_compact().to_vec())
644
+ }
645
+
646
+ /// Sign a funding transaction input
647
+ pub fn sign_fund_transaction_input(
648
+ fund_transaction: Transaction,
649
+ privkey: Vec<u8>,
650
+ prev_tx_id: String,
651
+ prev_tx_vout: u32,
652
+ value: u64,
653
+ ) -> Result<Transaction, DLCError> {
654
+ let mut btc_tx = transaction_to_btc_tx(&fund_transaction)?;
655
+ let sk = SecretKey::from_slice(&privkey).map_err(|_| DLCError::InvalidArgument)?;
656
+ let prev_txid = Txid::from_str(&prev_tx_id).map_err(|_| DLCError::InvalidArgument)?;
657
+
658
+ // Find the input index
659
+ let input_index = btc_tx
660
+ .input
661
+ .iter()
662
+ .position(|input| {
663
+ input.previous_output.txid == prev_txid && input.previous_output.vout == prev_tx_vout
664
+ })
665
+ .ok_or(DLCError::InvalidArgument)?;
666
+
667
+ let secp = Secp256k1::signing_only();
668
+ dlc::util::sign_p2wpkh_input(
669
+ &secp,
670
+ &sk,
671
+ &mut btc_tx,
672
+ input_index,
673
+ EcdsaSighashType::All,
674
+ Amount::from_sat(value),
675
+ )
676
+ .map_err(DLCError::from)?;
677
+
678
+ Ok(btc_tx_to_transaction(&btc_tx))
679
+ }
680
+
681
+ /// Create CET adaptor signature from oracle info
682
+ pub fn create_cet_adaptor_signature_from_oracle_info(
683
+ cet: Transaction,
684
+ oracle_info: OracleInfo,
685
+ funding_sk: Vec<u8>,
686
+ funding_script_pubkey: Vec<u8>,
687
+ total_collateral: u64,
688
+ msgs: Vec<Vec<u8>>,
689
+ ) -> Result<AdaptorSignature, DLCError> {
690
+ let btc_tx = transaction_to_btc_tx(&cet)?;
691
+ let sk = SecretKey::from_slice(&funding_sk).map_err(|_| DLCError::InvalidArgument)?;
692
+ let funding_script = bitcoin::Script::from_bytes(&funding_script_pubkey);
693
+
694
+ // Convert oracle info
695
+ let oracle_pk = XOnlyPublicKey::from_slice(&oracle_info.public_key)
696
+ .map_err(|_| DLCError::InvalidPublicKey)?;
697
+ let nonces: Result<Vec<_>, _> = oracle_info
698
+ .nonces
699
+ .iter()
700
+ .map(|n| XOnlyPublicKey::from_slice(n))
701
+ .collect();
702
+ let oracle_nonces = nonces.map_err(|_| DLCError::InvalidPublicKey)?;
703
+
704
+ let dlc_oracle_info = DlcOracleInfo {
705
+ public_key: oracle_pk,
706
+ nonces: oracle_nonces,
707
+ };
708
+
709
+ // Convert messages
710
+ let messages: Result<Vec<_>, _> = msgs
711
+ .iter()
712
+ .map(|msg| Message::from_digest_slice(msg))
713
+ .collect();
714
+ let msg_vec = messages.map_err(|_| DLCError::InvalidArgument)?;
715
+ let nested_msgs = vec![msg_vec]; // Wrap in vector for single oracle
716
+
717
+ let secp = Secp256k1::new();
718
+ let adaptor_sig = dlc::create_cet_adaptor_sig_from_oracle_info(
719
+ &secp,
720
+ &btc_tx,
721
+ &[dlc_oracle_info],
722
+ &sk,
723
+ funding_script,
724
+ Amount::from_sat(total_collateral),
725
+ &nested_msgs,
726
+ )
727
+ .map_err(DLCError::from)?;
728
+
729
+ Ok(AdaptorSignature {
730
+ signature: adaptor_sig.as_ref().to_vec(),
731
+ proof: Vec::new(), // EcdsaAdaptorSignature doesn't expose proof directly
732
+ })
733
+ }
734
+
735
+ #[cfg(test)]
736
+ mod tests {
737
+ use super::*;
738
+ use bitcoin::locktime::absolute::LockTime;
739
+ use secp256k1_zkp::rand::{thread_rng, RngCore};
740
+ use std::str::FromStr;
741
+
742
+ /// Create test keys similar to rust-dlc tests
743
+ fn create_test_keys() -> (SecretKey, PublicKey, SecretKey, PublicKey) {
744
+ let secp = Secp256k1::new();
745
+ let offer_sk =
746
+ SecretKey::from_str("0000000000000000000000000000000000000000000000000000000000000001")
747
+ .unwrap();
748
+ let offer_pk = PublicKey::from_secret_key(&secp, &offer_sk);
749
+ let accept_sk =
750
+ SecretKey::from_str("0000000000000000000000000000000000000000000000000000000000000002")
751
+ .unwrap();
752
+ let accept_pk = PublicKey::from_secret_key(&secp, &accept_sk);
753
+ (offer_sk, offer_pk, accept_sk, accept_pk)
754
+ }
755
+
756
+ /// Create realistic party params for testing
757
+ fn create_test_party_params(
758
+ input_amount: u64,
759
+ collateral: u64,
760
+ fund_pubkey: Vec<u8>,
761
+ serial_id: u64,
762
+ ) -> PartyParams {
763
+ let secp = Secp256k1::new();
764
+ let mut rng = thread_rng();
765
+
766
+ // Create a realistic P2WPKH script
767
+ let mut random_hash = [0u8; 20];
768
+ rng.fill_bytes(&mut random_hash);
769
+ let mut change_script = vec![0x00, 0x14]; // OP_0 + 20 bytes (P2WPKH)
770
+ change_script.extend_from_slice(&random_hash);
771
+
772
+ rng.fill_bytes(&mut random_hash);
773
+ let mut payout_script = vec![0x00, 0x14]; // OP_0 + 20 bytes (P2WPKH)
774
+ payout_script.extend_from_slice(&random_hash);
775
+
776
+ PartyParams {
777
+ fund_pubkey,
778
+ change_script_pubkey: change_script,
779
+ change_serial_id: serial_id + 1,
780
+ payout_script_pubkey: payout_script,
781
+ payout_serial_id: serial_id + 2,
782
+ inputs: vec![TxInputInfo {
783
+ txid: "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456"
784
+ .to_string(),
785
+ vout: serial_id as u32,
786
+ script_sig: vec![],
787
+ max_witness_length: 108,
788
+ serial_id,
789
+ }],
790
+ input_amount,
791
+ collateral,
792
+ dlc_inputs: vec![],
793
+ }
794
+ }
795
+
796
+ #[test]
797
+ fn test_create_fund_tx_locking_script_matches_rust_dlc() {
798
+ let (_offer_sk, offer_pk, _accept_sk, accept_pk) = create_test_keys();
799
+
800
+ // Test our wrapper
801
+ let wrapper_result = create_fund_tx_locking_script(
802
+ offer_pk.serialize().to_vec(),
803
+ accept_pk.serialize().to_vec(),
804
+ )
805
+ .unwrap();
806
+
807
+ // Compare with direct rust-dlc call
808
+ let direct_result = dlc::make_funding_redeemscript(&offer_pk, &accept_pk);
809
+
810
+ assert_eq!(wrapper_result, direct_result.to_bytes());
811
+ }
812
+
813
+ #[test]
814
+ fn test_get_change_output_and_fees_wrapper() {
815
+ let (_offer_sk, offer_pk, _accept_sk, _accept_pk) = create_test_keys();
816
+
817
+ let params = create_test_party_params(
818
+ 150_000_000, // 1.5 BTC input
819
+ 100_000_000, // 1 BTC collateral
820
+ offer_pk.serialize().to_vec(),
821
+ 1,
822
+ );
823
+
824
+ let result = get_change_output_and_fees(params.clone(), 4);
825
+ assert!(result.is_ok());
826
+
827
+ let change_and_fees = result.unwrap();
828
+
829
+ // Verify we get reasonable values
830
+ assert!(change_and_fees.fund_fee > 0);
831
+ assert!(change_and_fees.cet_fee > 0);
832
+ assert!(change_and_fees.change_output.value > 0);
833
+
834
+ // Compare with direct rust-dlc call
835
+ let rust_params = party_params_to_rust(&params).unwrap();
836
+ let total_collateral = Amount::from_sat(params.collateral * 2);
837
+ let direct_result = rust_params
838
+ .get_change_output_and_fees(total_collateral, 4, Amount::ZERO)
839
+ .unwrap();
840
+
841
+ assert_eq!(change_and_fees.fund_fee, direct_result.1.to_sat());
842
+ assert_eq!(change_and_fees.cet_fee, direct_result.2.to_sat());
843
+ assert_eq!(
844
+ change_and_fees.change_output.value,
845
+ direct_result.0.value.to_sat()
846
+ );
847
+ }
848
+
849
+ #[test]
850
+ fn test_create_dlc_transactions_wrapper() {
851
+ let (_offer_sk, offer_pk, _accept_sk, accept_pk) = create_test_keys();
852
+
853
+ let offer_params = create_test_party_params(
854
+ 1_000_000_000, // 10 BTC input
855
+ 100_000_000, // 1 BTC collateral
856
+ offer_pk.serialize().to_vec(),
857
+ 1,
858
+ );
859
+
860
+ let accept_params = create_test_party_params(
861
+ 1_000_000_000, // 10 BTC input
862
+ 100_000_000, // 1 BTC collateral
863
+ accept_pk.serialize().to_vec(),
864
+ 2,
865
+ );
866
+
867
+ let outcomes = vec![
868
+ DlcOutcome {
869
+ local_payout: 200_000_000, // 2 BTC to offer
870
+ remote_payout: 0, // 0 BTC to accept
871
+ },
872
+ DlcOutcome {
873
+ local_payout: 0, // 0 BTC to offer
874
+ remote_payout: 200_000_000, // 2 BTC to accept
875
+ },
876
+ ];
877
+
878
+ let result = create_dlc_transactions(
879
+ outcomes,
880
+ offer_params,
881
+ accept_params,
882
+ 100, // refund locktime
883
+ 4, // fee rate
884
+ 10, // fund lock time
885
+ 10, // cet lock time
886
+ 0, // fund output serial id
887
+ );
888
+
889
+ assert!(result.is_ok());
890
+ let dlc_txs = result.unwrap();
891
+
892
+ // Verify structure
893
+ assert_eq!(dlc_txs.fund.lock_time, 10);
894
+ assert_eq!(dlc_txs.refund.lock_time, 100);
895
+ assert_eq!(dlc_txs.cets.len(), 2);
896
+ assert!(dlc_txs.cets.iter().all(|cet| cet.lock_time == 10));
897
+
898
+ // Verify funding transaction has correct structure
899
+ assert_eq!(dlc_txs.fund.inputs.len(), 2); // Two parties contributing
900
+ assert!(dlc_txs.fund.outputs.len() >= 1); // At least funding output
901
+
902
+ // Verify CETs have correct structure
903
+ for cet in &dlc_txs.cets {
904
+ assert_eq!(cet.inputs.len(), 1); // Single funding input
905
+ assert!(cet.outputs.len() >= 1); // At least one output (dust may be filtered)
906
+ }
907
+
908
+ // Verify refund transaction
909
+ assert_eq!(dlc_txs.refund.inputs.len(), 1); // Single funding input
910
+ assert!(dlc_txs.refund.outputs.len() >= 2); // At least two refund outputs
911
+ }
912
+
913
+ #[test]
914
+ fn test_create_cet_wrapper() {
915
+ let local_output = TxOutput {
916
+ value: 100_000_000, // 1 BTC
917
+ script_pubkey: vec![
918
+ 0x00, 0x14, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
919
+ 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14,
920
+ ],
921
+ };
922
+
923
+ let remote_output = TxOutput {
924
+ value: 100_000_000, // 1 BTC
925
+ script_pubkey: vec![
926
+ 0x00, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
927
+ 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
928
+ ],
929
+ };
930
+
931
+ let result = create_cet(
932
+ local_output,
933
+ 1,
934
+ remote_output,
935
+ 2,
936
+ "0000000000000000000000000000000000000000000000000000000000000000".to_string(),
937
+ 0,
938
+ 10,
939
+ );
940
+
941
+ assert!(result.is_ok());
942
+ let cet = result.unwrap();
943
+
944
+ assert_eq!(cet.lock_time, 10);
945
+ assert_eq!(cet.inputs.len(), 1);
946
+ assert_eq!(cet.outputs.len(), 2);
947
+ assert_eq!(cet.outputs[0].value, 100_000_000);
948
+ assert_eq!(cet.outputs[1].value, 100_000_000);
949
+ }
950
+
951
+ #[test]
952
+ fn test_create_refund_transaction_wrapper() {
953
+ let local_script = vec![
954
+ 0x00, 0x14, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
955
+ 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14,
956
+ ];
957
+ let remote_script = vec![
958
+ 0x00, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
959
+ 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
960
+ ];
961
+
962
+ let result = create_refund_transaction(
963
+ local_script,
964
+ remote_script,
965
+ 100_000_000, // 1 BTC to local
966
+ 100_000_000, // 1 BTC to remote
967
+ 144, // locktime (1 day in blocks)
968
+ "0000000000000000000000000000000000000000000000000000000000000000".to_string(),
969
+ 0,
970
+ );
971
+
972
+ assert!(result.is_ok());
973
+ let refund_tx = result.unwrap();
974
+
975
+ assert_eq!(refund_tx.lock_time, 144);
976
+ assert_eq!(refund_tx.inputs.len(), 1);
977
+ assert_eq!(refund_tx.outputs.len(), 2);
978
+ assert_eq!(refund_tx.outputs[0].value, 100_000_000);
979
+ assert_eq!(refund_tx.outputs[1].value, 100_000_000);
980
+ }
981
+
982
+ #[test]
983
+ fn test_is_dust_output() {
984
+ let dust_output = TxOutput {
985
+ value: 500, // Below dust limit
986
+ script_pubkey: vec![],
987
+ };
988
+
989
+ let non_dust_output = TxOutput {
990
+ value: 5000, // Above dust limit
991
+ script_pubkey: vec![],
992
+ };
993
+
994
+ assert!(is_dust_output(dust_output));
995
+ assert!(!is_dust_output(non_dust_output));
996
+ }
997
+
998
+ #[test]
999
+ fn test_conversion_functions() {
1000
+ let (_offer_sk, offer_pk, _accept_sk, accept_pk) = create_test_keys();
1001
+
1002
+ // Test party params conversion
1003
+ let params =
1004
+ create_test_party_params(100_000_000, 50_000_000, offer_pk.serialize().to_vec(), 1);
1005
+
1006
+ let rust_params = party_params_to_rust(&params).unwrap();
1007
+ assert_eq!(rust_params.fund_pubkey, offer_pk);
1008
+ assert_eq!(rust_params.input_amount, Amount::from_sat(100_000_000));
1009
+ assert_eq!(rust_params.collateral, Amount::from_sat(50_000_000));
1010
+
1011
+ // Test TX input conversion
1012
+ let tx_input = TxInputInfo {
1013
+ txid: "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456".to_string(),
1014
+ vout: 0,
1015
+ script_sig: vec![],
1016
+ max_witness_length: 108,
1017
+ serial_id: 1,
1018
+ };
1019
+
1020
+ let rust_input = tx_input_info_to_rust(&tx_input).unwrap();
1021
+ assert_eq!(rust_input.serial_id, 1);
1022
+ assert_eq!(rust_input.max_witness_len, 108);
1023
+ assert_eq!(rust_input.outpoint.vout, 0);
1024
+ }
1025
+
1026
+ #[test]
1027
+ fn test_transaction_bidirectional_conversion() {
1028
+ // Create a test Bitcoin transaction
1029
+ let btc_tx = BtcTransaction {
1030
+ version: bitcoin::transaction::Version::TWO,
1031
+ lock_time: LockTime::from_consensus(144),
1032
+ input: vec![TxIn {
1033
+ previous_output: OutPoint {
1034
+ txid: Txid::from_str(
1035
+ "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456",
1036
+ )
1037
+ .unwrap(),
1038
+ vout: 0,
1039
+ },
1040
+ script_sig: ScriptBuf::new(),
1041
+ sequence: Sequence::ZERO,
1042
+ witness: Witness::new(),
1043
+ }],
1044
+ output: vec![BtcTxOut {
1045
+ value: Amount::from_sat(100_000_000),
1046
+ script_pubkey: ScriptBuf::from(vec![0x00, 0x14]),
1047
+ }],
1048
+ };
1049
+
1050
+ // Convert to UniFFI format and back
1051
+ let uniffi_tx = btc_tx_to_transaction(&btc_tx);
1052
+ let converted_back = transaction_to_btc_tx(&uniffi_tx).unwrap();
1053
+
1054
+ // Verify they're equivalent
1055
+ assert_eq!(btc_tx.version, converted_back.version);
1056
+ assert_eq!(btc_tx.lock_time, converted_back.lock_time);
1057
+ assert_eq!(btc_tx.input.len(), converted_back.input.len());
1058
+ assert_eq!(btc_tx.output.len(), converted_back.output.len());
1059
+ assert_eq!(
1060
+ btc_tx.input[0].previous_output,
1061
+ converted_back.input[0].previous_output
1062
+ );
1063
+ assert_eq!(btc_tx.output[0].value, converted_back.output[0].value);
1064
+ }
1065
+
1066
+ #[test]
1067
+ fn test_error_handling_invalid_keys() {
1068
+ // Test invalid public key
1069
+ let result = create_fund_tx_locking_script(
1070
+ vec![0u8; 20], // Invalid key length
1071
+ vec![1u8; 33],
1072
+ );
1073
+ assert!(matches!(result, Err(DLCError::InvalidPublicKey)));
1074
+
1075
+ // Test invalid txid
1076
+ let result = create_cet(
1077
+ TxOutput {
1078
+ value: 1000,
1079
+ script_pubkey: vec![],
1080
+ },
1081
+ 1,
1082
+ TxOutput {
1083
+ value: 1000,
1084
+ script_pubkey: vec![],
1085
+ },
1086
+ 2,
1087
+ "invalid_txid".to_string(),
1088
+ 0,
1089
+ 0,
1090
+ );
1091
+ assert!(matches!(result, Err(DLCError::InvalidArgument)));
1092
+ }
1093
+ }