use chia_bls::PublicKey;
use chia_protocol::{Bytes32, Coin, CoinSpend};
use chia_puzzle_types::standard::StandardArgs;
use chia_puzzle_types::Memos;
use chia_sdk_driver::{Cat, CatSpend, SpendContext, SpendWithConditions, StandardLayer};
use chia_sdk_types::Conditions;
use clvmr::NodePtr;
use crate::error::WalletError;
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PaymentAsset {
Xch,
Cat(Bytes32),
}
impl PaymentAsset {
pub fn asset_id(&self) -> Option<Bytes32> {
match self {
PaymentAsset::Xch => None,
PaymentAsset::Cat(id) => Some(*id),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PaymentReceipt {
pub owner_puzzle_hash: Bytes32,
pub amount: u64,
pub asset: PaymentAsset,
pub nonce: Bytes32,
pub payment_coin: Coin,
}
#[derive(Clone, Debug)]
pub struct PaymentResponse {
pub coin_spends: Vec<CoinSpend>,
pub receipt: PaymentReceipt,
}
fn payment_memos(
ctx: &mut SpendContext,
owner_puzzle_hash: Bytes32,
nonce: Bytes32,
) -> Result<Memos<NodePtr>, WalletError> {
Ok(ctx.memos(&[owner_puzzle_hash, nonce])?)
}
pub fn build_xch_payment(
buyer_synthetic_key: PublicKey,
selected_coins: Vec<Coin>,
owner_puzzle_hash: Bytes32,
amount: u64,
nonce: Bytes32,
fee: u64,
) -> Result<PaymentResponse, WalletError> {
if selected_coins.is_empty() {
return Err(WalletError::Parse("selected_coins is empty".to_string()));
}
let buyer_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(buyer_synthetic_key).into();
let total: u64 = selected_coins.iter().map(|c| c.amount).sum();
let reserved = amount + fee;
if total < reserved {
return Err(WalletError::Parse(
"selected coins do not cover amount + fee".to_string(),
));
}
let mut ctx = SpendContext::new();
let p2 = StandardLayer::new(buyer_synthetic_key);
let lead_coin = selected_coins[0];
let lead_coin_name = lead_coin.coin_id();
for coin in selected_coins.into_iter().skip(1) {
p2.spend(
&mut ctx,
coin,
Conditions::new().assert_concurrent_spend(lead_coin_name),
)?;
}
let pay_memos = payment_memos(&mut ctx, owner_puzzle_hash, nonce)?;
let mut lead_conditions = Conditions::new().create_coin(owner_puzzle_hash, amount, pay_memos);
if fee > 0 {
lead_conditions = lead_conditions.reserve_fee(fee);
}
if total > reserved {
let change_hint = ctx.hint(buyer_puzzle_hash)?;
lead_conditions =
lead_conditions.create_coin(buyer_puzzle_hash, total - reserved, change_hint);
}
p2.spend(&mut ctx, lead_coin, lead_conditions)?;
let payment_coin = Coin::new(lead_coin_name, owner_puzzle_hash, amount);
Ok(PaymentResponse {
coin_spends: ctx.take(),
receipt: PaymentReceipt {
owner_puzzle_hash,
amount,
asset: PaymentAsset::Xch,
nonce,
payment_coin,
},
})
}
pub fn build_cat_payment(
buyer_synthetic_key: PublicKey,
selected_cats: Vec<Cat>,
owner_puzzle_hash: Bytes32,
amount: u64,
nonce: Bytes32,
) -> Result<PaymentResponse, WalletError> {
if selected_cats.is_empty() {
return Err(WalletError::Parse("selected_cats is empty".to_string()));
}
let asset_id = selected_cats[0].info.asset_id;
if selected_cats.iter().any(|c| c.info.asset_id != asset_id) {
return Err(WalletError::Parse(
"selected_cats mix more than one asset id".to_string(),
));
}
let buyer_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(buyer_synthetic_key).into();
let total: u64 = selected_cats.iter().map(|c| c.coin.amount).sum();
if total < amount {
return Err(WalletError::Parse(
"selected CATs do not cover amount".to_string(),
));
}
let mut ctx = SpendContext::new();
let p2 = StandardLayer::new(buyer_synthetic_key);
let lead_coin_name = selected_cats[0].coin.coin_id();
let pay_memos = payment_memos(&mut ctx, owner_puzzle_hash, nonce)?;
let change = total - amount;
let mut cat_spends = Vec::with_capacity(selected_cats.len());
for (i, cat) in selected_cats.iter().enumerate() {
let conditions = if i == 0 {
let mut c = Conditions::new().create_coin(owner_puzzle_hash, amount, pay_memos);
if change > 0 {
let change_hint = ctx.hint(buyer_puzzle_hash)?;
c = c.create_coin(buyer_puzzle_hash, change, change_hint);
}
c
} else {
Conditions::new().assert_concurrent_spend(lead_coin_name)
};
let inner = p2.spend_with_conditions(&mut ctx, conditions)?;
cat_spends.push(CatSpend::new(*cat, inner));
}
Cat::spend_all(&mut ctx, &cat_spends)?;
let payment_coin = selected_cats[0].child(owner_puzzle_hash, amount).coin;
Ok(PaymentResponse {
coin_spends: ctx.take(),
receipt: PaymentReceipt {
owner_puzzle_hash,
amount,
asset: PaymentAsset::Cat(asset_id),
nonce,
payment_coin,
},
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObservedPayment {
pub paid_to_puzzle_hash: Bytes32,
pub amount: u64,
pub asset: PaymentAsset,
pub nonce: Option<Bytes32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PaywallError {
WrongRecipient { expected: Bytes32, got: Bytes32 },
InsufficientAmount { required: u64, got: u64 },
WrongAsset {
required: PaymentAsset,
got: PaymentAsset,
},
NonceMismatch {
expected: Bytes32,
got: Option<Bytes32>,
},
}
impl PaywallError {
pub fn code(&self) -> &'static str {
match self {
PaywallError::WrongRecipient { .. } => "WRONG_RECIPIENT",
PaywallError::InsufficientAmount { .. } => "INSUFFICIENT_AMOUNT",
PaywallError::WrongAsset { .. } => "WRONG_ASSET",
PaywallError::NonceMismatch { .. } => "NONCE_MISMATCH",
}
}
}
impl core::fmt::Display for PaywallError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
PaywallError::WrongRecipient { expected, got } => {
write!(
f,
"payment recipient {got} is not the required owner {expected}"
)
}
PaywallError::InsufficientAmount { required, got } => {
write!(f, "payment amount {got} is below the required {required}")
}
PaywallError::WrongAsset { required, got } => {
write!(f, "payment asset {got:?} is not the required {required:?}")
}
PaywallError::NonceMismatch { expected, got } => {
write!(
f,
"payment nonce {got:?} does not match the expected {expected}"
)
}
}
}
}
pub fn verify_payment_receipt(
observed: &ObservedPayment,
owner_puzzle_hash: Bytes32,
min_amount: u64,
asset: PaymentAsset,
require_nonce: Option<Bytes32>,
) -> Result<(), PaywallError> {
if observed.paid_to_puzzle_hash != owner_puzzle_hash {
return Err(PaywallError::WrongRecipient {
expected: owner_puzzle_hash,
got: observed.paid_to_puzzle_hash,
});
}
if observed.asset != asset {
return Err(PaywallError::WrongAsset {
required: asset,
got: observed.asset,
});
}
if observed.amount < min_amount {
return Err(PaywallError::InsufficientAmount {
required: min_amount,
got: observed.amount,
});
}
if let Some(expected) = require_nonce {
if observed.nonce != Some(expected) {
return Err(PaywallError::NonceMismatch {
expected,
got: observed.nonce,
});
}
}
Ok(())
}
pub fn payment_nonce(request_bytes: &[u8]) -> Bytes32 {
crate::metadata::sha256(request_bytes)
}