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) = inputs.into_iter()
94 .map(|v| (v.id(), v))
95 .collect::<(Vec<_>, Vec<_>)>();
96
97 self.register_vtxo_transactions_with_server(&inputs).await
104 .context("failed to register arkoor input vtxo transactions with server")?;
105
106 let (change_keypair, change_key_index) = self.peek_next_keypair().await?;
109 let change_pubkey = change_keypair.public_key();
110
111 if arkoor_dest.policy.user_pubkey() == change_pubkey {
112 bail!("Cannot create arkoor to same address as change");
113 }
114
115 let mut user_keypairs = vec![];
116 for vtxo in &inputs {
117 user_keypairs.push(self.get_vtxo_key(vtxo).await?);
118 }
119
120 let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
121 inputs.into_iter().map(|v| v.vtxo),
122 arkoor_dest.clone(),
123 VtxoPolicy::new_pubkey(change_pubkey),
124 )
125 .context("Failed to construct arkoor package")?
126 .generate_user_nonces(&user_keypairs)
127 .context("invalid nb of keypairs")?;
128
129 let cosign_request = protos::ArkoorPackageCosignRequest::from(
130 builder.cosign_request(),
131 );
132
133 let response = srv.client.request_arkoor_cosign(cosign_request).await
134 .context("server failed to cosign arkoor")?
135 .into_inner();
136
137 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
138 .context("Failed to parse cosign response from server")?;
139
140 let vtxos = builder
141 .user_cosign(&user_keypairs, cosign_responses)
142 .context("Failed to cosign vtxos")?
143 .build_signed_vtxos();
144
145 let (dest, change) = vtxos.into_iter()
147 .partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
148
149 if !change.is_empty() {
150 self.db.store_vtxo_key(change_key_index, change_pubkey).await?;
152 }
153
154 Ok(ArkoorCreateResult {
155 inputs: input_ids,
156 created: dest,
157 change,
158 })
159 }
160
161 pub async fn send_arkoor_payment(
171 &self,
172 destination: &ark::Address,
173 amount: Amount,
174 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
175 let (mut srv, _) = self.require_server().await?;
176
177 self.validate_arkoor_address(&destination).await
178 .context("address validation failed")?;
179
180 let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
181
182 let dest = ArkoorDestination { total_amount: amount, policy: destination.policy().clone() };
183 let inputs = self.select_vtxos_to_cover(dest.total_amount).await?;
184 let arkoor = self.create_checkpointed_arkoor_with_vtxos(dest.clone(), inputs.into_iter())
185 .await.context("failed to create arkoor transaction")?;
186
187 let to_register = arkoor.created.iter()
196 .chain(arkoor.change.iter())
197 .collect::<Vec<_>>();
198 if let Err(e) = self.register_vtxo_transactions_with_server(&to_register).await {
199 warn!("Failed to register arkoor output vtxo transactions with server: {:#}", e);
200 }
201
202 let mut movement = self.movements.new_guarded_movement_with_update(
203 Subsystem::ARKOOR,
204 ArkoorMovement::Send.to_string(),
205 OnDropStatus::Failed,
206 MovementUpdate::new()
207 .intended_and_effective_balance(negative_amount)
208 .consumed_vtxos(&arkoor.inputs)
209 .sent_to([MovementDestination::ark(destination.clone(), amount)])
210 ).await?;
211
212 let mut delivered = false;
213 for delivery in destination.delivery() {
214 match delivery {
215 VtxoDelivery::ServerMailbox { blinded_id } => {
216 let req = protos::mailbox_server::PostArkoorMessageRequest {
217 blinded_id: blinded_id.to_vec(),
218 vtxos: arkoor.created.iter().map(|v| v.serialize().to_vec()).collect(),
219 };
220
221 if let Err(e) = srv.mailbox_client.post_arkoor_message(req).await {
222 error!("Failed to post the vtxos to the destination's mailbox: '{:#}'", e);
223 } else {
225 delivered = true;
226 }
227 },
228 VtxoDelivery::Unknown { delivery_type, data } => {
229 error!("Unknown delivery type {} for arkoor payment: {}", delivery_type, data.as_hex());
230 },
231 _ => {
232 error!("Unsupported delivery type for arkoor payment: {:?}", delivery);
233 }
234 }
235 }
236 self.mark_vtxos_as_spent(&arkoor.inputs).await?;
237 if !arkoor.change.is_empty() {
238 self.store_spendable_vtxos(&arkoor.change).await?;
239 movement.apply_update(MovementUpdate::new().produced_vtxos(arkoor.change)).await?;
240 }
241
242 if delivered {
243 movement.success().await?;
244 } else {
245 bail!("Failed to deliver arkoor vtxos to any destination");
246 }
247
248 Ok(arkoor.created)
249 }
250}