1use std::fmt;
8
9#[cfg(feature = "driver")]
10use anyswap_core::types::Outpoint;
11use anyswap_core::{
12 script::{BitcoinSwapScript, LiquidSwapScript, derive_liquid_blinding_key},
13 transaction::{
14 self, ClaimByPreimageParams, KeypathSpendParams, LiquidChainCtx, RefundByCsvParams, TxError,
15 },
16 types::{Chain, SwapNetwork},
17 utils::sha256_payment_hash,
18};
19use bitcoin::secp256k1::{PublicKey, SecretKey};
20use elements::AssetId;
21use thiserror::Error;
22use uuid::Uuid;
23
24use crate::{
25 context::SwapContext,
26 utxo::{self, SwapUtxo, UtxoError},
27 wallet::SwapWallet,
28};
29
30#[derive(Clone, Debug, Error)]
31pub enum ScriptError {
32 #[error("this client is not set up for Liquid; add the Liquid parameters to the context")]
33 LiquidNotConfigured,
34
35 #[error("the swap trades asset {found}, but this client is set up for {expected}")]
36 UnexpectedAsset { expected: String, found: String },
37
38 #[error("missing field: {0}")]
39 MissingField(&'static str),
40
41 #[error("wallet: {0}")]
42 Wallet(String),
43
44 #[error("invalid swap script: {0}")]
45 Script(String),
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub enum Side {
52 Source,
53 Dest,
54}
55
56impl fmt::Display for Side {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 f.write_str(match self {
59 Self::Source => "source",
60 Self::Dest => "dest",
61 })
62 }
63}
64
65#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct Terms {
69 pub swap_id: Uuid,
70 pub side: Side,
71 pub chain: Chain,
72 pub asset: String,
73 pub csv: u32,
74 pub payment_hash: [u8; 32],
75 pub server_pubkey: PublicKey,
76 pub server_blinding_pubkey: Option<PublicKey>,
77}
78
79pub enum Tx {
81 Bitcoin(bitcoin::Transaction),
82 Liquid(elements::Transaction),
83}
84
85impl Tx {
86 pub fn decode(chain: Chain, hex: &str) -> Result<Self, TxError> {
87 Ok(match chain {
88 Chain::Bitcoin => Self::Bitcoin(transaction::bitcoin::decode_tx_hex(hex)?),
89 Chain::Liquid => Self::Liquid(transaction::liquid::decode_tx_hex(hex)?),
90 })
91 }
92
93 pub fn txid(&self) -> String {
94 match self {
95 Self::Bitcoin(tx) => tx.compute_txid().to_string(),
96 Self::Liquid(tx) => tx.txid().to_string(),
97 }
98 }
99
100 #[cfg(feature = "driver")]
101 pub fn inputs(&self) -> Vec<Outpoint> {
102 let outpoint = |txid: String, vout| Outpoint::new(txid, vout);
103 match self {
104 Self::Bitcoin(tx) => tx
105 .input
106 .iter()
107 .map(|i| outpoint(i.previous_output.txid.to_string(), i.previous_output.vout))
108 .collect(),
109 Self::Liquid(tx) => tx
110 .input
111 .iter()
112 .map(|i| outpoint(i.previous_output.txid.to_string(), i.previous_output.vout))
113 .collect(),
114 }
115 }
116
117 #[cfg(feature = "driver")]
121 pub fn fee(&self, value: impl Fn(&Outpoint) -> Option<u64>) -> Option<u64> {
122 match self {
123 Self::Bitcoin(tx) => {
124 let input = self
125 .inputs()
126 .iter()
127 .try_fold(0_u64, |total, prev| total.checked_add(value(prev)?))?;
128 transaction::bitcoin::bitcoin_tx_fee(tx, input).ok()
129 }
130 Self::Liquid(tx) => transaction::liquid::tx_fee(tx).ok(),
131 }
132 }
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum SpendPath {
138 Keypath,
139 Preimage([u8; 32]),
140 Csv,
141}
142
143enum Inner {
144 Bitcoin(Box<BitcoinSwapScript>),
145 Liquid {
146 script: Box<LiquidSwapScript>,
147 blinding_key: SecretKey,
148 ctx: LiquidChainCtx,
149 },
150}
151
152pub struct SwapScript {
153 chain: Chain,
154 network: SwapNetwork,
155 payment_hash: [u8; 32],
156 inner: Inner,
157}
158
159impl<W: SwapWallet> SwapContext<W> {
160 pub fn script(&self, terms: &Terms) -> Result<SwapScript, ScriptError> {
164 let wallet = self.wallet();
165 let wallet_error = |e: W::Error| ScriptError::Wallet(e.to_string());
166 let script_error = |e: anyhow::Error| ScriptError::Script(e.to_string());
167 let (claim, refund) = match terms.side {
168 Side::Source => (
169 terms.server_pubkey,
170 wallet
171 .source_refund_pubkey(&terms.swap_id)
172 .map_err(wallet_error)?,
173 ),
174 Side::Dest => (
175 wallet
176 .dest_claim_pubkey(&terms.swap_id)
177 .map_err(wallet_error)?,
178 terms.server_pubkey,
179 ),
180 };
181 let (claim, refund) = (claim.x_only_public_key().0, refund.x_only_public_key().0);
182 let inner = match terms.chain {
183 Chain::Bitcoin => Inner::Bitcoin(Box::new(
184 BitcoinSwapScript::new(claim, refund, terms.payment_hash, terms.csv)
185 .map_err(script_error)?,
186 )),
187 Chain::Liquid => {
188 let liquid = self.liquid().ok_or(ScriptError::LiquidNotConfigured)?;
189 if terms.asset != liquid.policy_asset.to_string() {
190 return Err(ScriptError::UnexpectedAsset {
191 expected: liquid.policy_asset.to_string(),
192 found: terms.asset.clone(),
193 });
194 }
195 let own = match terms.side {
196 Side::Source => wallet.source_blinding_privkey(&terms.swap_id),
197 Side::Dest => wallet.dest_blinding_privkey(&terms.swap_id),
198 }
199 .map_err(wallet_error)?;
200 let peer = terms
201 .server_blinding_pubkey
202 .ok_or(ScriptError::MissingField("server_blinding_pubkey"))?;
203 let (blinding_pubkey, blinding_key) =
204 derive_liquid_blinding_key(&own, &peer).map_err(ScriptError::Script)?;
205 let script = LiquidSwapScript::new(
206 claim,
207 refund,
208 terms.payment_hash,
209 terms.csv,
210 blinding_pubkey,
211 )
212 .map_err(script_error)?;
213 Inner::Liquid {
214 script: Box::new(script),
215 blinding_key,
216 ctx: liquid,
217 }
218 }
219 };
220 Ok(SwapScript {
221 chain: terms.chain,
222 network: self.network(),
223 payment_hash: terms.payment_hash,
224 inner,
225 })
226 }
227}
228
229impl SwapScript {
230 pub fn address(&self) -> String {
231 match &self.inner {
232 Inner::Bitcoin(script) => script.address(self.network),
233 Inner::Liquid { script, .. } => script.confidential_address(self.network).to_string(),
234 }
235 }
236
237 pub fn outputs(&self, tx: &Tx) -> Vec<SwapUtxo> {
240 let txid = tx.txid();
241 match (&self.inner, tx) {
242 (Inner::Bitcoin(script), Tx::Bitcoin(tx)) => {
243 let spk = script.script_pubkey();
244 tx.output
245 .iter()
246 .enumerate()
247 .filter(|(_, o)| o.script_pubkey == spk)
248 .map(|(vout, o)| SwapUtxo::bitcoin(txid.clone(), vout as u32, o.value.to_sat()))
249 .collect()
250 }
251 (Inner::Liquid { script, .. }, Tx::Liquid(tx)) => {
252 let spk = script.script_pubkey();
253 tx.output
254 .iter()
255 .enumerate()
256 .filter(|(_, o)| o.script_pubkey == spk)
257 .map(|(vout, o)| SwapUtxo::liquid(txid.clone(), vout as u32, o.clone()))
258 .filter(|utxo| self.value(utxo).is_some())
259 .collect()
260 }
261 _ => Vec::new(),
262 }
263 }
264
265 pub fn output_at(&self, tx: &Tx, vout: u32) -> Result<(SwapUtxo, u64), TxError> {
268 let txid = tx.txid();
269 match (&self.inner, tx) {
270 (Inner::Bitcoin(script), Tx::Bitcoin(tx)) => {
271 let value = transaction::bitcoin::opening_value(tx, vout, script)?.to_sat();
272 Ok((SwapUtxo::bitcoin(txid, vout, value), value))
273 }
274 (
275 Inner::Liquid {
276 script,
277 blinding_key,
278 ctx,
279 },
280 Tx::Liquid(tx),
281 ) => {
282 let value = transaction::liquid::opening_value(
283 tx,
284 vout,
285 script,
286 blinding_key,
287 ctx.policy_asset,
288 )?;
289 let output = tx.output[vout as usize].clone();
290 Ok((SwapUtxo::liquid(txid, vout, output), value))
291 }
292 _ => Err(TxError::InvalidPrevout(
293 "transaction of another chain".into(),
294 )),
295 }
296 }
297
298 pub fn value(&self, utxo: &SwapUtxo) -> Option<u64> {
300 utxo.value(self.unblinding())
301 }
302
303 pub fn economical(
305 &self,
306 utxos: &[SwapUtxo],
307 fee_rate: f64,
308 ) -> Result<(Vec<SwapUtxo>, Vec<SwapUtxo>), UtxoError> {
309 let liquid = self.unblinding().map(|(key, asset)| (*key, asset));
310 utxo::partition_economical(utxos, self.chain, fee_rate, liquid)
311 }
312
313 pub fn spend_path(&self, tx: &Tx, vin: u32) -> Option<SpendPath> {
316 let mut items: Vec<&[u8]> = match tx {
317 Tx::Bitcoin(tx) => tx.input.get(vin as usize)?.witness.iter().collect(),
318 Tx::Liquid(tx) => tx
319 .input
320 .get(vin as usize)?
321 .witness
322 .script_witness
323 .iter()
324 .collect(),
325 };
326 if items.len() > 1 && items.last()?.first() == Some(&0x50) {
328 items.pop();
329 }
330 let (claim, refund) = match &self.inner {
331 Inner::Bitcoin(script) => (
332 script.claim_script().as_bytes(),
333 script.refund_script().as_bytes(),
334 ),
335 Inner::Liquid { script, .. } => (
336 script.claim_script().as_bytes(),
337 script.refund_script().as_bytes(),
338 ),
339 };
340 match items.as_slice() {
341 [_signature] => Some(SpendPath::Keypath),
342 [_signature, leaf, _control] if *leaf == refund => Some(SpendPath::Csv),
343 [_signature, preimage, leaf, _control] if *leaf == claim => {
344 let preimage = <[u8; 32]>::try_from(*preimage).ok()?;
345 (sha256_payment_hash(&preimage) == self.payment_hash)
346 .then_some(SpendPath::Preimage(preimage))
347 }
348 _ => None,
349 }
350 }
351
352 pub fn keypath(
354 &self,
355 utxos: &[SwapUtxo],
356 claim_privkey: SecretKey,
357 refund_privkey: SecretKey,
358 destination: &str,
359 fee_rate: f64,
360 ) -> Result<String, TxError> {
361 let params = match &self.inner {
362 Inner::Bitcoin(script) => KeypathSpendParams::Bitcoin {
363 inputs: utxo::bitcoin_inputs(utxos).map_err(wrong_chain)?,
364 script: (**script).clone(),
365 claim_privkey,
366 refund_privkey,
367 },
368 Inner::Liquid {
369 script,
370 blinding_key,
371 ctx,
372 } => KeypathSpendParams::Liquid {
373 inputs: utxo::liquid_inputs(utxos).map_err(wrong_chain)?,
374 blinding_key: *blinding_key,
375 script: (**script).clone(),
376 genesis_hash: ctx.genesis_hash,
377 policy_asset: ctx.policy_asset,
378 claim_privkey,
379 refund_privkey,
380 },
381 };
382 transaction::build_refund_by_keypath(params, destination, fee_rate)
383 }
384
385 pub fn refund_by_csv(
386 &self,
387 utxos: &[SwapUtxo],
388 refund_privkey: SecretKey,
389 destination: &str,
390 fee_rate: f64,
391 ) -> Result<String, TxError> {
392 let params = match &self.inner {
393 Inner::Bitcoin(script) => RefundByCsvParams::Bitcoin {
394 inputs: utxo::bitcoin_inputs(utxos).map_err(wrong_chain)?,
395 script: (**script).clone(),
396 refund_privkey,
397 },
398 Inner::Liquid {
399 script,
400 blinding_key,
401 ctx,
402 } => RefundByCsvParams::Liquid {
403 inputs: utxo::liquid_inputs(utxos).map_err(wrong_chain)?,
404 blinding_key: *blinding_key,
405 script: (**script).clone(),
406 genesis_hash: ctx.genesis_hash,
407 policy_asset: ctx.policy_asset,
408 refund_privkey,
409 },
410 };
411 transaction::build_refund_by_csv(params, destination, fee_rate)
412 }
413
414 pub fn claim_by_preimage(
415 &self,
416 utxos: &[SwapUtxo],
417 claim_privkey: SecretKey,
418 preimage: [u8; 32],
419 destination: &str,
420 fee_rate: f64,
421 ) -> Result<String, TxError> {
422 let params = match &self.inner {
423 Inner::Bitcoin(script) => ClaimByPreimageParams::Bitcoin {
424 inputs: utxo::bitcoin_inputs(utxos).map_err(wrong_chain)?,
425 script: (**script).clone(),
426 claim_privkey,
427 preimage,
428 },
429 Inner::Liquid {
430 script,
431 blinding_key,
432 ctx,
433 } => ClaimByPreimageParams::Liquid {
434 inputs: utxo::liquid_inputs(utxos).map_err(wrong_chain)?,
435 blinding_key: *blinding_key,
436 script: (**script).clone(),
437 genesis_hash: ctx.genesis_hash,
438 policy_asset: ctx.policy_asset,
439 claim_privkey,
440 preimage,
441 },
442 };
443 transaction::build_claim_by_preimage(params, destination, fee_rate)
444 }
445
446 #[cfg(test)]
447 pub(crate) fn bitcoin(&self) -> Option<&BitcoinSwapScript> {
448 match &self.inner {
449 Inner::Bitcoin(script) => Some(script),
450 Inner::Liquid { .. } => None,
451 }
452 }
453
454 #[cfg(test)]
455 pub(crate) fn liquid(&self) -> Option<&LiquidSwapScript> {
456 match &self.inner {
457 Inner::Liquid { script, .. } => Some(script),
458 Inner::Bitcoin(_) => None,
459 }
460 }
461
462 fn unblinding(&self) -> Option<(&SecretKey, AssetId)> {
463 match &self.inner {
464 Inner::Bitcoin(_) => None,
465 Inner::Liquid {
466 blinding_key, ctx, ..
467 } => Some((blinding_key, ctx.policy_asset)),
468 }
469 }
470}
471
472fn wrong_chain(e: UtxoError) -> TxError {
473 TxError::InvalidPrevout(e.to_string())
474}