1pub use anyswap_core::types::{ErrorType, SwapInStatus as Status};
9use anyswap_core::{
10 api::SwapState,
11 invoice::{self, InvoiceError},
12 transaction::TxError,
13 types::{Chain, Outpoint, SwapType},
14 utils::sha256_payment_hash,
15};
16use bitcoin::secp256k1::{PublicKey, SecretKey};
17use thiserror::Error;
18use uuid::Uuid;
19
20use crate::{
21 codec::{self, CodecError},
22 context::SwapContext,
23 swap_script::{ScriptError, Side, SwapScript, Terms, Tx},
24 utxo::{SwapUtxo, UtxoError},
25 wallet::SwapWallet,
26};
27
28#[derive(Debug, Error)]
29pub enum SwapInError {
30 #[error("expected swap type SwapIn, got {0}")]
31 UnexpectedSwapType(SwapType),
32
33 #[error("unknown status: {0}")]
34 UnknownStatus(String),
35
36 #[error("unknown error type: {0}")]
37 UnknownErrorType(String),
38
39 #[error("missing field: {0}")]
40 MissingField(&'static str),
41
42 #[error("a keypath refund needs the claim key the server reveals when it cancels")]
43 KeypathRefundUnavailable,
44
45 #[error("this client is not set up for Liquid; add the Liquid parameters to the context")]
46 LiquidNotConfigured,
47
48 #[error("the swap trades asset {found}, but this client is set up for {expected}")]
49 UnexpectedAsset { expected: String, found: String },
50
51 #[error("wallet: {0}")]
52 Wallet(String),
53
54 #[error("invalid swap script: {0}")]
55 Script(String),
56
57 #[error("the output does not belong to this swap")]
58 UnknownOutput,
59
60 #[error("the server has not reported a preimage in this status")]
61 NoPreimage,
62
63 #[error(transparent)]
64 Invoice(#[from] InvoiceError),
65
66 #[error("the preimage hashes to {found}, but the claim invoice's payment hash is {expected}")]
67 UnexpectedPaymentHash { expected: String, found: String },
68
69 #[error(transparent)]
70 Utxo(#[from] UtxoError),
71
72 #[error(transparent)]
73 Tx(#[from] TxError),
74
75 #[error(transparent)]
76 Codec(#[from] CodecError),
77}
78
79impl From<ScriptError> for SwapInError {
80 fn from(e: ScriptError) -> Self {
81 match e {
82 ScriptError::LiquidNotConfigured => Self::LiquidNotConfigured,
83 ScriptError::UnexpectedAsset { expected, found } => {
84 Self::UnexpectedAsset { expected, found }
85 }
86 ScriptError::MissingField(field) => Self::MissingField(field),
87 ScriptError::Wallet(e) => Self::Wallet(e),
88 ScriptError::Script(e) => Self::Script(e),
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
97pub struct SwapIn {
98 pub swap_id: Uuid,
99 pub protocol_version: String,
100 pub chain: Chain,
101 pub asset: String,
102 pub receive_amount: u64,
103 pub send_amount: u64,
104 pub base_fee: u64,
105 pub service_fee: u64,
106 pub payment_hash: [u8; 32],
107 pub csv: u32,
108 pub refund_pubkey: PublicKey,
110 pub claim_pubkey: PublicKey,
112 pub user_blinding_pubkey: Option<PublicKey>,
114 pub service_blinding_pubkey: Option<PublicKey>,
116 pub claim_invoice: String,
117 pub refund_address: Option<String>,
118 pub created_at: u64,
119 pub transitioned_at: u64,
120 pub status: SwapInStatus,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, strum::Display, strum::IntoStaticStr)]
135pub enum SwapInStatus {
136 AwaitOpeningTxConfirmation,
140 PayInvoice { outpoint: Outpoint },
142 AwaitClaimKeyRevealMessage {
149 outpoint: Outpoint,
150 preimage: [u8; 32],
151 },
152 AwaitClaimTxConfirmation {
154 outpoint: Outpoint,
155 preimage: [u8; 32],
156 },
157 Claimed {
159 outpoint: Outpoint,
160 preimage: [u8; 32],
161 },
162 Canceled {
167 error_code: ErrorType,
168 claim_privkey: SecretKey,
169 cancel_message: String,
170 },
171}
172
173impl SwapInStatus {
174 pub fn outpoint(&self) -> Option<&Outpoint> {
178 match self {
179 Self::AwaitOpeningTxConfirmation | Self::Canceled { .. } => None,
180 Self::PayInvoice { outpoint }
181 | Self::AwaitClaimKeyRevealMessage { outpoint, .. }
182 | Self::AwaitClaimTxConfirmation { outpoint, .. }
183 | Self::Claimed { outpoint, .. } => Some(outpoint),
184 }
185 }
186
187 pub fn preimage(&self) -> Option<&[u8; 32]> {
190 match self {
191 Self::AwaitClaimKeyRevealMessage { preimage, .. }
192 | Self::AwaitClaimTxConfirmation { preimage, .. }
193 | Self::Claimed { preimage, .. } => Some(preimage),
194 _ => None,
195 }
196 }
197
198 pub fn is_terminal(&self) -> bool {
199 matches!(self, Self::Claimed { .. } | Self::Canceled { .. })
200 }
201}
202
203impl SwapIn {
208 pub fn verify_payment(&self) -> Result<(), SwapInError> {
214 let expected = invoice::payment_hash(&self.claim_invoice)?;
215 let preimage = self.status.preimage().ok_or(SwapInError::NoPreimage)?;
216 let found = sha256_payment_hash(preimage);
217 if found != expected {
218 return Err(SwapInError::UnexpectedPaymentHash {
219 expected: hex::encode(expected),
220 found: hex::encode(found),
221 });
222 }
223 Ok(())
224 }
225
226 pub fn address<W: SwapWallet>(&self, ctx: &SwapContext<W>) -> Result<String, SwapInError> {
228 Ok(self.script(ctx)?.address())
229 }
230
231 pub fn outputs<W: SwapWallet>(
238 &self,
239 ctx: &SwapContext<W>,
240 tx_hex: &str,
241 ) -> Result<Vec<SwapUtxo>, SwapInError> {
242 let tx = Tx::decode(self.chain, tx_hex)?;
243 Ok(self.script(ctx)?.outputs(&tx))
244 }
245
246 pub fn partition_economical<W: SwapWallet>(
251 &self,
252 ctx: &SwapContext<W>,
253 utxos: &[SwapUtxo],
254 fee_rate: f64,
255 ) -> Result<(Vec<SwapUtxo>, Vec<SwapUtxo>), SwapInError> {
256 Ok(self.script(ctx)?.economical(utxos, fee_rate)?)
257 }
258
259 pub fn value<W: SwapWallet>(
262 &self,
263 ctx: &SwapContext<W>,
264 utxo: &SwapUtxo,
265 ) -> Result<u64, SwapInError> {
266 self.script(ctx)?
267 .value(utxo)
268 .ok_or(SwapInError::UnknownOutput)
269 }
270
271 pub fn refund_by_keypath<W: SwapWallet>(
275 &self,
276 ctx: &SwapContext<W>,
277 utxos: &[SwapUtxo],
278 destination: &str,
279 fee_rate: f64,
280 ) -> Result<String, SwapInError> {
281 let SwapInStatus::Canceled { claim_privkey, .. } = &self.status else {
282 return Err(SwapInError::KeypathRefundUnavailable);
283 };
284 let refund_privkey = self.refund_privkey(ctx)?;
285 Ok(self.script(ctx)?.keypath(
286 utxos,
287 *claim_privkey,
288 refund_privkey,
289 destination,
290 fee_rate,
291 )?)
292 }
293
294 pub fn refund_by_csv<W: SwapWallet>(
298 &self,
299 ctx: &SwapContext<W>,
300 utxos: &[SwapUtxo],
301 destination: &str,
302 fee_rate: f64,
303 ) -> Result<String, SwapInError> {
304 let refund_privkey = self.refund_privkey(ctx)?;
305 Ok(self
306 .script(ctx)?
307 .refund_by_csv(utxos, refund_privkey, destination, fee_rate)?)
308 }
309
310 pub fn terms(&self) -> Terms {
312 Terms {
313 swap_id: self.swap_id,
314 side: Side::Source,
315 chain: self.chain,
316 asset: self.asset.clone(),
317 csv: self.csv,
318 payment_hash: self.payment_hash,
319 server_pubkey: self.claim_pubkey,
320 server_blinding_pubkey: self.service_blinding_pubkey,
321 }
322 }
323
324 fn script<W: SwapWallet>(&self, ctx: &SwapContext<W>) -> Result<SwapScript, SwapInError> {
325 Ok(ctx.script(&self.terms())?)
326 }
327
328 fn refund_privkey<W: SwapWallet>(
330 &self,
331 ctx: &SwapContext<W>,
332 ) -> Result<SecretKey, SwapInError> {
333 ctx.wallet()
334 .source_refund_privkey(&self.swap_id)
335 .map_err(|error| SwapInError::Wallet(error.to_string()))
336 }
337}
338
339impl TryFrom<SwapState> for SwapIn {
340 type Error = SwapInError;
341
342 fn try_from(s: SwapState) -> Result<Self, SwapInError> {
343 if s.swap_type != SwapType::SwapIn {
344 return Err(SwapInError::UnexpectedSwapType(s.swap_type));
345 }
346
347 let status: Status = s
348 .status
349 .parse()
350 .map_err(|_| SwapInError::UnknownStatus(s.status.clone()))?;
351
352 let status = match status {
353 Status::AwaitOpeningTxConfirmation => SwapInStatus::AwaitOpeningTxConfirmation,
354 Status::PayInvoice => SwapInStatus::PayInvoice {
355 outpoint: source_outpoint(&s)?,
356 },
357 Status::AwaitClaimKeyRevealMessage => SwapInStatus::AwaitClaimKeyRevealMessage {
358 outpoint: source_outpoint(&s)?,
359 preimage: preimage(&s)?,
360 },
361 Status::AwaitClaimTxConfirmation => SwapInStatus::AwaitClaimTxConfirmation {
362 outpoint: source_outpoint(&s)?,
363 preimage: preimage(&s)?,
364 },
365 Status::Claimed => SwapInStatus::Claimed {
366 outpoint: source_outpoint(&s)?,
367 preimage: preimage(&s)?,
368 },
369 Status::Canceled => {
370 let raw = require(s.error_type.clone(), "error_type")?;
371 SwapInStatus::Canceled {
372 error_code: raw
373 .parse()
374 .map_err(|_| SwapInError::UnknownErrorType(raw.clone()))?,
375 claim_privkey: codec::parse_privkey(&require(
376 s.source_claim_privkey.clone(),
377 "source_claim_privkey",
378 )?)?,
379 cancel_message: require(s.cancel_message.clone(), "cancel_message")?,
380 }
381 }
382 };
383 let user_blinding_pubkey = s
384 .source_user_blinding_pubkey
385 .as_deref()
386 .map(codec::parse_pubkey)
387 .transpose()?;
388 let service_blinding_pubkey = s
389 .source_service_blinding_pubkey
390 .as_deref()
391 .map(codec::parse_pubkey)
392 .transpose()?;
393
394 Ok(SwapIn {
395 swap_id: s.swap_id,
396 protocol_version: s.protocol_version,
397 chain: require(s.source_chain, "source_chain")?,
398 asset: require(s.source_asset, "source_asset")?,
399 receive_amount: s.receive_amount,
400 send_amount: s.send_amount,
401 base_fee: s.base_fee,
402 service_fee: s.service_fee,
403 payment_hash: codec::parse_hash32(&s.payment_hash)?,
404 csv: require(s.source_csv, "source_csv")?,
405 refund_pubkey: codec::parse_pubkey(&require(
406 s.source_refund_pubkey,
407 "source_refund_pubkey",
408 )?)?,
409 claim_pubkey: codec::parse_pubkey(&require(
410 s.source_claim_pubkey,
411 "source_claim_pubkey",
412 )?)?,
413 user_blinding_pubkey,
414 service_blinding_pubkey,
415 claim_invoice: require(s.claim_invoice, "claim_invoice")?,
416 refund_address: s.user_refund_address,
417 created_at: s.created_at,
418 transitioned_at: s.transitioned_at,
419 status,
420 })
421 }
422}
423
424fn require<T>(opt: Option<T>, field: &'static str) -> Result<T, SwapInError> {
425 opt.ok_or(SwapInError::MissingField(field))
426}
427
428fn source_outpoint(s: &SwapState) -> Result<Outpoint, SwapInError> {
429 Ok(codec::parse_outpoint(
430 require(s.source_txid.as_deref(), "source_txid")?,
431 require(s.source_vout, "source_vout")?,
432 )?)
433}
434
435fn preimage(s: &SwapState) -> Result<[u8; 32], SwapInError> {
436 Ok(codec::parse_hash32(require(
437 s.preimage.as_deref(),
438 "preimage",
439 )?)?)
440}
441
442#[cfg(test)]
443mod tests {
444 use anyswap_core::{
445 script::{BitcoinSwapScript, LiquidSwapScript},
446 secp,
447 types::{ErrorType, NATIVE_ASSET, SwapNetwork},
448 };
449 use bitcoin::hashes::{Hash as _, sha256};
450
451 use super::*;
452 use crate::{
453 swap_script::SpendPath,
454 utxo::fixtures::{confidential_tx, genesis_hash, liquid_asset, liquid_destination},
455 wallet::Wallet,
456 };
457
458 const SERVICE_SIGNING_KEY: [u8; 32] = [21; 32];
459 const SERVICE_BLINDING_KEY: [u8; 32] = [22; 32];
460 const BITCOIN_DESTINATION: &str = "bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080";
461
462 fn context() -> SwapContext<Wallet> {
463 let (wallet, _) = Wallet::generate(SwapNetwork::Regtest).unwrap();
464 SwapContext::new(wallet)
465 .with_liquid(&genesis_hash(), &liquid_asset())
466 .unwrap()
467 }
468
469 fn swap_in(ctx: &SwapContext<Wallet>, chain: Chain, status: SwapInStatus) -> SwapIn {
470 let swap_id = Uuid::from_u128(11);
471 let service_signing = SecretKey::from_slice(&SERVICE_SIGNING_KEY).unwrap();
472 let service_blinding = SecretKey::from_slice(&SERVICE_BLINDING_KEY).unwrap();
473 let preimage = ctx.wallet().preimage(&swap_id).unwrap();
474 SwapIn {
475 swap_id,
476 protocol_version: "2.0.0".into(),
477 chain,
478 asset: match chain {
479 Chain::Bitcoin => NATIVE_ASSET.to_string(),
480 Chain::Liquid => liquid_asset(),
481 },
482 receive_amount: 100_000,
483 send_amount: 101_000,
484 base_fee: 1_000,
485 service_fee: 0,
486 payment_hash: sha256::Hash::hash(&preimage).to_byte_array(),
487 csv: 144,
488 refund_pubkey: ctx.wallet().source_refund_pubkey(&swap_id).unwrap(),
489 claim_pubkey: service_signing.public_key(secp()),
490 user_blinding_pubkey: Some(ctx.wallet().source_blinding_pubkey(&swap_id).unwrap()),
491 service_blinding_pubkey: Some(service_blinding.public_key(secp())),
492 claim_invoice: "lnbcrt1".into(),
493 refund_address: None,
494 created_at: 1,
495 transitioned_at: 1,
496 status,
497 }
498 }
499
500 fn canceled() -> SwapInStatus {
501 SwapInStatus::Canceled {
502 error_code: ErrorType::UserCanceled,
503 claim_privkey: SecretKey::from_slice(&SERVICE_SIGNING_KEY).unwrap(),
504 cancel_message: "canceled".into(),
505 }
506 }
507
508 #[test]
509 fn liquid_refunds_the_deposit_and_a_stray_together() {
510 let ctx = context();
511 let swap = swap_in(&ctx, Chain::Liquid, canceled());
512 let opening_tx = confidential_tx(&liquid_script(&swap, &ctx), &[101_000, 40_000]);
513
514 let utxos = swap.outputs(&ctx, &opening_tx).unwrap();
515
516 assert_eq!(utxos.len(), 2);
517 assert_eq!(swap.value(&ctx, &utxos[0]).unwrap(), 101_000);
518 assert_eq!(swap.value(&ctx, &utxos[1]).unwrap(), 40_000);
519 let destination = liquid_destination();
520 for tx_hex in [
521 swap.refund_by_csv(&ctx, &utxos, &destination, 1.0).unwrap(),
522 swap.refund_by_keypath(&ctx, &utxos, &destination, 1.0)
523 .unwrap(),
524 ] {
525 let tx = anyswap_core::transaction::liquid::decode_tx_hex(&tx_hex).unwrap();
526 assert_eq!(tx.input.len(), 2);
527 }
528 }
529
530 #[test]
531 fn liquid_skips_outputs_of_another_swap() {
532 let ctx = context();
533 let swap = swap_in(&ctx, Chain::Liquid, canceled());
534 let other = SwapIn {
535 csv: 288,
536 ..swap_in(&ctx, Chain::Liquid, canceled())
537 };
538 let other_tx = confidential_tx(&liquid_script(&other, &ctx), &[101_000]);
539
540 assert!(swap.outputs(&ctx, &other_tx).unwrap().is_empty());
541 }
542
543 #[test]
544 fn another_swaps_output_has_no_value_here() {
545 let ctx = context();
546 let swap = swap_in(&ctx, Chain::Liquid, canceled());
547 let other = SwapIn {
550 swap_id: Uuid::from_u128(99),
551 ..swap_in(&ctx, Chain::Liquid, canceled())
552 };
553 let other_tx = confidential_tx(&liquid_script(&other, &ctx), &[101_000]);
554 let theirs = other.outputs(&ctx, &other_tx).unwrap().remove(0);
555
556 assert!(matches!(
557 swap.value(&ctx, &theirs),
558 Err(SwapInError::UnknownOutput)
559 ));
560 }
561
562 #[test]
563 fn refunds_a_utxo_the_caller_supplied() {
564 let ctx = context();
565 let swap = swap_in(&ctx, Chain::Bitcoin, canceled());
566 let opening_tx = bitcoin_tx(&bitcoin_script(&swap, &ctx), &[100_000]);
567 let txid = anyswap_core::transaction::bitcoin::decode_tx_hex(&opening_tx)
568 .unwrap()
569 .compute_txid()
570 .to_string();
571 let utxos = vec![SwapUtxo::bitcoin(txid, 0, 100_000)];
574
575 let tx_hex = swap
576 .refund_by_csv(
577 &ctx,
578 &utxos,
579 "bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
580 1.0,
581 )
582 .unwrap();
583
584 let tx = anyswap_core::transaction::bitcoin::decode_tx_hex(&tx_hex).unwrap();
585 assert_eq!(tx.input.len(), 1);
586 assert_eq!(swap.value(&ctx, &utxos[0]).unwrap(), 100_000);
587 }
588
589 #[test]
590 fn a_keypath_refund_needs_the_revealed_claim_key() {
591 let ctx = context();
592 let swap = swap_in(
593 &ctx,
594 Chain::Liquid,
595 SwapInStatus::AwaitOpeningTxConfirmation,
596 );
597
598 assert!(matches!(
599 swap.refund_by_keypath(&ctx, &[], &liquid_destination(), 1.0),
600 Err(SwapInError::KeypathRefundUnavailable)
601 ));
602 }
603
604 #[test]
605 fn a_liquid_swap_needs_a_context_set_up_for_liquid() {
606 let (wallet, _) = Wallet::generate(SwapNetwork::Regtest).unwrap();
607 let bitcoin_only = SwapContext::new(wallet);
608 let swap = swap_in(&context(), Chain::Liquid, canceled());
609
610 assert!(matches!(
611 swap.address(&bitcoin_only),
612 Err(SwapInError::LiquidNotConfigured)
613 ));
614 }
615
616 #[test]
617 fn a_swap_trading_another_asset_is_refused() {
618 let (wallet, _) = Wallet::generate(SwapNetwork::Regtest).unwrap();
619 let ctx = SwapContext::new(wallet)
620 .with_liquid(&genesis_hash(), &"11".repeat(32))
621 .unwrap();
622 let swap = swap_in(&ctx, Chain::Liquid, canceled());
623
624 assert!(matches!(
625 swap.address(&ctx),
626 Err(SwapInError::UnexpectedAsset { .. })
627 ));
628 }
629
630 #[test]
631 fn partition_drops_utxos_that_cannot_pay_their_own_fee() {
632 let ctx = context();
633 let swap = swap_in(&ctx, Chain::Bitcoin, canceled());
634 let opening_tx = bitcoin_tx(&bitcoin_script(&swap, &ctx), &[100_000, 1_480]);
635
636 let utxos = swap.outputs(&ctx, &opening_tx).unwrap();
637 let (economical, dust) = swap.partition_economical(&ctx, &utxos, 10.0).unwrap();
640
641 assert_eq!(economical.len(), 1);
642 assert_eq!(swap.value(&ctx, &economical[0]).unwrap(), 100_000);
643 assert_eq!(dust.len(), 1);
644 assert_eq!(swap.value(&ctx, &dust[0]).unwrap(), 1_480);
645 }
646
647 #[test]
648 fn partition_reports_a_bad_fee_rate_instead_of_calling_everything_dust() {
649 let ctx = context();
650 let swap = swap_in(&ctx, Chain::Bitcoin, canceled());
651 let opening_tx = bitcoin_tx(&bitcoin_script(&swap, &ctx), &[100_000]);
652 let utxos = swap.outputs(&ctx, &opening_tx).unwrap();
653
654 for rate in [f64::NAN, -1.0] {
655 assert!(swap.partition_economical(&ctx, &utxos, rate).is_err());
656 }
657 }
658
659 #[test]
660 fn bitcoin_refunds_by_csv() {
661 let ctx = context();
662 let swap = swap_in(&ctx, Chain::Bitcoin, canceled());
663 let opening_tx = bitcoin_tx(&bitcoin_script(&swap, &ctx), &[100_000]);
664 let utxos = swap.outputs(&ctx, &opening_tx).unwrap();
665
666 let tx_hex = swap
667 .refund_by_csv(
668 &ctx,
669 &utxos,
670 "bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
671 1.0,
672 )
673 .unwrap();
674
675 let tx = anyswap_core::transaction::bitcoin::decode_tx_hex(&tx_hex).unwrap();
676 assert_eq!(tx.input.len(), 1);
677 assert_eq!(tx.input[0].sequence.to_consensus_u32(), swap.csv);
678 }
679
680 #[test]
681 fn reads_each_spend_path_from_the_witness() {
682 let ctx = context();
683 let server = SecretKey::from_slice(&SERVICE_SIGNING_KEY).unwrap();
684 for chain in [Chain::Bitcoin, Chain::Liquid] {
685 let swap = swap_in(&ctx, chain, canceled());
686 let script = swap.script(&ctx).unwrap();
687 let (opening_tx, destination) = match chain {
688 Chain::Bitcoin => (
689 bitcoin_tx(&bitcoin_script(&swap, &ctx), &[100_000]),
690 BITCOIN_DESTINATION.to_string(),
691 ),
692 Chain::Liquid => (
693 confidential_tx(&liquid_script(&swap, &ctx), &[100_000]),
694 liquid_destination(),
695 ),
696 };
697 let utxos = swap.outputs(&ctx, &opening_tx).unwrap();
698 let preimage = ctx.wallet().preimage(&swap.swap_id).unwrap();
699 let path = |tx_hex: String| script.spend_path(&Tx::decode(chain, &tx_hex).unwrap(), 0);
700
701 let keypath = swap.refund_by_keypath(&ctx, &utxos, &destination, 1.0);
702 let refund = swap.refund_by_csv(&ctx, &utxos, &destination, 1.0);
703 let claim = script.claim_by_preimage(&utxos, server, preimage, &destination, 1.0);
704 assert_eq!(path(keypath.unwrap()), Some(SpendPath::Keypath));
705 assert_eq!(path(refund.unwrap()), Some(SpendPath::Csv));
706 assert_eq!(path(claim.unwrap()), Some(SpendPath::Preimage(preimage)));
707 }
708 }
709
710 #[test]
711 fn reads_the_spend_path_past_an_annex() {
712 let ctx = context();
713 let swap = swap_in(&ctx, Chain::Bitcoin, canceled());
714 let script = swap.script(&ctx).unwrap();
715 let opening_tx = bitcoin_tx(&bitcoin_script(&swap, &ctx), &[100_000]);
716 let utxos = swap.outputs(&ctx, &opening_tx).unwrap();
717
718 let keypath = swap.refund_by_keypath(&ctx, &utxos, BITCOIN_DESTINATION, 1.0);
719 let refund = swap.refund_by_csv(&ctx, &utxos, BITCOIN_DESTINATION, 1.0);
720 for (tx_hex, expected) in [
721 (keypath.unwrap(), SpendPath::Keypath),
722 (refund.unwrap(), SpendPath::Csv),
723 ] {
724 let mut tx = anyswap_core::transaction::bitcoin::decode_tx_hex(&tx_hex).unwrap();
725 tx.input[0].witness.push([0x50, 1]);
726 assert_eq!(script.spend_path(&Tx::Bitcoin(tx), 0), Some(expected));
727 }
728 }
729
730 #[test]
731 fn reads_no_spend_path_from_a_witness_the_script_does_not_open() {
732 let ctx = context();
733 let swap = swap_in(&ctx, Chain::Bitcoin, canceled());
734 let script = swap.script(&ctx).unwrap();
735 let opening_tx = bitcoin_tx(&bitcoin_script(&swap, &ctx), &[100_000]);
736 let utxos = swap.outputs(&ctx, &opening_tx).unwrap();
737 let tx_hex = swap
738 .refund_by_keypath(&ctx, &utxos, BITCOIN_DESTINATION, 1.0)
739 .unwrap();
740
741 let mut tx = anyswap_core::transaction::bitcoin::decode_tx_hex(&tx_hex).unwrap();
742 tx.input[0].witness.push([1]);
743 let tx = Tx::Bitcoin(tx);
744 assert_eq!(script.spend_path(&tx, 0), None);
745 assert_eq!(script.spend_path(&tx, 1), None);
746 }
747
748 fn liquid_script(swap: &SwapIn, ctx: &SwapContext<Wallet>) -> LiquidSwapScript {
750 swap.script(ctx).unwrap().liquid().unwrap().clone()
751 }
752
753 fn bitcoin_script(swap: &SwapIn, ctx: &SwapContext<Wallet>) -> BitcoinSwapScript {
754 swap.script(ctx).unwrap().bitcoin().unwrap().clone()
755 }
756
757 fn bitcoin_tx(script: &BitcoinSwapScript, values: &[u64]) -> String {
759 let tx = bitcoin::Transaction {
760 version: bitcoin::transaction::Version::TWO,
761 lock_time: bitcoin::absolute::LockTime::ZERO,
762 input: vec![],
763 output: values
764 .iter()
765 .map(|value| bitcoin::TxOut {
766 value: bitcoin::Amount::from_sat(*value),
767 script_pubkey: script.script_pubkey(),
768 })
769 .collect(),
770 };
771 anyswap_core::transaction::bitcoin::encode_tx_hex(&tx)
772 }
773}