1use anyhow::Context;
2use bitcoin::{Amount, NetworkKind};
3use bitcoin::hex::DisplayHex;
4use log::{info, warn, error};
5
6use ark::{VtxoPolicy, ProtocolEncoding};
7use ark::arkoor::ArkoorDestination;
8use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
9use ark::vtxo::{Full, Vtxo, VtxoId};
10use server_rpc::protos;
11
12use crate::subsystem::Subsystem;
13use crate::{ArkoorMovement, VtxoDelivery, MovementUpdate, Wallet, WalletVtxo};
14use crate::movement::MovementDestination;
15use crate::movement::manager::OnDropStatus;
16
17pub struct ArkoorCreateResult {
19 pub inputs: Vec<VtxoId>,
20 pub created: Vec<Vtxo<Full>>,
21 pub change: Vec<Vtxo<Full>>,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
25pub enum ArkoorAddressError {
26 #[error("Ark address is for different network")]
27 NetworkMismatch,
28 #[error("Ark address is for different server")]
29 ServerMismatch,
30 #[error("VTXO policy in address cannot be used for arkoor payment: {0:?}")]
31 PolicyNotSupported(VtxoPolicy),
32 #[error("No VTXO delivery mechanism provided in address")]
33 NoDeliveryMechanism,
34 #[error("Unknown delivery mechanism: {0}")]
35 UnknownDeliveryMechanism(String),
36 #[error("Other error: {0}")]
37 Other(String),
38}
39
40impl Wallet {
41 pub async fn validate_arkoor_address(&self, address: &ark::Address) -> Result<(), ArkoorAddressError> {
45 let network = self.network().await
46 .map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
47 let (_, ark_info) = self.require_server().await
48 .map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
49
50 let network_kind = NetworkKind::from(network);
51 if address.is_testnet() == network_kind.is_mainnet() {
52 return Err(ArkoorAddressError::NetworkMismatch);
53 }
54
55 if !address.ark_id().is_for_server(ark_info.server_pubkey) {
56 return Err(ArkoorAddressError::ServerMismatch);
57 }
58
59 match address.policy() {
61 VtxoPolicy::Pubkey(_) => {},
62 VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) => {
63 return Err(ArkoorAddressError::PolicyNotSupported(address.policy().clone()));
64 }
65 }
66
67 if address.delivery().is_empty() {
68 return Err(ArkoorAddressError::NoDeliveryMechanism);
69 }
70 if !address.delivery().iter().any(|d| !d.is_unknown()) {
74 for d in address.delivery() {
75 if let VtxoDelivery::Unknown { delivery_type, data } = d {
76 info!("Unknown delivery in address: type={:#x}, data={}",
77 delivery_type, data.as_hex(),
78 );
79 }
80 }
81 }
82
83 Ok(())
84 }
85
86 pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
87 &self,
88 arkoor_dest: ArkoorDestination,
89 inputs: impl IntoIterator<Item = WalletVtxo>,
90 ) -> anyhow::Result<ArkoorCreateResult> {
91 let (mut srv, _) = self.require_server().await?;
93 let input_ids = inputs.into_iter().map(|v| v.id()).collect::<Vec<_>>();
94
95 let inputs = self.inner.db.get_full_vtxos(&input_ids).await
99 .context("failed to hydrate arkoor input vtxos")?;
100
101 self.register_vtxo_transactions_with_server(&inputs).await
108 .context("failed to register arkoor input vtxo transactions with server")?;
109
110 let (change_keypair, change_key_index) = self.peek_next_keypair().await?;
113 let change_pubkey = change_keypair.public_key();
114
115 if arkoor_dest.policy.user_pubkey() == change_pubkey {
116 bail!("Cannot create arkoor to same address as change");
117 }
118
119 let mut user_keypairs = vec![];
120 for vtxo in &inputs {
121 user_keypairs.push(self.get_vtxo_key(vtxo).await?);
122 }
123
124 let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
125 inputs.into_iter(),
126 arkoor_dest.clone(),
127 VtxoPolicy::new_pubkey(change_pubkey),
128 )
129 .context("Failed to construct arkoor package")?
130 .generate_user_nonces(&user_keypairs)
131 .context("invalid nb of keypairs")?;
132
133 let cosign_request = protos::ArkoorPackageCosignRequest::from(
134 builder.cosign_request(),
135 );
136
137 let response = srv.client.request_arkoor_cosign(cosign_request).await
138 .context("server failed to cosign arkoor")?
139 .into_inner();
140
141 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
142 .context("Failed to parse cosign response from server")?;
143
144 let vtxos = builder
145 .user_cosign(&user_keypairs, cosign_responses)
146 .context("Failed to cosign vtxos")?
147 .build_signed_vtxos();
148
149 let (dest, change) = vtxos.into_iter()
151 .partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
152
153 if !change.is_empty() {
154 self.inner.db.store_vtxo_key(change_key_index, change_pubkey).await?;
156 }
157
158 Ok(ArkoorCreateResult {
159 inputs: input_ids,
160 created: dest,
161 change,
162 })
163 }
164
165 pub async fn send_arkoor_payment(
175 &self,
176 destination: &ark::Address,
177 amount: Amount,
178 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
179 let (mut srv, _) = self.require_server().await?;
180
181 self.validate_arkoor_address(&destination).await
182 .context("address validation failed")?;
183
184 let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
185
186 let dest = ArkoorDestination { total_amount: amount, policy: destination.policy().clone() };
187 let inputs = self.select_vtxos_to_cover(dest.total_amount).await?;
188 let arkoor = self.create_checkpointed_arkoor_with_vtxos(dest.clone(), inputs.into_iter())
189 .await.context("failed to create arkoor transaction")?;
190
191 let to_register = arkoor.created.iter()
200 .chain(arkoor.change.iter())
201 .collect::<Vec<_>>();
202 if let Err(e) = self.register_vtxo_transactions_with_server(&to_register).await {
203 warn!("Failed to register arkoor output vtxo transactions with server: {:#}", e);
204 }
205
206 let mut movement = self.inner.movements.new_guarded_movement_with_update(
207 Subsystem::ARKOOR,
208 ArkoorMovement::Send.to_string(),
209 OnDropStatus::Failed,
210 MovementUpdate::new()
211 .intended_and_effective_balance(negative_amount)
212 .consumed_vtxos(&arkoor.inputs)
213 .sent_to([MovementDestination::ark(destination.clone(), amount)])
214 ).await?;
215
216 let mut delivered = false;
217 for delivery in destination.delivery() {
218 match delivery {
219 VtxoDelivery::ServerMailbox { blinded_id } => {
220 let req = protos::mailbox_server::PostArkoorMessageRequest {
221 blinded_id: blinded_id.to_vec(),
222 vtxos: arkoor.created.iter().map(|v| v.serialize().to_vec()).collect(),
223 };
224
225 if let Err(e) = srv.mailbox_client.post_arkoor_message(req).await {
226 error!("Failed to post the vtxos to the destination's mailbox: '{:#}'", e);
227 } else {
229 delivered = true;
230 }
231 },
232 VtxoDelivery::Unknown { delivery_type, data } => {
233 error!("Unknown delivery type {} for arkoor payment: {}", delivery_type, data.as_hex());
234 },
235 _ => {
236 error!("Unsupported delivery type for arkoor payment: {:?}", delivery);
237 }
238 }
239 }
240 self.mark_vtxos_as_spent(&arkoor.inputs).await?;
241 if !arkoor.change.is_empty() {
242 self.store_spendable_vtxos(&arkoor.change).await?;
243 movement.apply_update(MovementUpdate::new().produced_vtxos(arkoor.change)).await?;
244 }
245
246 if delivered {
247 movement.success().await?;
248 } else {
249 bail!("Failed to deliver arkoor vtxos to any destination");
250 }
251
252 Ok(arkoor.created)
253 }
254}