1use anyhow::Context;
2use bitcoin::{Amount, NetworkKind};
3use bitcoin::hex::DisplayHex;
4use log::{info, 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(
87 &self,
88 arkoor_dest: ArkoorDestination,
89 ) -> anyhow::Result<ArkoorCreateResult> {
90 let inputs = self.select_vtxos_to_cover(arkoor_dest.total_amount).await?;
91 self.create_checkpointed_arkoor_with_vtxos(
92 arkoor_dest,
93 inputs.into_iter(),
94 ).await
95 }
96
97 pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
98 &self,
99 arkoor_dest: ArkoorDestination,
100 inputs: impl IntoIterator<Item = WalletVtxo>,
101 ) -> anyhow::Result<ArkoorCreateResult> {
102 let (mut srv, _) = self.require_server().await?;
104 let (input_ids, inputs) = inputs.into_iter()
105 .map(|v| (v.id(), v))
106 .collect::<(Vec<_>, Vec<_>)>();
107
108 let (change_keypair, change_key_index) = self.peek_next_keypair().await?;
111 let change_pubkey = change_keypair.public_key();
112
113 if arkoor_dest.policy.user_pubkey() == change_pubkey {
114 bail!("Cannot create arkoor to same address as change");
115 }
116
117 let mut user_keypairs = vec![];
118 for vtxo in &inputs {
119 user_keypairs.push(self.get_vtxo_key(vtxo).await?);
120 }
121
122 let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
123 inputs.into_iter().map(|v| v.vtxo),
124 arkoor_dest.clone(),
125 VtxoPolicy::new_pubkey(change_pubkey),
126 )
127 .context("Failed to construct arkoor package")?
128 .generate_user_nonces(&user_keypairs)
129 .context("invalid nb of keypairs")?;
130
131 let cosign_request = protos::ArkoorPackageCosignRequest::from(
132 builder.cosign_request(),
133 );
134
135 let response = srv.client.request_arkoor_cosign(cosign_request).await
136 .context("server failed to cosign arkoor")?
137 .into_inner();
138
139 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
140 .context("Failed to parse cosign response from server")?;
141
142 let vtxos = builder
143 .user_cosign(&user_keypairs, cosign_responses)
144 .context("Failed to cosign vtxos")?
145 .build_signed_vtxos();
146
147 let (dest, change) = vtxos.into_iter()
149 .partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
150
151 if !change.is_empty() {
152 self.db.store_vtxo_key(change_key_index, change_pubkey).await?;
154 }
155
156 Ok(ArkoorCreateResult {
157 inputs: input_ids,
158 created: dest,
159 change,
160 })
161 }
162
163 pub async fn send_arkoor_payment(
173 &self,
174 destination: &ark::Address,
175 amount: Amount,
176 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
177 let (mut srv, _) = self.require_server().await?;
178
179 self.validate_arkoor_address(&destination).await
180 .context("address validation failed")?;
181
182 let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
183
184 let dest = ArkoorDestination { total_amount: amount, policy: destination.policy().clone() };
185 let arkoor = self.create_checkpointed_arkoor(dest.clone())
186 .await.context("failed to create arkoor transaction")?;
187
188 let mut movement = self.movements.new_guarded_movement_with_update(
189 Subsystem::ARKOOR,
190 ArkoorMovement::Send.to_string(),
191 OnDropStatus::Failed,
192 MovementUpdate::new()
193 .intended_and_effective_balance(negative_amount)
194 .consumed_vtxos(&arkoor.inputs)
195 .sent_to([MovementDestination::ark(destination.clone(), amount)])
196 ).await?;
197
198 let mut delivered = false;
199 for delivery in destination.delivery() {
200 match delivery {
201 VtxoDelivery::ServerMailbox { blinded_id } => {
202 let req = protos::mailbox_server::PostArkoorMessageRequest {
203 blinded_id: blinded_id.to_vec(),
204 vtxos: arkoor.created.iter().map(|v| v.serialize().to_vec()).collect(),
205 };
206
207 if let Err(e) = srv.mailbox_client.post_arkoor_message(req).await {
208 error!("Failed to post the vtxos to the destination's mailbox: '{:#}'", e);
209 } else {
211 delivered = true;
212 }
213 },
214 VtxoDelivery::Unknown { delivery_type, data } => {
215 error!("Unknown delivery type {} for arkoor payment: {}", delivery_type, data.as_hex());
216 },
217 _ => {
218 error!("Unsupported delivery type for arkoor payment: {:?}", delivery);
219 }
220 }
221 }
222 self.mark_vtxos_as_spent(&arkoor.inputs).await?;
223 if !arkoor.change.is_empty() {
224 self.store_spendable_vtxos(&arkoor.change).await?;
225 movement.apply_update(MovementUpdate::new().produced_vtxos(arkoor.change)).await?;
226 }
227
228 if delivered {
229 movement.success().await?;
230 } else {
231 bail!("Failed to deliver arkoor vtxos to any destination");
232 }
233
234 Ok(arkoor.created)
235 }
236}