1use anyhow::Context;
2use bitcoin::Amount;
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
24impl Wallet {
25 pub async fn validate_arkoor_address(&self, address: &ark::Address) -> anyhow::Result<()> {
29 let (srv, _) = self.require_server().await?;
30
31 if !address.ark_id().is_for_server(srv.ark_info().await?.server_pubkey) {
32 bail!("Ark address is for different server");
33 }
34
35 match address.policy() {
37 VtxoPolicy::Pubkey(_) => {},
38 VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) => {
39 bail!("VTXO policy in address cannot be used for arkoor payment: {}",
40 address.policy().policy_type(),
41 );
42 }
43 }
44
45 if address.delivery().is_empty() {
46 bail!("No VTXO delivery mechanism provided in address");
47 }
48 if !address.delivery().iter().any(|d| !d.is_unknown()) {
52 for d in address.delivery() {
53 if let VtxoDelivery::Unknown { delivery_type, data } = d {
54 info!("Unknown delivery in address: type={:#x}, data={}",
55 delivery_type, data.as_hex(),
56 );
57 }
58 }
59 }
60
61 Ok(())
62 }
63
64 pub(crate) async fn create_checkpointed_arkoor(
65 &self,
66 arkoor_dest: ArkoorDestination,
67 ) -> anyhow::Result<ArkoorCreateResult> {
68 let inputs = self.select_vtxos_to_cover(arkoor_dest.total_amount).await?;
69 self.create_checkpointed_arkoor_with_vtxos(
70 arkoor_dest,
71 inputs.into_iter(),
72 ).await
73 }
74
75 pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
76 &self,
77 arkoor_dest: ArkoorDestination,
78 inputs: impl IntoIterator<Item = WalletVtxo>,
79 ) -> anyhow::Result<ArkoorCreateResult> {
80 let (mut srv, _) = self.require_server().await?;
82 let (input_ids, inputs) = inputs.into_iter()
83 .map(|v| (v.id(), v))
84 .collect::<(Vec<_>, Vec<_>)>();
85
86 let (change_keypair, change_key_index) = self.peek_next_keypair().await?;
89 let change_pubkey = change_keypair.public_key();
90
91 if arkoor_dest.policy.user_pubkey() == change_pubkey {
92 bail!("Cannot create arkoor to same address as change");
93 }
94
95 let mut user_keypairs = vec![];
96 for vtxo in &inputs {
97 user_keypairs.push(self.get_vtxo_key(vtxo).await?);
98 }
99
100 let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
101 inputs.into_iter().map(|v| v.vtxo),
102 arkoor_dest.clone(),
103 VtxoPolicy::new_pubkey(change_pubkey),
104 )
105 .context("Failed to construct arkoor package")?
106 .generate_user_nonces(&user_keypairs)
107 .context("invalid nb of keypairs")?;
108
109 let cosign_request = protos::ArkoorPackageCosignRequest::from(
110 builder.cosign_request(),
111 );
112
113 let response = srv.client.request_arkoor_cosign(cosign_request).await
114 .context("server failed to cosign arkoor")?
115 .into_inner();
116
117 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
118 .context("Failed to parse cosign response from server")?;
119
120 let vtxos = builder
121 .user_cosign(&user_keypairs, cosign_responses)
122 .context("Failed to cosign vtxos")?
123 .build_signed_vtxos();
124
125 let (dest, change) = vtxos.into_iter()
127 .partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
128
129 if !change.is_empty() {
130 self.db.store_vtxo_key(change_key_index, change_pubkey).await?;
132 }
133
134 Ok(ArkoorCreateResult {
135 inputs: input_ids,
136 created: dest,
137 change,
138 })
139 }
140
141 pub async fn send_arkoor_payment(
151 &self,
152 destination: &ark::Address,
153 amount: Amount,
154 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
155 let (mut srv, _) = self.require_server().await?;
156
157 self.validate_arkoor_address(&destination).await
158 .context("address validation failed")?;
159
160 let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
161
162 let dest = ArkoorDestination { total_amount: amount, policy: destination.policy().clone() };
163 let arkoor = self.create_checkpointed_arkoor(dest.clone())
164 .await.context("failed to create arkoor transaction")?;
165
166 let mut movement = self.movements.new_guarded_movement_with_update(
167 Subsystem::ARKOOR,
168 ArkoorMovement::Send.to_string(),
169 OnDropStatus::Failed,
170 MovementUpdate::new()
171 .intended_and_effective_balance(negative_amount)
172 .consumed_vtxos(&arkoor.inputs)
173 .sent_to([MovementDestination::ark(destination.clone(), amount)])
174 ).await?;
175
176 let mut delivered = false;
177 for delivery in destination.delivery() {
178 match delivery {
179 VtxoDelivery::ServerMailbox { blinded_id } => {
180 let req = protos::mailbox_server::PostArkoorMessageRequest {
181 blinded_id: blinded_id.to_vec(),
182 vtxos: arkoor.created.iter().map(|v| v.serialize().to_vec()).collect(),
183 };
184
185 if let Err(e) = srv.mailbox_client.post_arkoor_message(req).await {
186 error!("Failed to post the vtxos to the destination's 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}