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) enum DeliveryOutcome {
60 AnySucceeded,
62 AllFailed { summary: String },
65}
66
67pub(crate) async fn post_arkoor_to_mailboxes(
74 srv: &mut ServerConnection,
75 delivery: &[VtxoDelivery],
76 vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<Full>>>,
77) -> DeliveryOutcome {
78 let serialized = vtxos.into_iter()
79 .map(|v| v.as_ref().serialize().to_vec())
80 .collect::<Vec<_>>();
81
82 let mut any_succeeded = false;
83 let mut failures: Vec<String> = Vec::new();
84 for method in delivery {
85 let VtxoDelivery::ServerMailbox { blinded_id } = method else { continue };
86 let req = protos::mailbox_server::PostArkoorMessageRequest {
87 blinded_id: blinded_id.as_ref().to_vec(),
88 vtxos: serialized.clone(),
89 };
90 match srv.mailbox_client.post_arkoor_message(req).await {
91 Ok(_) => any_succeeded = true,
92 Err(e) => {
93 let reason = format!("{:#}", e);
94 error!("failed to post arkoor vtxos to mailbox: {}", reason);
95 failures.push(reason);
96 },
97 }
98 }
99
100 if any_succeeded {
101 return DeliveryOutcome::AnySucceeded;
102 }
103 let summary = if failures.is_empty() {
104 "no mailbox delivery mechanism configured on destination".to_string()
105 } else {
106 format!("no delivery mechanism accepted the arkoor vtxos: {}", failures.join("; "))
107 };
108 DeliveryOutcome::AllFailed { summary }
109}
110
111impl Wallet {
112 pub async fn validate_arkoor_address(&self, address: &ark::Address) -> Result<(), ArkoorAddressError> {
116 let network = self.network().await
117 .map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
118 let (_, ark_info) = self.require_server().await
119 .map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
120
121 let network_kind = NetworkKind::from(network);
122 if address.is_testnet() == network_kind.is_mainnet() {
123 return Err(ArkoorAddressError::NetworkMismatch);
124 }
125
126 if !address.ark_id().is_for_server(ark_info.server_pubkey) {
127 return Err(ArkoorAddressError::ServerMismatch);
128 }
129
130 match address.policy() {
132 VtxoPolicy::Pubkey(_) => {},
133 VtxoPolicy::ServerHtlcRecv_v0(_) | VtxoPolicy::ServerHtlcSend_v0(_)
134 | VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) =>
135 {
136 return Err(ArkoorAddressError::PolicyNotSupported(address.policy().clone()));
137 }
138 }
139
140 if address.delivery().is_empty() {
141 return Err(ArkoorAddressError::NoDeliveryMechanism);
142 }
143 if !address.delivery().iter().any(|d| !d.is_unknown()) {
147 for d in address.delivery() {
148 if let VtxoDelivery::Unknown { delivery_type, data } = d {
149 info!("Unknown delivery in address: type={:#x}, data={}",
150 delivery_type, data.as_hex(),
151 );
152 }
153 }
154 }
155
156 Ok(())
157 }
158
159 pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
167 &self,
168 arkoor_dest: ArkoorDestination,
169 inputs: impl IntoIterator<Item = WalletVtxo>,
170 change_keypair: Keypair,
171 ) -> Result<ArkoorCreateResult, ArkoorCreateError> {
172 let (mut srv, _) = self.require_server().await?;
173 let input_ids = inputs.into_iter().map(|v| v.id()).collect::<Vec<_>>();
174
175 let inputs = self.inner.db.get_full_vtxos(&input_ids).await
179 .context("failed to hydrate arkoor input vtxos")?;
180
181 self.register_vtxo_transactions_with_server(&inputs).await
188 .context("failed to register arkoor input vtxo transactions with server")?;
189
190 let change_pubkey = change_keypair.public_key();
191 if arkoor_dest.policy.user_pubkey() == change_pubkey {
192 return Err(anyhow!("Cannot create arkoor to same address as change").into());
193 }
194
195 let mut user_keypairs = vec![];
196 for vtxo in &inputs {
197 user_keypairs.push(self.get_vtxo_key(vtxo).await?);
198 }
199
200 let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
201 inputs.into_iter(),
202 arkoor_dest.clone(),
203 VtxoPolicy::new_pubkey(change_pubkey),
204 )
205 .context("Failed to construct arkoor package")?
206 .generate_user_nonces(&user_keypairs)
207 .context("invalid nb of keypairs")?;
208
209 let cosign_request = protos::ArkoorPackageCosignRequest::from(
210 builder.cosign_request(),
211 );
212
213 let response = srv.client.request_arkoor_cosign(cosign_request).await
214 .map_err(ArkoorCreateError::Cosign)?
215 .into_inner();
216
217 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
218 .context("Failed to parse cosign response from server")?;
219
220 let vtxos = builder
221 .user_cosign(&user_keypairs, cosign_responses)
222 .context("Failed to cosign vtxos")?
223 .build_signed_vtxos();
224
225 let (dest, change) = vtxos.into_iter()
227 .partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
228
229 Ok(ArkoorCreateResult {
230 inputs: input_ids,
231 created: dest,
232 change,
233 })
234 }
235
236 pub async fn send_arkoor_payment(
246 &self,
247 destination: &ark::Address,
248 amount: Amount,
249 ) -> anyhow::Result<()> {
250 let action = start_arkoor_send(self, destination.clone(), amount).await?;
251
252 self.inner.db.upsert_wallet_action_checkpoint(&action.id, &action.clone().into()).await?;
257
258 self.drive_action(action, DriveMode::UntilDone).await
259 }
260
261 pub async fn pending_arkoor_sends(&self) -> anyhow::Result<Vec<ArkoorSend>> {
263 Ok(self.inner.db.get_all_wallet_action_checkpoints().await?
264 .into_iter()
265 .filter_map(|cp| cp.into_arkoor_send())
266 .collect())
267 }
268
269 pub async fn sync_pending_arkoor_sends(&self) -> anyhow::Result<()> {
272 let pending = self.pending_arkoor_sends().await?;
273 if pending.is_empty() {
274 return Ok(());
275 }
276 info!("Syncing {} pending arkoor sends", pending.len());
277 for send in pending {
278 let id = send.id.clone();
279 if let Err(e) = self.drive_action(send, DriveMode::UntilParkOrDone).await {
280 warn!("Failed to sync arkoor send {}: {:#}", id, e);
281 }
282 }
283 Ok(())
284 }
285}