@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.
- package/ddk-ffi/Cargo.lock +1096 -0
- package/ddk-ffi/Cargo.toml +31 -0
- package/ddk-ffi/build.rs +3 -0
- package/ddk-ffi/rust-toolchain.toml +10 -0
- package/ddk-ffi/scratchpad.txt +243 -0
- package/ddk-ffi/src/ddk_ffi.udl +230 -0
- package/ddk-ffi/src/lib.rs +1093 -0
- package/ddk-ffi/src/uniffi-bindgen.rs +3 -0
- package/ddk-ffi/uniffi.toml +18 -0
- package/package.json +4 -1
- package/scripts/postinstall.js +45 -3
- package/scripts/prepare-rust-src.js +51 -0
|
@@ -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(¶ms.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(¶ms)?;
|
|
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(¶ms).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(¶ms).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
|
+
}
|