1use anyhow::Context;
2use bitcoin::Amount;
3use bitcoin::hex::DisplayHex;
4use bitcoin::secp256k1::PublicKey;
5use log::{info, error};
6
7use ark::{VtxoPolicy, ProtocolEncoding};
8use ark::arkoor::ArkoorDestination;
9use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
10use ark::vtxo::{Full, Vtxo, VtxoId};
11use server_rpc::protos;
12
13use crate::subsystem::Subsystem;
14use crate::{ArkoorMovement, VtxoDelivery, MovementUpdate, Wallet, WalletVtxo};
15use crate::movement::MovementDestination;
16use crate::movement::manager::OnDropStatus;
17
18pub struct ArkoorCreateResult {
20 pub inputs: Vec<VtxoId>,
21 pub created: Vec<Vtxo<Full>>,
22 pub change: Vec<Vtxo<Full>>,
23}
24
25impl Wallet {
26 pub async fn validate_arkoor_address(&self, address: &ark::Address) -> anyhow::Result<()> {
30 let (srv, _) = self.require_server().await?;
31
32 if !address.ark_id().is_for_server(srv.ark_info().await?.server_pubkey) {
33 bail!("Ark address is for different server");
34 }
35
36 match address.policy() {
38 VtxoPolicy::Pubkey(_) => {},
39 VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) => {
40 bail!("VTXO policy in address cannot be used for arkoor payment: {}",
41 address.policy().policy_type(),
42 );
43 }
44 }
45
46 if address.delivery().is_empty() {
47 bail!("No VTXO delivery mechanism provided in address");
48 }
49 if !address.delivery().iter().any(|d| !d.is_unknown()) {
53 for d in address.delivery() {
54 if let VtxoDelivery::Unknown { delivery_type, data } = d {
55 info!("Unknown delivery in address: type={:#x}, data={}",
56 delivery_type, data.as_hex(),
57 );
58 }
59 }
60 }
61
62 Ok(())
63 }
64
65 pub(crate) async fn create_checkpointed_arkoor(
66 &self,
67 arkoor_dest: ArkoorDestination,
68 change_pubkey: PublicKey,
69 ) -> anyhow::Result<ArkoorCreateResult> {
70 let inputs = self.select_vtxos_to_cover(arkoor_dest.total_amount).await?;
71 self.create_checkpointed_arkoor_with_vtxos(
72 arkoor_dest,
73 change_pubkey,
74 inputs.into_iter(),
75 ).await
76 }
77
78 pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
79 &self,
80 arkoor_dest: ArkoorDestination,
81 change_pubkey: PublicKey,
82 inputs: impl IntoIterator<Item = WalletVtxo>,
83 ) -> anyhow::Result<ArkoorCreateResult> {
84 if arkoor_dest.policy.user_pubkey() == change_pubkey {
85 bail!("Cannot create arkoor to same address as change");
86 }
87
88 let (mut srv, _) = self.require_server().await?;
90 let (input_ids, inputs) = inputs.into_iter()
91 .map(|v| (v.id(), v))
92 .collect::<(Vec<_>, Vec<_>)>();
93
94 let mut user_keypairs = Vec::with_capacity(inputs.len());
95 for vtxo in &inputs {
96 user_keypairs.push(self.get_vtxo_key(vtxo).await?);
97 }
98
99 let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
100 inputs.into_iter().map(|v| v.vtxo),
101 arkoor_dest.clone(),
102 VtxoPolicy::new_pubkey(change_pubkey),
103 )
104 .context("Failed to construct arkoor package")?
105 .generate_user_nonces(&user_keypairs)
106 .context("invalid nb of keypairs")?;
107
108 let cosign_request = protos::ArkoorPackageCosignRequest::from(
109 builder.cosign_request().convert_vtxo(|vtxo| vtxo.id()),
110 );
111
112 let response = srv.client.request_arkoor_cosign(cosign_request).await
113 .context("server failed to cosign arkoor")?
114 .into_inner();
115
116 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
117 .context("Failed to parse cosign response from server")?;
118
119 let vtxos = builder
120 .user_cosign(&user_keypairs, cosign_responses)
121 .context("Failed to cosign vtxos")?
122 .build_signed_vtxos();
123
124 let (dest, change) = vtxos.into_iter()
126 .partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
127
128 Ok(ArkoorCreateResult {
129 inputs: input_ids,
130 created: dest,
131 change: change,
132 })
133 }
134
135 pub async fn send_arkoor_payment(
145 &self,
146 destination: &ark::Address,
147 amount: Amount,
148 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
149 let (mut srv, _) = self.require_server().await?;
150
151 self.validate_arkoor_address(&destination).await
152 .context("address validation failed")?;
153
154 let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
155
156 let change_pubkey = self.derive_store_next_keypair().await
157 .context("Failed to create change keypair")?.0;
158
159 let dest = ArkoorDestination { total_amount: amount, policy: destination.policy().clone() };
160 let arkoor = self.create_checkpointed_arkoor(
161 dest.clone(),
162 change_pubkey.public_key(),
163 ).await.context("failed to create arkoor transaction")?;
164
165 let mut movement = self.movements.new_guarded_movement_with_update(
166 Subsystem::ARKOOR,
167 ArkoorMovement::Send.to_string(),
168 OnDropStatus::Failed,
169 MovementUpdate::new()
170 .intended_and_effective_balance(negative_amount)
171 .consumed_vtxos(&arkoor.inputs)
172 .sent_to([MovementDestination::ark(destination.clone(), amount)])
173 ).await?;
174
175 let mut delivered = false;
176 for delivery in destination.delivery() {
177 match delivery {
178 VtxoDelivery::ServerMailbox { blinded_id } => {
179 let req = protos::mailbox_server::PostVtxosMailboxRequest {
180 blinded_id: blinded_id.to_vec(),
181 vtxos: arkoor.created.iter().map(|v| v.serialize().to_vec()).collect(),
182 };
183
184 if let Err(e) = srv.mailbox_client.post_vtxos_mailbox(req).await {
185 error!("Failed to post the vtxos to the destination's mailbox: '{:#}'", e);
186 } else {
188 delivered = true;
189 }
190 },
191 VtxoDelivery::Unknown { delivery_type, data } => {
192 error!("Unknown delivery type {} for arkoor payment: {}", delivery_type, data.as_hex());
193 },
194 _ => {
195 error!("Unsupported delivery type for arkoor payment: {:?}", delivery);
196 }
197 }
198 }
199 self.mark_vtxos_as_spent(&arkoor.inputs).await?;
200 if !arkoor.change.is_empty() {
201 self.store_spendable_vtxos(&arkoor.change).await?;
202 movement.apply_update(MovementUpdate::new().produced_vtxos(arkoor.change)).await?;
203 }
204
205 if delivered {
206 movement.success().await?;
207 } else {
208 bail!("Failed to deliver arkoor vtxos to any destination");
209 }
210
211 Ok(arkoor.created)
212 }
213}