1use anyhow::Context;
2use bitcoin::{Amount, NetworkKind};
3use bitcoin::hex::DisplayHex;
4use bitcoin::secp256k1::Keypair;
5use log::{error, info, warn};
6
7use ark::{ProtocolEncoding, VtxoPolicy};
8use ark::arkoor::ArkoorDestination;
9use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
10use ark::vtxo::{Full, Vtxo, VtxoId};
11use server_rpc::{protos, ServerConnection};
12
13use crate::{VtxoDelivery, Wallet, WalletVtxo};
14use crate::actions::DriveMode;
15use crate::actions::arkoor_send::{ArkoorSend, start_arkoor_send};
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(Debug, thiserror::Error)]
32pub enum ArkoorCreateError {
33 #[error("server failed to cosign arkoor: {0}")]
37 Cosign(#[source] tonic::Status),
38 #[error(transparent)]
39 Other(#[from] anyhow::Error),
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
43pub enum ArkoorAddressError {
44 #[error("Ark address is for different network")]
45 NetworkMismatch,
46 #[error("Ark address is for different server")]
47 ServerMismatch,
48 #[error("VTXO policy in address cannot be used for arkoor payment: {0:?}")]
49 PolicyNotSupported(VtxoPolicy),
50 #[error("Unknown delivery mechanism: {0}")]
51 UnknownDeliveryMechanism(String),
52 #[error("Other error: {0}")]
53 Other(String),
54}
55
56pub(crate) fn split_change_amount(change: Amount, pay: Amount, split_factor: u8) -> Vec<Amount> {
66 if change == Amount::ZERO {
67 return Vec::new();
68 }
69 let pieces = if change > pay { u64::from(split_factor.max(1)) } else { 1 };
70 let base = change / pieces;
71 let mut ret = vec![base; pieces as usize];
72 *ret.last_mut().unwrap() = change - base * (pieces - 1);
73 ret
74}
75
76pub(crate) fn resolve_change_pieces(
80 stored: Option<Vec<Amount>>,
81 change: Amount,
82) -> anyhow::Result<Vec<Amount>> {
83 match stored {
84 Some(pieces) => {
85 let sum = pieces.iter().copied().sum::<Amount>();
86 ensure!(sum == change, "stored change pieces sum to {}, expected {}", sum, change);
87 Ok(pieces)
88 },
89 None if change == Amount::ZERO => Ok(Vec::new()),
90 None => Ok(vec![change]),
91 }
92}
93
94pub(crate) enum DeliveryOutcome {
96 AnySucceeded,
98 AllFailed { summary: String },
101}
102
103pub(crate) async fn post_arkoor_to_mailboxes(
110 srv: &mut ServerConnection,
111 delivery: &[VtxoDelivery],
112 vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<Full>>>,
113) -> DeliveryOutcome {
114 let serialized = vtxos.into_iter()
115 .map(|v| v.as_ref().serialize().to_vec())
116 .collect::<Vec<_>>();
117
118 let mut any_succeeded = false;
119 let mut failures: Vec<String> = Vec::new();
120 for method in delivery {
121 let VtxoDelivery::ServerMailbox { blinded_id } = method else { continue };
122 let req = protos::mailbox_server::PostArkoorMessageRequest {
123 blinded_id: blinded_id.as_ref().to_vec(),
124 vtxos: serialized.clone(),
125 };
126 match srv.mailbox_client.post_arkoor_message(req).await {
127 Ok(_) => any_succeeded = true,
128 Err(e) => {
129 let reason = format!("{:#}", e);
130 error!("failed to post arkoor vtxos to mailbox: {}", reason);
131 failures.push(reason);
132 },
133 }
134 }
135
136 if any_succeeded {
137 return DeliveryOutcome::AnySucceeded;
138 }
139 let summary = if failures.is_empty() {
140 "no mailbox delivery mechanism configured on destination".to_string()
141 } else {
142 format!("no delivery mechanism accepted the arkoor vtxos: {}", failures.join("; "))
143 };
144 DeliveryOutcome::AllFailed { summary }
145}
146
147fn check_delivery(delivery: &[VtxoDelivery]) -> Result<(), ArkoorAddressError> {
155 if delivery.is_empty() {
157 return Ok(());
158 }
159
160 if delivery.iter().any(|d| matches!(d, VtxoDelivery::ServerMailbox { .. })) {
161 return Ok(());
162 }
163
164 let listed = delivery.iter()
165 .map(|d| match d {
166 VtxoDelivery::Unknown { delivery_type, data } => {
167 format!("type={:#x}, data={}", delivery_type, data.as_hex())
168 },
169 other => format!("{:?}", other),
170 })
171 .collect::<Vec<_>>()
172 .join("; ");
173 Err(ArkoorAddressError::UnknownDeliveryMechanism(listed))
174}
175
176impl Wallet {
177 pub async fn validate_arkoor_address(&self, address: &ark::Address) -> Result<(), ArkoorAddressError> {
181 let network = self.network().await
182 .map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
183 let (_, ark_info) = self.require_server().await
184 .map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
185
186 let network_kind = NetworkKind::from(network);
187 if address.is_testnet() == network_kind.is_mainnet() {
188 return Err(ArkoorAddressError::NetworkMismatch);
189 }
190
191 if !address.ark_id().is_for_server(ark_info.server_pubkey) {
192 return Err(ArkoorAddressError::ServerMismatch);
193 }
194
195 match address.policy() {
197 VtxoPolicy::Pubkey(_) => {},
198 VtxoPolicy::ServerHtlcRecv_v0(_) | VtxoPolicy::ServerHtlcSend_v0(_)
199 | VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) =>
200 {
201 return Err(ArkoorAddressError::PolicyNotSupported(address.policy().clone()));
202 }
203 }
204
205 check_delivery(address.delivery())?;
206
207 Ok(())
208 }
209
210 pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
218 &self,
219 arkoor_dest: ArkoorDestination,
220 inputs: impl IntoIterator<Item = WalletVtxo>,
221 change_keypair: Keypair,
222 change_pieces: Option<Vec<Amount>>,
223 ) -> Result<ArkoorCreateResult, ArkoorCreateError> {
224 let (mut srv, _) = self.require_server().await?;
225 let input_ids = inputs.into_iter().map(|v| v.id()).collect::<Vec<_>>();
226
227 let inputs = self.inner.db.get_full_vtxos(&input_ids).await
231 .context("failed to hydrate arkoor input vtxos")?;
232
233 self.register_vtxo_transactions_with_server(&inputs).await
240 .context("failed to register arkoor input vtxo transactions with server")?;
241
242 let change_pubkey = change_keypair.public_key();
243 if arkoor_dest.policy.user_pubkey() == change_pubkey {
244 return Err(anyhow!("Cannot create arkoor to same address as change").into());
245 }
246
247 let mut user_keypairs = vec![];
248 for vtxo in &inputs {
249 user_keypairs.push(self.get_vtxo_key(vtxo).await?);
250 }
251
252 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
253 let change_amount = total_input.checked_sub(arkoor_dest.total_amount)
254 .ok_or_else(|| anyhow!("arkoor inputs ({}) don't cover destination ({})",
255 total_input, arkoor_dest.total_amount,
256 ))?;
257
258 let change_policy = VtxoPolicy::new_pubkey(change_pubkey);
259 let mut outputs = vec![arkoor_dest.clone()];
260 for piece in resolve_change_pieces(change_pieces, change_amount)? {
261 outputs.push(ArkoorDestination {
262 total_amount: piece,
263 policy: change_policy.clone(),
264 });
265 }
266
267 let builder = ArkoorPackageBuilder::new_with_checkpoints(inputs, outputs)
268 .context("Failed to construct arkoor package")?
269 .generate_user_nonces(&user_keypairs)
270 .context("invalid nb of keypairs")?;
271
272 let cosign_request = protos::ArkoorPackageCosignRequest::from(
273 builder.cosign_request(),
274 );
275
276 let response = srv.client.request_arkoor_cosign(cosign_request).await
277 .map_err(ArkoorCreateError::Cosign)?
278 .into_inner();
279
280 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
281 .context("Failed to parse cosign response from server")?;
282
283 let vtxos = builder
284 .user_cosign(&user_keypairs, cosign_responses)
285 .context("Failed to cosign vtxos")?
286 .build_signed_vtxos();
287
288 let (dest, change) = vtxos.into_iter()
290 .partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
291
292 Ok(ArkoorCreateResult {
293 inputs: input_ids,
294 created: dest,
295 change,
296 })
297 }
298
299 pub async fn send_arkoor_payment(
309 &self,
310 destination: &ark::Address,
311 amount: Amount,
312 ) -> anyhow::Result<()> {
313 let action = start_arkoor_send(self, destination.clone(), amount).await?;
314
315 self.inner.db.upsert_wallet_action_checkpoint(&action.id, &action.clone().into()).await?;
320
321 self.drive_action(action, DriveMode::UntilDone).await
322 }
323
324 pub async fn pending_arkoor_sends(&self) -> anyhow::Result<Vec<ArkoorSend>> {
326 Ok(self.inner.db.get_all_wallet_action_checkpoints().await?
327 .into_iter()
328 .filter_map(|cp| cp.into_arkoor_send())
329 .collect())
330 }
331
332 pub async fn sync_pending_arkoor_sends(&self) -> anyhow::Result<()> {
335 let pending = self.pending_arkoor_sends().await?;
336 if pending.is_empty() {
337 return Ok(());
338 }
339 info!("Syncing {} pending arkoor sends", pending.len());
340 for send in pending {
341 let id = send.id.clone();
342 if let Err(e) = self.drive_action(send, DriveMode::UntilParkOrDone).await {
343 warn!("Failed to sync arkoor send {}: {:#}", id, e);
344 }
345 }
346 Ok(())
347 }
348}
349
350#[cfg(test)]
351mod test {
352 use super::*;
353
354 #[test]
355 fn resolve_change_pieces_fallback_and_validation() {
356 let change = Amount::from_sat(30_000);
357 let pieces = vec![Amount::from_sat(10_000), Amount::from_sat(20_000)];
358
359 assert_eq!(resolve_change_pieces(Some(pieces.clone()), change).unwrap(), pieces);
361
362 assert!(resolve_change_pieces(Some(pieces), Amount::from_sat(30_001)).is_err());
364
365 assert_eq!(resolve_change_pieces(None, change).unwrap(), vec![change]);
367 assert_eq!(resolve_change_pieces(None, Amount::ZERO).unwrap(), Vec::<Amount>::new());
368 }
369
370 #[test]
371 fn check_delivery_requires_a_mailbox() {
372 use std::str::FromStr;
373
374 let mailbox = VtxoDelivery::ServerMailbox {
375 blinded_id: ark::mailbox::BlindedMailboxIdentifier::from_str(
376 "024b0d4a4e8a29d2f36a83b4ff4a0e5c5e6f0f8b8d1f2a3b4c5d6e7f80912a3b4c",
377 ).unwrap(),
378 };
379 let unknown = VtxoDelivery::Unknown { delivery_type: 0xff, data: vec![1, 2, 3] };
380
381 assert_eq!(check_delivery(&[]), Ok(()));
383
384 assert!(matches!(
386 check_delivery(&[unknown.clone()]),
387 Err(ArkoorAddressError::UnknownDeliveryMechanism(_)),
388 ));
389
390 assert_eq!(check_delivery(&[mailbox.clone()]), Ok(()));
392 assert_eq!(check_delivery(&[unknown, mailbox]), Ok(()));
393 }
394
395 #[test]
396 fn split_change_amount_pieces() {
397 let pay = Amount::from_sat(10_000);
398
399 for factor in 1..=3u8 {
400 assert_eq!(split_change_amount(Amount::ZERO, pay, factor), Vec::<Amount>::new());
402
403 for sats in [1, 5_000, 10_000] {
405 let change = Amount::from_sat(sats);
406 assert_eq!(split_change_amount(change, pay, factor), vec![change]);
407 }
408
409 for sats in [10_001, 123_457, 100_000_000] {
411 let change = Amount::from_sat(sats);
412 let pieces = split_change_amount(change, pay, factor);
413 assert_eq!(pieces.len(), factor as usize);
414 assert_eq!(pieces.iter().copied().sum::<Amount>(), change);
415 }
416 }
417 }
418}