Skip to main content

anyswap_sdk/
swap_script.rs

1//! The swap script of one on-chain side with everything spending it needs:
2//! the chain, the network, and on Liquid the shared blinding key and chain
3//! parameters. [`SwapIn`](crate::swap_in::SwapIn),
4//! [`SwapOut`](crate::swap_out::SwapOut) and the driver all build their
5//! transactions through here.
6
7use 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/// Which side of a swap an on-chain output is. `Source`: we refund and the
49/// server claims. `Dest`: the reverse.
50#[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/// One on-chain side of a swap: what its script is built from. Our own key
66/// and blinding key derive from `swap_id`, so only the server's are kept.
67#[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
79/// A decoded transaction of either chain.
80pub 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    /// The miner fee: Liquid's explicit fee output, or on Bitcoin what the
118    /// inputs, each worth `value`, carry over the outputs. `None` on Bitcoin
119    /// while an input's value is unknown.
120    #[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/// How an input spent a swap output: the three ways its script opens.
136#[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    /// The script of `terms`, with our key in the role the side gives it and,
161    /// on Liquid, the blinding key shared with the server. Every Liquid path
162    /// goes through here, so none can skip the asset check.
163    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    /// The outputs of `tx` that are this swap's: paying the script, and on
238    /// Liquid unblinding to the policy asset with the shared key.
239    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    /// The output of `tx` at `vout`, checked to pay this script and, on
266    /// Liquid, to hold its asset, with its value.
267    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    /// The value in sats, or `None` when the output is not this swap's.
299    pub fn value(&self, utxo: &SwapUtxo) -> Option<u64> {
300        utxo.value(self.unblinding())
301    }
302
303    /// Splits `utxos` into the ones worth spending at `fee_rate` and the rest.
304    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    /// How input `vin` of `tx` spent this script, read from its witness.
314    /// `None` when the witness opens none of its paths.
315    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        // The last item starting 0x50 is the annex (BIP341).
327        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    /// A cooperative keypath spend, claim or refund, with both keys in hand.
353    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}