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("No VTXO delivery mechanism provided in address")]
51 NoDeliveryMechanism,
52 #[error("Unknown delivery mechanism: {0}")]
53 UnknownDeliveryMechanism(String),
54 #[error("Other error: {0}")]
55 Other(String),
56}
57
58pub(crate) fn split_change_amount(change: Amount, pay: Amount, split_factor: u8) -> Vec<Amount> {
68 if change == Amount::ZERO {
69 return Vec::new();
70 }
71 let pieces = if change > pay { u64::from(split_factor.max(1)) } else { 1 };
72 let base = change / pieces;
73 let mut ret = vec![base; pieces as usize];
74 *ret.last_mut().unwrap() = change - base * (pieces - 1);
75 ret
76}
77
78pub(crate) fn resolve_change_pieces(
82 stored: Option<Vec<Amount>>,
83 change: Amount,
84) -> anyhow::Result<Vec<Amount>> {
85 match stored {
86 Some(pieces) => {
87 let sum = pieces.iter().copied().sum::<Amount>();
88 ensure!(sum == change, "stored change pieces sum to {}, expected {}", sum, change);
89 Ok(pieces)
90 },
91 None if change == Amount::ZERO => Ok(Vec::new()),
92 None => Ok(vec![change]),
93 }
94}
95
96pub(crate) enum DeliveryOutcome {
98 AnySucceeded,
100 AllFailed { summary: String },
103}
104
105pub(crate) async fn post_arkoor_to_mailboxes(
112 srv: &mut ServerConnection,
113 delivery: &[VtxoDelivery],
114 vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<Full>>>,
115) -> DeliveryOutcome {
116 let serialized = vtxos.into_iter()
117 .map(|v| v.as_ref().serialize().to_vec())
118 .collect::<Vec<_>>();
119
120 let mut any_succeeded = false;
121 let mut failures: Vec<String> = Vec::new();
122 for method in delivery {
123 let VtxoDelivery::ServerMailbox { blinded_id } = method else { continue };
124 let req = protos::mailbox_server::PostArkoorMessageRequest {
125 blinded_id: blinded_id.as_ref().to_vec(),
126 vtxos: serialized.clone(),
127 };
128 match srv.mailbox_client.post_arkoor_message(req).await {
129 Ok(_) => any_succeeded = true,
130 Err(e) => {
131 let reason = format!("{:#}", e);
132 error!("failed to post arkoor vtxos to mailbox: {}", reason);
133 failures.push(reason);
134 },
135 }
136 }
137
138 if any_succeeded {
139 return DeliveryOutcome::AnySucceeded;
140 }
141 let summary = if failures.is_empty() {
142 "no mailbox delivery mechanism configured on destination".to_string()
143 } else {
144 format!("no delivery mechanism accepted the arkoor vtxos: {}", failures.join("; "))
145 };
146 DeliveryOutcome::AllFailed { summary }
147}
148
149impl Wallet {
150 pub async fn validate_arkoor_address(&self, address: &ark::Address) -> Result<(), ArkoorAddressError> {
154 let network = self.network().await
155 .map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
156 let (_, ark_info) = self.require_server().await
157 .map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
158
159 let network_kind = NetworkKind::from(network);
160 if address.is_testnet() == network_kind.is_mainnet() {
161 return Err(ArkoorAddressError::NetworkMismatch);
162 }
163
164 if !address.ark_id().is_for_server(ark_info.server_pubkey) {
165 return Err(ArkoorAddressError::ServerMismatch);
166 }
167
168 match address.policy() {
170 VtxoPolicy::Pubkey(_) => {},
171 VtxoPolicy::ServerHtlcRecv_v0(_) | VtxoPolicy::ServerHtlcSend_v0(_)
172 | VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) =>
173 {
174 return Err(ArkoorAddressError::PolicyNotSupported(address.policy().clone()));
175 }
176 }
177
178 if address.delivery().is_empty() {
179 return Err(ArkoorAddressError::NoDeliveryMechanism);
180 }
181 if !address.delivery().iter().any(|d| !d.is_unknown()) {
185 for d in address.delivery() {
186 if let VtxoDelivery::Unknown { delivery_type, data } = d {
187 info!("Unknown delivery in address: type={:#x}, data={}",
188 delivery_type, data.as_hex(),
189 );
190 }
191 }
192 }
193
194 Ok(())
195 }
196
197 pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
205 &self,
206 arkoor_dest: ArkoorDestination,
207 inputs: impl IntoIterator<Item = WalletVtxo>,
208 change_keypair: Keypair,
209 change_pieces: Option<Vec<Amount>>,
210 ) -> Result<ArkoorCreateResult, ArkoorCreateError> {
211 let (mut srv, _) = self.require_server().await?;
212 let input_ids = inputs.into_iter().map(|v| v.id()).collect::<Vec<_>>();
213
214 let inputs = self.inner.db.get_full_vtxos(&input_ids).await
218 .context("failed to hydrate arkoor input vtxos")?;
219
220 self.register_vtxo_transactions_with_server(&inputs).await
227 .context("failed to register arkoor input vtxo transactions with server")?;
228
229 let change_pubkey = change_keypair.public_key();
230 if arkoor_dest.policy.user_pubkey() == change_pubkey {
231 return Err(anyhow!("Cannot create arkoor to same address as change").into());
232 }
233
234 let mut user_keypairs = vec![];
235 for vtxo in &inputs {
236 user_keypairs.push(self.get_vtxo_key(vtxo).await?);
237 }
238
239 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
240 let change_amount = total_input.checked_sub(arkoor_dest.total_amount)
241 .ok_or_else(|| anyhow!("arkoor inputs ({}) don't cover destination ({})",
242 total_input, arkoor_dest.total_amount,
243 ))?;
244
245 let change_policy = VtxoPolicy::new_pubkey(change_pubkey);
246 let mut outputs = vec![arkoor_dest.clone()];
247 for piece in resolve_change_pieces(change_pieces, change_amount)? {
248 outputs.push(ArkoorDestination {
249 total_amount: piece,
250 policy: change_policy.clone(),
251 });
252 }
253
254 let builder = ArkoorPackageBuilder::new_with_checkpoints(inputs, outputs)
255 .context("Failed to construct arkoor package")?
256 .generate_user_nonces(&user_keypairs)
257 .context("invalid nb of keypairs")?;
258
259 let cosign_request = protos::ArkoorPackageCosignRequest::from(
260 builder.cosign_request(),
261 );
262
263 let response = srv.client.request_arkoor_cosign(cosign_request).await
264 .map_err(ArkoorCreateError::Cosign)?
265 .into_inner();
266
267 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
268 .context("Failed to parse cosign response from server")?;
269
270 let vtxos = builder
271 .user_cosign(&user_keypairs, cosign_responses)
272 .context("Failed to cosign vtxos")?
273 .build_signed_vtxos();
274
275 let (dest, change) = vtxos.into_iter()
277 .partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
278
279 Ok(ArkoorCreateResult {
280 inputs: input_ids,
281 created: dest,
282 change,
283 })
284 }
285
286 pub async fn send_arkoor_payment(
296 &self,
297 destination: &ark::Address,
298 amount: Amount,
299 ) -> anyhow::Result<()> {
300 let action = start_arkoor_send(self, destination.clone(), amount).await?;
301
302 self.inner.db.upsert_wallet_action_checkpoint(&action.id, &action.clone().into()).await?;
307
308 self.drive_action(action, DriveMode::UntilDone).await
309 }
310
311 pub async fn pending_arkoor_sends(&self) -> anyhow::Result<Vec<ArkoorSend>> {
313 Ok(self.inner.db.get_all_wallet_action_checkpoints().await?
314 .into_iter()
315 .filter_map(|cp| cp.into_arkoor_send())
316 .collect())
317 }
318
319 pub async fn sync_pending_arkoor_sends(&self) -> anyhow::Result<()> {
322 let pending = self.pending_arkoor_sends().await?;
323 if pending.is_empty() {
324 return Ok(());
325 }
326 info!("Syncing {} pending arkoor sends", pending.len());
327 for send in pending {
328 let id = send.id.clone();
329 if let Err(e) = self.drive_action(send, DriveMode::UntilParkOrDone).await {
330 warn!("Failed to sync arkoor send {}: {:#}", id, e);
331 }
332 }
333 Ok(())
334 }
335}
336
337#[cfg(test)]
338mod test {
339 use super::*;
340
341 #[test]
342 fn resolve_change_pieces_fallback_and_validation() {
343 let change = Amount::from_sat(30_000);
344 let pieces = vec![Amount::from_sat(10_000), Amount::from_sat(20_000)];
345
346 assert_eq!(resolve_change_pieces(Some(pieces.clone()), change).unwrap(), pieces);
348
349 assert!(resolve_change_pieces(Some(pieces), Amount::from_sat(30_001)).is_err());
351
352 assert_eq!(resolve_change_pieces(None, change).unwrap(), vec![change]);
354 assert_eq!(resolve_change_pieces(None, Amount::ZERO).unwrap(), Vec::<Amount>::new());
355 }
356
357 #[test]
358 fn split_change_amount_pieces() {
359 let pay = Amount::from_sat(10_000);
360
361 for factor in 1..=3u8 {
362 assert_eq!(split_change_amount(Amount::ZERO, pay, factor), Vec::<Amount>::new());
364
365 for sats in [1, 5_000, 10_000] {
367 let change = Amount::from_sat(sats);
368 assert_eq!(split_change_amount(change, pay, factor), vec![change]);
369 }
370
371 for sats in [10_001, 123_457, 100_000_000] {
373 let change = Amount::from_sat(sats);
374 let pieces = split_change_amount(change, pay, factor);
375 assert_eq!(pieces.len(), factor as usize);
376 assert_eq!(pieces.iter().copied().sum::<Amount>(), change);
377 }
378 }
379 }
380}