use anyhow::Context;
use bitcoin::{Amount, NetworkKind};
use bitcoin::hex::DisplayHex;
use bitcoin::secp256k1::Keypair;
use log::{error, info, warn};
use ark::{ProtocolEncoding, VtxoPolicy};
use ark::arkoor::ArkoorDestination;
use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
use ark::vtxo::{Full, Vtxo, VtxoId};
use server_rpc::{protos, ServerConnection};
use crate::{VtxoDelivery, Wallet, WalletVtxo};
use crate::actions::DriveMode;
use crate::actions::arkoor_send::{ArkoorSend, start_arkoor_send};
pub struct ArkoorCreateResult {
pub inputs: Vec<VtxoId>,
pub created: Vec<Vtxo<Full>>,
pub change: Vec<Vtxo<Full>>,
}
#[derive(Debug, thiserror::Error)]
pub enum ArkoorCreateError {
#[error("server failed to cosign arkoor: {0}")]
Cosign(#[source] tonic::Status),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
#[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("Unknown delivery mechanism: {0}")]
UnknownDeliveryMechanism(String),
#[error("Other error: {0}")]
Other(String),
}
pub(crate) fn split_change_amount(change: Amount, pay: Amount, split_factor: u8) -> Vec<Amount> {
if change == Amount::ZERO {
return Vec::new();
}
let pieces = if change > pay { u64::from(split_factor.max(1)) } else { 1 };
let base = change / pieces;
let mut ret = vec![base; pieces as usize];
*ret.last_mut().unwrap() = change - base * (pieces - 1);
ret
}
pub(crate) fn resolve_change_pieces(
stored: Option<Vec<Amount>>,
change: Amount,
) -> anyhow::Result<Vec<Amount>> {
match stored {
Some(pieces) => {
let sum = pieces.iter().copied().sum::<Amount>();
ensure!(sum == change, "stored change pieces sum to {}, expected {}", sum, change);
Ok(pieces)
},
None if change == Amount::ZERO => Ok(Vec::new()),
None => Ok(vec![change]),
}
}
pub(crate) enum DeliveryOutcome {
AnySucceeded,
AllFailed { summary: String },
}
pub(crate) async fn post_arkoor_to_mailboxes(
srv: &mut ServerConnection,
delivery: &[VtxoDelivery],
vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<Full>>>,
) -> DeliveryOutcome {
let serialized = vtxos.into_iter()
.map(|v| v.as_ref().serialize().to_vec())
.collect::<Vec<_>>();
let mut any_succeeded = false;
let mut failures: Vec<String> = Vec::new();
for method in delivery {
let VtxoDelivery::ServerMailbox { blinded_id } = method else { continue };
let req = protos::mailbox_server::PostArkoorMessageRequest {
blinded_id: blinded_id.as_ref().to_vec(),
vtxos: serialized.clone(),
};
match srv.mailbox_client.post_arkoor_message(req).await {
Ok(_) => any_succeeded = true,
Err(e) => {
let reason = format!("{:#}", e);
error!("failed to post arkoor vtxos to mailbox: {}", reason);
failures.push(reason);
},
}
}
if any_succeeded {
return DeliveryOutcome::AnySucceeded;
}
let summary = if failures.is_empty() {
"no mailbox delivery mechanism configured on destination".to_string()
} else {
format!("no delivery mechanism accepted the arkoor vtxos: {}", failures.join("; "))
};
DeliveryOutcome::AllFailed { summary }
}
fn check_delivery(delivery: &[VtxoDelivery]) -> Result<(), ArkoorAddressError> {
if delivery.is_empty() {
return Ok(());
}
if delivery.iter().any(|d| matches!(d, VtxoDelivery::ServerMailbox { .. })) {
return Ok(());
}
let listed = delivery.iter()
.map(|d| match d {
VtxoDelivery::Unknown { delivery_type, data } => {
format!("type={:#x}, data={}", delivery_type, data.as_hex())
},
other => format!("{:?}", other),
})
.collect::<Vec<_>>()
.join("; ");
Err(ArkoorAddressError::UnknownDeliveryMechanism(listed))
}
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_v0(_) | VtxoPolicy::ServerHtlcSend_v0(_)
| VtxoPolicy::ServerHtlcRecv(_) | VtxoPolicy::ServerHtlcSend(_) =>
{
return Err(ArkoorAddressError::PolicyNotSupported(address.policy().clone()));
}
}
check_delivery(address.delivery())?;
Ok(())
}
pub(crate) async fn create_checkpointed_arkoor_with_vtxos(
&self,
arkoor_dest: ArkoorDestination,
inputs: impl IntoIterator<Item = WalletVtxo>,
change_keypair: Keypair,
change_pieces: Option<Vec<Amount>>,
) -> Result<ArkoorCreateResult, ArkoorCreateError> {
let (mut srv, _) = self.require_server().await?;
let input_ids = inputs.into_iter().map(|v| v.id()).collect::<Vec<_>>();
let inputs = self.inner.db.get_full_vtxos(&input_ids).await
.context("failed to hydrate arkoor input vtxos")?;
self.register_vtxo_transactions_with_server(&inputs).await
.context("failed to register arkoor input vtxo transactions with server")?;
let change_pubkey = change_keypair.public_key();
if arkoor_dest.policy.user_pubkey() == change_pubkey {
return Err(anyhow!("Cannot create arkoor to same address as change").into());
}
let mut user_keypairs = vec![];
for vtxo in &inputs {
user_keypairs.push(self.get_vtxo_key(vtxo).await?);
}
let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
let change_amount = total_input.checked_sub(arkoor_dest.total_amount)
.ok_or_else(|| anyhow!("arkoor inputs ({}) don't cover destination ({})",
total_input, arkoor_dest.total_amount,
))?;
let change_policy = VtxoPolicy::new_pubkey(change_pubkey);
let mut outputs = vec![arkoor_dest.clone()];
for piece in resolve_change_pieces(change_pieces, change_amount)? {
outputs.push(ArkoorDestination {
total_amount: piece,
policy: change_policy.clone(),
});
}
let builder = ArkoorPackageBuilder::new_with_checkpoints(inputs, outputs)
.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
.map_err(ArkoorCreateError::Cosign)?
.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);
Ok(ArkoorCreateResult {
inputs: input_ids,
created: dest,
change,
})
}
pub async fn send_arkoor_payment(
&self,
destination: &ark::Address,
amount: Amount,
) -> anyhow::Result<()> {
let action = start_arkoor_send(self, destination.clone(), amount).await?;
self.inner.db.upsert_wallet_action_checkpoint(&action.id, &action.clone().into()).await?;
self.drive_action(action, DriveMode::UntilDone).await
}
pub async fn pending_arkoor_sends(&self) -> anyhow::Result<Vec<ArkoorSend>> {
Ok(self.inner.db.get_all_wallet_action_checkpoints().await?
.into_iter()
.filter_map(|cp| cp.into_arkoor_send())
.collect())
}
pub async fn sync_pending_arkoor_sends(&self) -> anyhow::Result<()> {
let pending = self.pending_arkoor_sends().await?;
if pending.is_empty() {
return Ok(());
}
info!("Syncing {} pending arkoor sends", pending.len());
for send in pending {
let id = send.id.clone();
if let Err(e) = self.drive_action(send, DriveMode::UntilParkOrDone).await {
warn!("Failed to sync arkoor send {}: {:#}", id, e);
}
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn resolve_change_pieces_fallback_and_validation() {
let change = Amount::from_sat(30_000);
let pieces = vec![Amount::from_sat(10_000), Amount::from_sat(20_000)];
assert_eq!(resolve_change_pieces(Some(pieces.clone()), change).unwrap(), pieces);
assert!(resolve_change_pieces(Some(pieces), Amount::from_sat(30_001)).is_err());
assert_eq!(resolve_change_pieces(None, change).unwrap(), vec![change]);
assert_eq!(resolve_change_pieces(None, Amount::ZERO).unwrap(), Vec::<Amount>::new());
}
#[test]
fn check_delivery_requires_a_mailbox() {
use std::str::FromStr;
let mailbox = VtxoDelivery::ServerMailbox {
blinded_id: ark::mailbox::BlindedMailboxIdentifier::from_str(
"024b0d4a4e8a29d2f36a83b4ff4a0e5c5e6f0f8b8d1f2a3b4c5d6e7f80912a3b4c",
).unwrap(),
};
let unknown = VtxoDelivery::Unknown { delivery_type: 0xff, data: vec![1, 2, 3] };
assert_eq!(check_delivery(&[]), Ok(()));
assert!(matches!(
check_delivery(&[unknown.clone()]),
Err(ArkoorAddressError::UnknownDeliveryMechanism(_)),
));
assert_eq!(check_delivery(&[mailbox.clone()]), Ok(()));
assert_eq!(check_delivery(&[unknown, mailbox]), Ok(()));
}
#[test]
fn split_change_amount_pieces() {
let pay = Amount::from_sat(10_000);
for factor in 1..=3u8 {
assert_eq!(split_change_amount(Amount::ZERO, pay, factor), Vec::<Amount>::new());
for sats in [1, 5_000, 10_000] {
let change = Amount::from_sat(sats);
assert_eq!(split_change_amount(change, pay, factor), vec![change]);
}
for sats in [10_001, 123_457, 100_000_000] {
let change = Amount::from_sat(sats);
let pieces = split_change_amount(change, pay, factor);
assert_eq!(pieces.len(), factor as usize);
assert_eq!(pieces.iter().copied().sum::<Amount>(), change);
}
}
}
}