1use crate::error::ErrorContext;
2use crate::swap_storage::SwapStorage;
3use crate::utils::timeout_op;
4use crate::wallet::BoardingWallet;
5use crate::wallet::OnchainWallet;
6use crate::Blockchain;
7use crate::Client;
8use crate::Error;
9use ark_core::coin_select::select_vtxos;
10use ark_core::intent;
11use ark_core::script::extract_checksig_pubkeys;
12use ark_core::send;
13use ark_core::send::build_offchain_transactions;
14use ark_core::send::sign_ark_transaction;
15use ark_core::send::sign_checkpoint_transaction;
16use ark_core::send::OffchainTransactions;
17use ark_core::ArkAddress;
18use ark_core::ErrorContext as _;
19use bitcoin::key::Secp256k1;
20use bitcoin::psbt;
21use bitcoin::secp256k1;
22use bitcoin::secp256k1::schnorr;
23use bitcoin::Amount;
24use bitcoin::OutPoint;
25use bitcoin::TxOut;
26use bitcoin::Txid;
27use bitcoin::XOnlyPublicKey;
28use std::time::Duration;
29
30impl<B, W, S, K> Client<B, W, S, K>
31where
32 B: Blockchain,
33 W: BoardingWallet + OnchainWallet,
34 S: SwapStorage + 'static,
35 K: crate::KeyProvider,
36{
37 pub async fn send_vtxo(&self, address: ArkAddress, amount: Amount) -> Result<Txid, Error> {
49 let (vtxo_list, script_pubkey_to_vtxo_map) = self
50 .list_vtxos()
51 .await
52 .context("failed to get spendable VTXOs")?;
53
54 let spendable_virtual_tx_outpoints = vtxo_list
56 .spendable_offchain()
57 .map(|vtxo| ark_core::coin_select::VirtualTxOutPoint {
58 outpoint: vtxo.outpoint,
59 script_pubkey: vtxo.script.clone(),
60 expire_at: vtxo.expires_at,
61 amount: vtxo.amount,
62 })
63 .collect::<Vec<_>>();
64
65 let selected_coins = select_vtxos(
66 spendable_virtual_tx_outpoints,
67 amount,
68 self.server_info.dust,
69 true,
70 )
71 .map_err(Error::from)
72 .context("failed to select coins")?;
73
74 let vtxo_inputs = selected_coins
75 .into_iter()
76 .map(|virtual_tx_outpoint| {
77 let vtxo = script_pubkey_to_vtxo_map
78 .get(&virtual_tx_outpoint.script_pubkey)
79 .ok_or_else(|| {
80 ark_core::Error::ad_hoc(format!(
81 "missing VTXO for script pubkey: {}",
82 virtual_tx_outpoint.script_pubkey
83 ))
84 })?;
85
86 let (forfeit_script, control_block) = vtxo
87 .forfeit_spend_info()
88 .context("failed to get forfeit spend info")?;
89
90 Ok(send::VtxoInput::new(
91 forfeit_script,
92 None,
93 control_block,
94 vtxo.tapscripts(),
95 vtxo.script_pubkey(),
96 virtual_tx_outpoint.amount,
97 virtual_tx_outpoint.outpoint,
98 ))
99 })
100 .collect::<Result<Vec<_>, Error>>()?;
101
102 self.build_and_sign_offchain_tx(vtxo_inputs, address, amount)
103 .await
104 }
105
106 pub async fn send_vtxo_selection(
131 &self,
132 vtxo_outpoints: &[OutPoint],
133 address: ArkAddress,
134 amount: Amount,
135 ) -> Result<Txid, Error> {
136 let (vtxo_list, script_pubkey_to_vtxo_map) =
137 self.list_vtxos().await.context("failed to get VTXO list")?;
138
139 let all_spendable = vtxo_list
141 .spendable_offchain()
142 .map(|vtxo| ark_core::coin_select::VirtualTxOutPoint {
143 outpoint: vtxo.outpoint,
144 script_pubkey: vtxo.script.clone(),
145 expire_at: vtxo.expires_at,
146 amount: vtxo.amount,
147 })
148 .collect::<Vec<_>>();
149
150 let selected_coins: Vec<_> = all_spendable
152 .into_iter()
153 .filter(|vtxo| vtxo_outpoints.contains(&vtxo.outpoint))
154 .collect();
155
156 if selected_coins.is_empty() {
157 return Err(Error::ad_hoc("no matching VTXO outpoints found"));
158 }
159
160 let total_amount = selected_coins
162 .iter()
163 .fold(Amount::ZERO, |acc, vtxo| acc + vtxo.amount);
164
165 if total_amount < amount {
166 return Err(Error::coin_select(format!(
167 "insufficient VTXO amount: {} < {}",
168 total_amount, amount
169 )));
170 }
171
172 let vtxo_inputs = selected_coins
174 .into_iter()
175 .map(|virtual_tx_outpoint| {
176 let vtxo = script_pubkey_to_vtxo_map
177 .get(&virtual_tx_outpoint.script_pubkey)
178 .ok_or_else(|| {
179 ark_core::Error::ad_hoc(format!(
180 "missing VTXO for script pubkey: {}",
181 virtual_tx_outpoint.script_pubkey
182 ))
183 })?;
184
185 let (forfeit_script, control_block) = vtxo
186 .forfeit_spend_info()
187 .context("failed to get forfeit spend info")?;
188
189 Ok(send::VtxoInput::new(
190 forfeit_script,
191 None,
192 control_block,
193 vtxo.tapscripts(),
194 vtxo.script_pubkey(),
195 virtual_tx_outpoint.amount,
196 virtual_tx_outpoint.outpoint,
197 ))
198 })
199 .collect::<Result<Vec<_>, Error>>()?;
200
201 self.build_and_sign_offchain_tx(vtxo_inputs, address, amount)
202 .await
203 }
204
205 #[cfg(feature = "test-utils")]
212 pub async fn submit_offchain_tx(
213 &self,
214 vtxo_inputs: Vec<send::VtxoInput>,
215 address: ArkAddress,
216 amount: Amount,
217 ) -> Result<Txid, Error> {
218 let (change_address, _) = self.get_offchain_address()?;
219
220 let OffchainTransactions {
221 mut ark_tx,
222 checkpoint_txs,
223 } = build_offchain_transactions(
224 &[(&address, amount)],
225 Some(&change_address),
226 &vtxo_inputs,
227 &self.server_info,
228 )
229 .map_err(Error::from)
230 .context("failed to build offchain transactions")?;
231
232 for i in 0..checkpoint_txs.len() {
233 let sign_fn = |input: &mut psbt::Input,
234 msg: secp256k1::Message|
235 -> Result<
236 Vec<(schnorr::Signature, XOnlyPublicKey)>,
237 ark_core::Error,
238 > {
239 match &input.witness_script {
240 None => Err(ark_core::Error::ad_hoc(
241 "Missing witness script for psbt::Input when signing ark transaction",
242 )),
243 Some(script) => {
244 let mut res = vec![];
245 let pks = extract_checksig_pubkeys(script);
246 for pk in pks {
247 if let Ok(keypair) = self.keypair_by_pk(&pk) {
248 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
249 let pk = keypair.x_only_public_key().0;
250 res.push((sig, pk))
251 }
252 }
253 Ok(res)
254 }
255 }
256 };
257
258 sign_ark_transaction(sign_fn, &mut ark_tx, i)?;
259 }
260
261 let ark_txid = ark_tx.unsigned_tx.compute_txid();
262
263 self.network_client()
264 .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
265 .await
266 .map_err(Error::ark_server)
267 .context("failed to submit offchain transaction request")?;
268
269 Ok(ark_txid)
270 }
271
272 async fn build_and_sign_offchain_tx(
276 &self,
277 vtxo_inputs: Vec<send::VtxoInput>,
278 address: ArkAddress,
279 amount: Amount,
280 ) -> Result<Txid, Error> {
281 let (change_address, change_address_vtxo) = self.get_offchain_address()?;
282
283 let OffchainTransactions {
284 mut ark_tx,
285 checkpoint_txs,
286 } = build_offchain_transactions(
287 &[(&address, amount)],
288 Some(&change_address),
289 &vtxo_inputs,
290 &self.server_info,
291 )
292 .map_err(Error::from)
293 .context("failed to build offchain transactions")?;
294
295 for i in 0..checkpoint_txs.len() {
296 let sign_fn = |input: &mut psbt::Input,
297 msg: secp256k1::Message|
298 -> Result<
299 Vec<(schnorr::Signature, XOnlyPublicKey)>,
300 ark_core::Error,
301 > {
302 match &input.witness_script {
303 None => Err(ark_core::Error::ad_hoc(
304 "Missing witness script for psbt::Input when signing ark transaction",
305 )),
306 Some(script) => {
307 let mut res = vec![];
308 let pks = extract_checksig_pubkeys(script);
309 for pk in pks {
310 if let Ok(keypair) = self.keypair_by_pk(&pk) {
311 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
312 let pk = keypair.x_only_public_key().0;
313 res.push((sig, pk))
314 }
315 }
316 Ok(res)
317 }
318 }
319 };
320
321 sign_ark_transaction(sign_fn, &mut ark_tx, i)?;
322 }
323
324 let ark_txid = ark_tx.unsigned_tx.compute_txid();
325
326 let mut res = self
327 .network_client()
328 .submit_offchain_transaction_request(ark_tx, checkpoint_txs.clone())
329 .await
330 .map_err(Error::ark_server)
331 .context("failed to submit offchain transaction request")?;
332
333 let client_checkpoint_ws: std::collections::HashMap<_, _> = checkpoint_txs
337 .iter()
338 .map(|cp| {
339 let txid = cp.unsigned_tx.compute_txid();
340 let ws = cp.inputs[0].witness_script.clone();
341 (txid, ws)
342 })
343 .collect();
344
345 for checkpoint_psbt in res.signed_checkpoint_txs.iter_mut() {
346 let sign_fn = |input: &mut psbt::Input,
347 msg: secp256k1::Message|
348 -> Result<
349 Vec<(schnorr::Signature, XOnlyPublicKey)>,
350 ark_core::Error,
351 > {
352 match &input.witness_script {
353 None => Err(ark_core::Error::ad_hoc(
354 "Missing witness script for psbt::Input signing checkpoint tx",
355 )),
356 Some(script) => {
357 let mut res = vec![];
358 let pks = extract_checksig_pubkeys(script);
359 for pk in pks {
360 if let Ok(keypair) = self.keypair_by_pk(&pk) {
361 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
362 let pk = keypair.x_only_public_key().0;
363 res.push((sig, pk));
364 }
365 }
366 Ok(res)
367 }
368 }
369 };
370
371 let cp_txid = checkpoint_psbt.unsigned_tx.compute_txid();
372 if let Some(ws) = client_checkpoint_ws.get(&cp_txid).cloned().flatten() {
373 checkpoint_psbt.inputs[0].witness_script = Some(ws);
374 }
375
376 sign_checkpoint_transaction(sign_fn, checkpoint_psbt)?;
377 }
378
379 self.finalize_with_retry(ark_txid, res.signed_checkpoint_txs)
380 .await?;
381
382 let used_pk = change_address_vtxo.owner_pk();
383 if let Err(err) = self.inner.key_provider.mark_as_used(&used_pk) {
384 tracing::warn!(
385 "Failed updating keypair cache for used change address: {:?} ",
386 err
387 );
388 }
389
390 Ok(ark_txid)
391 }
392
393 async fn finalize_with_retry(
399 &self,
400 ark_txid: Txid,
401 signed_checkpoint_txs: Vec<bitcoin::Psbt>,
402 ) -> Result<(), Error> {
403 const MAX_RETRIES: usize = 3;
404
405 let mut last_err = None;
406
407 for attempt in 0..=MAX_RETRIES {
408 if attempt > 0 {
409 let delay = Duration::from_millis(500 * (1 << (attempt - 1)));
410 tracing::warn!(
411 %ark_txid,
412 attempt,
413 ?delay,
414 "Retrying finalize after transient failure"
415 );
416 crate::utils::sleep(delay).await;
417 }
418
419 match timeout_op(
420 self.inner.timeout,
421 self.network_client()
422 .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs.clone()),
423 )
424 .await
425 {
426 Ok(Ok(_)) => return Ok(()),
427 Ok(Err(e)) => {
428 last_err = Some(Error::ark_server(e));
429 }
430 Err(e) => {
431 last_err = Some(e);
432 }
433 }
434 }
435
436 Err(last_err
437 .expect("at least one attempt was made")
438 .context("failed to finalize offchain transaction after retries"))
439 }
440
441 pub async fn list_pending_offchain_txs(
450 &self,
451 ) -> Result<Vec<ark_core::server::PendingTx>, Error> {
452 self.fetch_pending_offchain_txs().await
453 }
454
455 pub async fn continue_pending_offchain_txs(&self) -> Result<Vec<Txid>, Error> {
467 let pending_txs = self.fetch_pending_offchain_txs().await?;
468
469 if pending_txs.is_empty() {
470 return Ok(vec![]);
471 }
472
473 let mut finalized_txids = Vec::new();
474
475 for pending_tx in pending_txs {
476 let ark_txid = pending_tx.ark_txid;
477 let mut signed_checkpoint_txs = pending_tx.signed_checkpoint_txs;
478
479 let ark_tx_input_index_by_checkpoint_txid: std::collections::HashMap<_, _> = pending_tx
483 .signed_ark_tx
484 .unsigned_tx
485 .input
486 .iter()
487 .enumerate()
488 .map(|(i, inp)| (inp.previous_output.txid, i))
489 .collect();
490
491 for checkpoint_psbt in signed_checkpoint_txs.iter_mut() {
492 if checkpoint_psbt.inputs[0].witness_script.is_none() {
493 let checkpoint_txid = checkpoint_psbt.unsigned_tx.compute_txid();
496 let ark_input_idx = ark_tx_input_index_by_checkpoint_txid
497 .get(&checkpoint_txid)
498 .ok_or_else(|| {
499 Error::ad_hoc(format!(
500 "checkpoint txid {checkpoint_txid} not found in ark tx inputs for pending tx {ark_txid}"
501 ))
502 })?;
503
504 let witness_script = pending_tx
505 .signed_ark_tx
506 .inputs
507 .get(*ark_input_idx)
508 .and_then(|input| input.witness_script.clone())
509 .ok_or_else(|| {
510 Error::ad_hoc(format!(
511 "missing witness script on ark tx input {ark_input_idx} for pending tx {ark_txid}"
512 ))
513 })?;
514
515 checkpoint_psbt.inputs[0].witness_script = Some(witness_script);
516 }
517
518 let sign_fn = |input: &mut psbt::Input,
519 msg: secp256k1::Message|
520 -> Result<
521 Vec<(schnorr::Signature, XOnlyPublicKey)>,
522 ark_core::Error,
523 > {
524 match &input.witness_script {
525 None => Err(ark_core::Error::ad_hoc(
526 "Missing witness script for psbt::Input signing checkpoint tx",
527 )),
528 Some(script) => {
529 let mut res = vec![];
530 let pks = extract_checksig_pubkeys(script);
531 for pk in pks {
532 if let Ok(keypair) = self.keypair_by_pk(&pk) {
533 let sig =
534 Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
535 let pk = keypair.x_only_public_key().0;
536 res.push((sig, pk));
537 }
538 }
539 Ok(res)
540 }
541 }
542 };
543
544 sign_checkpoint_transaction(sign_fn, checkpoint_psbt)?;
545 }
546
547 timeout_op(
548 self.inner.timeout,
549 self.network_client()
550 .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs),
551 )
552 .await?
553 .map_err(Error::ark_server)
554 .context("failed to finalize pending offchain transaction")?;
555
556 finalized_txids.push(ark_txid);
557 }
558
559 Ok(finalized_txids)
560 }
561
562 async fn fetch_pending_offchain_txs(&self) -> Result<Vec<ark_core::server::PendingTx>, Error> {
567 const MAX_INPUTS_PER_INTENT: usize = 20;
568
569 let ark_addresses = self.get_offchain_addresses()?;
570
571 let script_pubkey_to_vtxo_map: std::collections::HashMap<_, _> = ark_addresses
572 .iter()
573 .map(|(a, v)| (a.to_p2tr_script_pubkey(), v.clone()))
574 .collect();
575
576 let addresses = ark_addresses.iter().map(|(a, _)| *a);
580 let request = ark_core::server::GetVtxosRequest::new_for_addresses(addresses)
581 .pending_only()
582 .map_err(Error::from)?;
583
584 let vtxos = self
585 .fetch_all_vtxos(request)
586 .await
587 .context("failed to fetch pending VTXOs")?;
588
589 tracing::debug!(num_pending_vtxos = vtxos.len(), "Fetched pending VTXOs");
590
591 if vtxos.is_empty() {
592 return Ok(vec![]);
593 }
594
595 let secp = Secp256k1::new();
596 let mut all_pending_txs = Vec::new();
597 let mut seen_ark_txids = std::collections::HashSet::new();
598
599 for (batch_idx, batch) in vtxos.chunks(MAX_INPUTS_PER_INTENT).enumerate() {
601 let mut vtxo_inputs = Vec::new();
602 for virtual_tx_outpoint in batch {
603 let vtxo = match script_pubkey_to_vtxo_map.get(&virtual_tx_outpoint.script) {
604 Some(v) => v,
605 None => {
606 tracing::warn!(
607 outpoint = %virtual_tx_outpoint.outpoint,
608 script = %virtual_tx_outpoint.script,
609 "Skipping VTXO with unknown script"
610 );
611 continue;
612 }
613 };
614 let spend_info = vtxo
615 .forfeit_spend_info()
616 .context("failed to get forfeit spend info")?;
617
618 vtxo_inputs.push(intent::Input::new(
619 virtual_tx_outpoint.outpoint,
620 vtxo.exit_delay(),
621 None,
622 TxOut {
623 value: virtual_tx_outpoint.amount,
624 script_pubkey: vtxo.script_pubkey(),
625 },
626 vtxo.tapscripts(),
627 spend_info,
628 false,
629 virtual_tx_outpoint.is_swept,
630 ));
631 }
632
633 if vtxo_inputs.is_empty() {
634 continue;
635 }
636
637 tracing::debug!(
638 batch = batch_idx,
639 num_inputs = vtxo_inputs.len(),
640 "Querying server for pending txs"
641 );
642
643 let message = intent::IntentMessage::GetPendingTx { expire_at: 0 };
645
646 let sign_for_vtxo_fn = |input: &mut psbt::Input,
647 msg: secp256k1::Message|
648 -> Result<
649 Vec<(schnorr::Signature, XOnlyPublicKey)>,
650 ark_core::Error,
651 > {
652 match &input.witness_script {
653 None => Err(ark_core::Error::ad_hoc(
654 "Missing witness script in psbt::Input when signing get-pending-tx intent",
655 )),
656 Some(script) => {
657 let pks = extract_checksig_pubkeys(script);
658 let mut res = vec![];
659 for pk in &pks {
660 if let Ok(keypair) = self.keypair_by_pk(pk) {
661 let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
662 res.push((sig, keypair.x_only_public_key().0));
663 }
664 }
665 Ok(res)
666 }
667 }
668 };
669
670 let sign_for_onchain_fn =
671 |_: &mut psbt::Input,
672 _: secp256k1::Message|
673 -> Result<(schnorr::Signature, XOnlyPublicKey), ark_core::Error> {
674 Err(ark_core::Error::ad_hoc(
675 "unexpected onchain input in get-pending-tx intent",
676 ))
677 };
678
679 let get_pending_intent = intent::make_intent(
680 sign_for_vtxo_fn,
681 sign_for_onchain_fn,
682 vtxo_inputs,
683 vec![],
684 message,
685 )?;
686
687 let pending_txs = self
688 .network_client()
689 .get_pending_tx(get_pending_intent)
690 .await
691 .map_err(Error::ark_server)
692 .context("failed to get pending transactions")?;
693
694 tracing::debug!(
695 batch = batch_idx,
696 num_pending_txs = pending_txs.len(),
697 "Server response for batch"
698 );
699
700 for tx in pending_txs {
701 if seen_ark_txids.insert(tx.ark_txid) {
702 tracing::info!(
703 ark_txid = %tx.ark_txid,
704 "Found pending transaction"
705 );
706 all_pending_txs.push(tx);
707 }
708 }
709 }
710
711 tracing::info!(
712 num_pending_txs = all_pending_txs.len(),
713 "Total pending transactions found"
714 );
715
716 Ok(all_pending_txs)
717 }
718}