use anyhow::Context;
use bitcoin::{Amount, NetworkKind};
use bitcoin::hex::DisplayHex;
use log::{info, error};
use ark::{VtxoPolicy, ProtocolEncoding};
use ark::arkoor::ArkoorDestination;
use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
use ark::vtxo::{Full, Vtxo, VtxoId};
use server_rpc::protos;
use crate::subsystem::Subsystem;
use crate::{ArkoorMovement, VtxoDelivery, MovementUpdate, Wallet, WalletVtxo};
use crate::movement::MovementDestination;
use crate::movement::manager::OnDropStatus;
pub struct ArkoorCreateResult {
pub inputs: Vec<VtxoId>,
pub created: Vec<Vtxo<Full>>,
pub change: Vec<Vtxo<Full>>,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ArkoorAddressError {
#[error("Ark address is for different network")]
NetworkMismatch,
#[error("Ark address is for different server")]
ServerMismatch,
#[error("VTXO policy in address cannot be used for arkoor payment: {0:?}")]
PolicyNotSupported(VtxoPolicy),
#[error("No VTXO delivery mechanism provided in address")]
NoDeliveryMechanism,
#[error("Unknown delivery mechanism: {0}")]
UnknownDeliveryMechanism(String),
#[error("Other error: {0}")]
Other(String),
}
impl Wallet {
pub async fn validate_arkoor_address(&self, address: &ark::Address) -> Result<(), ArkoorAddressError> {
let network = self.network().await
.map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
let (_, ark_info) = self.require_server().await
.map_err(|e| ArkoorAddressError::Other(e.to_string()))?;
let network_kind = NetworkKind::from(network);
if address.is_testnet() == network_kind.is_mainnet() {
return Err(ArkoorAddressError::NetworkMismatch);
}
if !address.ark_id().is_for_server(ark_info.server_pubkey) {
return Err(ArkoorAddressError::ServerMismatch);
}
match address.policy() {
VtxoPolicy::Pubkey(_) => {},
VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) => {
return Err(ArkoorAddressError::PolicyNotSupported(address.policy().clone()));
}
}
if address.delivery().is_empty() {
return Err(ArkoorAddressError::NoDeliveryMechanism);
}
if !address.delivery().iter().any(|d| !d.is_unknown()) {
for d in address.delivery() {
if let VtxoDelivery::Unknown { delivery_type, data } = d {
info!("Unknown delivery in address: type={:#x}, data={}",
delivery_type, data.as_hex(),
);
}
}
}
Ok(())
}
pub(crate) async fn create_checkpointed_arkoor(
&self,
arkoor_dest: ArkoorDestination,
) -> anyhow::Result<ArkoorCreateResult> {
let inputs = self.select_vtxos_to_cover(arkoor_dest.total_amount).await?;
self.create_checkpointed_arkoor_with_vtxos(
arkoor_dest,
inputs.into_iter(),
).await
}
pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
&self,
arkoor_dest: ArkoorDestination,
inputs: impl IntoIterator<Item = WalletVtxo>,
) -> anyhow::Result<ArkoorCreateResult> {
let (mut srv, _) = self.require_server().await?;
let (input_ids, inputs) = inputs.into_iter()
.map(|v| (v.id(), v))
.collect::<(Vec<_>, Vec<_>)>();
let (change_keypair, change_key_index) = self.peek_next_keypair().await?;
let change_pubkey = change_keypair.public_key();
if arkoor_dest.policy.user_pubkey() == change_pubkey {
bail!("Cannot create arkoor to same address as change");
}
let mut user_keypairs = vec![];
for vtxo in &inputs {
user_keypairs.push(self.get_vtxo_key(vtxo).await?);
}
let builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
inputs.into_iter().map(|v| v.vtxo),
arkoor_dest.clone(),
VtxoPolicy::new_pubkey(change_pubkey),
)
.context("Failed to construct arkoor package")?
.generate_user_nonces(&user_keypairs)
.context("invalid nb of keypairs")?;
let cosign_request = protos::ArkoorPackageCosignRequest::from(
builder.cosign_request(),
);
let response = srv.client.request_arkoor_cosign(cosign_request).await
.context("server failed to cosign arkoor")?
.into_inner();
let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
.context("Failed to parse cosign response from server")?;
let vtxos = builder
.user_cosign(&user_keypairs, cosign_responses)
.context("Failed to cosign vtxos")?
.build_signed_vtxos();
let (dest, change) = vtxos.into_iter()
.partition::<Vec<_>, _>(|v| *v.policy() == arkoor_dest.policy);
if !change.is_empty() {
self.db.store_vtxo_key(change_key_index, change_pubkey).await?;
}
Ok(ArkoorCreateResult {
inputs: input_ids,
created: dest,
change,
})
}
pub async fn send_arkoor_payment(
&self,
destination: &ark::Address,
amount: Amount,
) -> anyhow::Result<Vec<Vtxo<Full>>> {
let (mut srv, _) = self.require_server().await?;
self.validate_arkoor_address(&destination).await
.context("address validation failed")?;
let negative_amount = -amount.to_signed().context("Amount out-of-range")?;
let dest = ArkoorDestination { total_amount: amount, policy: destination.policy().clone() };
let arkoor = self.create_checkpointed_arkoor(dest.clone())
.await.context("failed to create arkoor transaction")?;
let mut movement = self.movements.new_guarded_movement_with_update(
Subsystem::ARKOOR,
ArkoorMovement::Send.to_string(),
OnDropStatus::Failed,
MovementUpdate::new()
.intended_and_effective_balance(negative_amount)
.consumed_vtxos(&arkoor.inputs)
.sent_to([MovementDestination::ark(destination.clone(), amount)])
).await?;
let mut delivered = false;
for delivery in destination.delivery() {
match delivery {
VtxoDelivery::ServerMailbox { blinded_id } => {
let req = protos::mailbox_server::PostArkoorMessageRequest {
blinded_id: blinded_id.to_vec(),
vtxos: arkoor.created.iter().map(|v| v.serialize().to_vec()).collect(),
};
if let Err(e) = srv.mailbox_client.post_arkoor_message(req).await {
error!("Failed to post the vtxos to the destination's mailbox: '{:#}'", e);
} else {
delivered = true;
}
},
VtxoDelivery::Unknown { delivery_type, data } => {
error!("Unknown delivery type {} for arkoor payment: {}", delivery_type, data.as_hex());
},
_ => {
error!("Unsupported delivery type for arkoor payment: {:?}", delivery);
}
}
}
self.mark_vtxos_as_spent(&arkoor.inputs).await?;
if !arkoor.change.is_empty() {
self.store_spendable_vtxos(&arkoor.change).await?;
movement.apply_update(MovementUpdate::new().produced_vtxos(arkoor.change)).await?;
}
if delivered {
movement.success().await?;
} else {
bail!("Failed to deliver arkoor vtxos to any destination");
}
Ok(arkoor.created)
}
}