1use anyhow::Context;
2use bitcoin::Amount;
3use bitcoin::hex::DisplayHex;
4use bitcoin::secp256k1::PublicKey;
5use log::{info, error};
6
7use ark::{VtxoRequest, ProtocolEncoding};
8use ark::arkoor::checkpointed_package::{CheckpointedPackageBuilder, PackageCosignResponse};
9use ark::vtxo::{Vtxo, VtxoId, VtxoPolicyKind};
10use bitcoin_ext::P2TR_DUST;
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 input: Vec<VtxoId>,
21 created: Vec<Vtxo>,
22 change: Option<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().policy_type() {
38 VtxoPolicyKind::Pubkey => {},
39 VtxoPolicyKind::Checkpoint | VtxoPolicyKind::ServerHtlcRecv | VtxoPolicyKind::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 vtxo_request: VtxoRequest,
68 change_pubkey: PublicKey,
69 ) -> anyhow::Result<ArkoorCreateResult> {
70 if vtxo_request.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(vtxo_request.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 = CheckpointedPackageBuilder::new(
85 inputs.iter().map(|v| &v.vtxo).cloned(),
86 vtxo_request,
87 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::CheckpointedPackageCosignRequest::from(
94 builder.cosign_requests().convert_vtxo(|vtxo| vtxo.id()));
95
96 let response = srv.client.checkpointed_cosign_oor(cosign_request).await
97 .context("server failed to cosign arkoor")?
98 .into_inner();
99
100 let cosign_responses = PackageCosignResponse::try_from(response)
101 .context("Failed to parse cosign response from server")?;
102
103 let vtxos = builder
104 .user_cosign(&user_keypairs, cosign_responses)
105 .context("Failed to cosign vtxos")?
106 .build_signed_vtxos();
107
108 if vtxos.last().expect("At least one vtxo").user_pubkey() == change_pubkey {
110 let nb_vtxos = vtxos.len();
111 let change = vtxos.last().cloned();
112 Ok(ArkoorCreateResult {
113 input: input_ids,
114 created: vtxos.into_iter().take(nb_vtxos.saturating_sub(1)).collect::<Vec<_>>(),
116 change: change,
117 })
118 } else {
119 Ok(ArkoorCreateResult {
120 input: input_ids,
121 created: vtxos,
122 change: None,
123 })
124 }
125 }
126
127 pub async fn send_arkoor_payment(
137 &self,
138 destination: &ark::Address,
139 amount: Amount,
140 ) -> anyhow::Result<Vec<Vtxo>> {
141 let mut srv = self.require_server()?;
142
143 self.validate_arkoor_address(&destination).await
144 .context("address validation failed")?;
145
146 let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
147 if amount < P2TR_DUST {
148 bail!("Sent amount must be at least {}", P2TR_DUST);
149 }
150
151 let change_pubkey = self.derive_store_next_keypair().await
152 .context("Failed to create change keypair")?.0;
153
154 let request = VtxoRequest { amount, policy: destination.policy().clone() };
155 let arkoor = self.create_checkpointed_arkoor(request.clone(), change_pubkey.public_key())
156 .await
157 .context("Failed to create checkpointed transactions")?;
158
159 let mut movement = self.movements.new_guarded_movement_with_update(
160 Subsystem::ARKOOR,
161 ArkoorMovement::Send.to_string(),
162 OnDropStatus::Failed,
163 MovementUpdate::new()
164 .intended_and_effective_balance(negative_amount)
165 .consumed_vtxos(&arkoor.input)
166 .sent_to([MovementDestination::ark(destination.clone(), amount)])
167 ).await?;
168
169 let req = protos::ArkoorPackage {
170 arkoors: arkoor.created.iter().map(|v| protos::ArkoorVtxo {
171 pubkey: request.policy.user_pubkey().serialize().to_vec(),
172 vtxo: v.serialize().to_vec(),
173 }).collect(),
174 };
175
176 #[allow(deprecated)]
177 if let Err(e) = srv.client.post_arkoor_package_mailbox(req).await {
178 error!("Failed to post the arkoor vtxo to the recipients mailbox: '{:#}'", e);
179 }
181 self.mark_vtxos_as_spent(&arkoor.input).await?;
182 if let Some(change) = arkoor.change {
183 self.store_spendable_vtxos([&change]).await?;
184 movement.apply_update(MovementUpdate::new().produced_vtxo(change)).await?;
185 }
186 movement.success().await?;
187 Ok(arkoor.created)
188 }
189}