1use anyhow::Context;
2use bitcoin::Amount;
3use bitcoin::hex::DisplayHex;
4use bitcoin::secp256k1::PublicKey;
5use log::{info, error};
6
7use ark::{VtxoPolicy, ProtocolEncoding};
8use ark::arkoor::ArkoorDestination;
9use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
10use ark::vtxo::{Vtxo, VtxoId};
11use server_rpc::protos;
12
13use crate::subsystem::Subsystem;
14use crate::{ArkoorMovement, VtxoDelivery, MovementUpdate, Wallet};
15use crate::movement::MovementDestination;
16use crate::movement::manager::OnDropStatus;
17
18pub struct ArkoorCreateResult {
20 pub inputs: Vec<VtxoId>,
21 pub created: Vec<Vtxo>,
22 pub change: Vec<Vtxo>,
23}
24
25impl Wallet {
26 pub async fn validate_arkoor_address(&self, address: &ark::Address) -> anyhow::Result<()> {
30 let srv = self.require_server()?;
31
32 if !address.ark_id().is_for_server(srv.ark_info().await?.server_pubkey) {
33 bail!("Ark address is for different server");
34 }
35
36 match address.policy() {
38 VtxoPolicy::Pubkey(_) => {},
39 VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) => {
40 bail!("VTXO policy in address cannot be used for arkoor payment: {}",
41 address.policy().policy_type(),
42 );
43 }
44 }
45
46 if address.delivery().is_empty() {
47 bail!("No VTXO delivery mechanism provided in address");
48 }
49 if !address.delivery().iter().any(|d| !d.is_unknown()) {
53 for d in address.delivery() {
54 if let VtxoDelivery::Unknown { delivery_type, data } = d {
55 info!("Unknown delivery in address: type={:#x}, data={}",
56 delivery_type, data.as_hex(),
57 );
58 }
59 }
60 }
61
62 Ok(())
63 }
64
65 pub(crate) async fn create_checkpointed_arkoor(
66 &self,
67 arkoor_dest: ArkoorDestination,
68 change_pubkey: PublicKey,
69 ) -> anyhow::Result<ArkoorCreateResult> {
70 if arkoor_dest.policy.user_pubkey() == change_pubkey {
71 bail!("Cannot create arkoor to same address as change");
72 }
73
74 let mut srv = self.require_server()?;
76 let inputs = self.select_vtxos_to_cover(arkoor_dest.total_amount).await?;
77 let input_ids = inputs.iter().map(|v| v.id()).collect();
78
79 let mut user_keypairs = vec![];
80 for vtxo in &inputs {
81 user_keypairs.push(self.get_vtxo_key(vtxo).await?);
82 }
83
84 let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
85 inputs.iter().map(|v| &v.vtxo).cloned(),
86 arkoor_dest.clone(),
87 VtxoPolicy::new_pubkey(change_pubkey),
88 )
89 .context("Failed to construct arkoor package")?
90 .generate_user_nonces(&user_keypairs)
91 .context("invalid nb of keypairs")?;
92
93 let cosign_request = protos::ArkoorPackageCosignRequest::from(
94 builder.cosign_request().convert_vtxo(|vtxo| vtxo.id()),
95 );
96
97 let response = srv.client.request_arkoor_cosign(cosign_request).await
98 .context("server failed to cosign arkoor")?
99 .into_inner();
100
101 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
102 .context("Failed to parse cosign response from server")?;
103
104 let vtxos = builder
105 .user_cosign(&user_keypairs, cosign_responses)
106 .context("Failed to cosign vtxos")?
107 .build_signed_vtxos();
108
109 let (dest, change) = vtxos.into_iter()
111 .partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
112
113 Ok(ArkoorCreateResult {
114 inputs: input_ids,
115 created: dest,
116 change: change,
117 })
118 }
119
120 pub async fn send_arkoor_payment(
130 &self,
131 destination: &ark::Address,
132 amount: Amount,
133 ) -> anyhow::Result<Vec<Vtxo>> {
134 let mut srv = self.require_server()?;
135
136 self.validate_arkoor_address(&destination).await
137 .context("address validation failed")?;
138
139 let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
140
141 let change_pubkey = self.derive_store_next_keypair().await
142 .context("Failed to create change keypair")?.0;
143
144 let dest = ArkoorDestination { total_amount: amount, policy: destination.policy().clone() };
145 let arkoor = self.create_checkpointed_arkoor(
146 dest.clone(),
147 change_pubkey.public_key(),
148 ).await.context("failed to create arkoor transaction")?;
149
150 let mut movement = self.movements.new_guarded_movement_with_update(
151 Subsystem::ARKOOR,
152 ArkoorMovement::Send.to_string(),
153 OnDropStatus::Failed,
154 MovementUpdate::new()
155 .intended_and_effective_balance(negative_amount)
156 .consumed_vtxos(&arkoor.inputs)
157 .sent_to([MovementDestination::ark(destination.clone(), amount)])
158 ).await?;
159
160 let mut delivered = false;
161 for delivery in destination.delivery() {
162 match delivery {
163 VtxoDelivery::ServerMailbox { blinded_id } => {
164 let req = protos::mailbox_server::PostVtxosMailboxRequest {
165 blinded_id: blinded_id.to_vec(),
166 vtxos: arkoor.created.iter().map(|v| v.serialize().to_vec()).collect(),
167 };
168
169 if let Err(e) = srv.mailbox_client.post_vtxos_mailbox(req).await {
170 error!("Failed to post the vtxos to the destination's mailbox: '{:#}'", e);
171 } else {
173 delivered = true;
174 }
175 },
176 VtxoDelivery::ServerBuiltin => {
177 let req = protos::ArkoorPackage {
178 arkoors: arkoor.created.iter().map(|v| protos::ArkoorVtxo {
179 pubkey: v.policy().user_pubkey().serialize().to_vec(),
180 vtxo: v.serialize().to_vec(),
181 }).collect(),
182 };
183
184 #[allow(deprecated)]
185 if let Err(e) = srv.client.post_arkoor_package_mailbox(req).await {
186 error!("Failed to post the arkoor vtxo to the recipients mailbox: '{:#}'", e);
187 } else {
189 delivered = true;
190 }
191 },
192 VtxoDelivery::Unknown { delivery_type, data } => {
193 error!("Unknown delivery type {} for arkoor payment: {}", delivery_type, data.as_hex());
194 },
195 _ => {
196 error!("Unsupported delivery type for arkoor payment: {:?}", delivery);
197 }
198 }
199 }
200 self.mark_vtxos_as_spent(&arkoor.inputs).await?;
201 if !arkoor.change.is_empty() {
202 self.store_spendable_vtxos(&arkoor.change).await?;
203 movement.apply_update(MovementUpdate::new().produced_vtxos(arkoor.change)).await?;
204 }
205
206 if delivered {
207 movement.success().await?;
208 } else {
209 bail!("Failed to deliver arkoor vtxos to any destination");
210 }
211
212 Ok(arkoor.created)
213 }
214}