use chia_protocol::{Bytes32, Coin, SpendBundle};
use chia_puzzle_types::offer::{NotarizedPayment, Payment};
use chia_puzzle_types::Memos;
use chia_wallet_sdk::driver::{
encode_offer, Action, AssetInfo, Cat, CatAssetInfo, Id, Offer, Relation, RequestedPayments,
SpendContext, Spends,
};
use chia_wallet_sdk::prelude::PublicKey;
use chia_wallet_sdk::types::puzzles::SettlementPayment;
use chia_wallet_sdk::types::Mod;
use indexmap::IndexMap;
use crate::error::{Error, Result};
use crate::types::{OfferedSide, RequestedSide, UnsignedMake};
fn settlement_payment_hash() -> Bytes32 {
Bytes32::from(<[u8; 32]>::from(SettlementPayment::mod_hash()))
}
pub fn make_build(
ctx: &mut SpendContext,
offered: OfferedSide<'_>,
requested: RequestedSide,
fee: u64,
) -> Result<UnsignedMake> {
ensure_non_empty(&offered, &requested)?;
let settlement_ph = settlement_payment_hash();
let mut spends = Spends::new(offered.change_puzzle_hash);
let key_map: IndexMap<Bytes32, PublicKey> = offered.owner_keys.clone();
let mut actions: Vec<Action> = Vec::new();
let mut offered_coin_ids: Vec<Bytes32> = Vec::new();
add_offered_nfts(
&offered,
&mut spends,
&mut actions,
&mut offered_coin_ids,
settlement_ph,
);
add_offered_xch(
&offered,
fee,
&mut spends,
&mut actions,
&mut offered_coin_ids,
settlement_ph,
)?;
add_offered_cats(
&offered,
&mut spends,
&mut actions,
&mut offered_coin_ids,
settlement_ph,
)?;
if offered_coin_ids.is_empty() {
return Err(Error::invalid("make selected no offered coins to spend"));
}
let nonce = Offer::nonce(offered_coin_ids);
let (requested_payments, requested_asset_info) = build_requested(ctx, &requested, nonce)?;
if fee > 0 {
actions.push(Action::fee(fee));
}
let deltas = spends.apply(ctx, &actions)?;
spends.conditions.required = spends
.conditions
.required
.extend(requested_payments.assertions(ctx, &requested_asset_info)?);
spends.finish_with_keys(ctx, &deltas, Relation::AssertConcurrent, &key_map)?;
Ok(UnsignedMake {
coin_spends: ctx.take(),
requested_payments,
requested_asset_info,
nonce,
})
}
pub fn make_assemble(
ctx: &mut SpendContext,
signed: SpendBundle,
requested_payments: RequestedPayments,
requested_asset_info: AssetInfo,
) -> Result<String> {
let offer =
Offer::from_input_spend_bundle(ctx, signed, requested_payments, requested_asset_info)?;
let spend_bundle = offer.to_spend_bundle(ctx)?;
encode_offer(&spend_bundle).map_err(|e| Error::decode(format!("could not encode offer: {e}")))
}
fn ensure_non_empty(offered: &OfferedSide<'_>, requested: &RequestedSide) -> Result<()> {
let offers_nothing =
offered.offer_xch == 0 && offered.offer_cats.is_empty() && offered.nfts.is_empty();
if offers_nothing {
return Err(Error::invalid("make must offer at least one asset"));
}
let requests_nothing =
requested.xch == 0 && requested.cats.is_empty() && requested.nfts.is_empty();
if requests_nothing {
return Err(Error::invalid("make must request at least one asset"));
}
Ok(())
}
fn add_offered_nfts(
offered: &OfferedSide<'_>,
spends: &mut Spends,
actions: &mut Vec<Action>,
offered_coin_ids: &mut Vec<Bytes32>,
settlement_ph: Bytes32,
) {
for nft in &offered.nfts {
spends.add(*nft);
offered_coin_ids.push(nft.coin.coin_id());
actions.push(Action::send(
Id::Existing(nft.info.launcher_id),
settlement_ph,
1,
Memos::None,
));
}
}
fn add_offered_xch(
offered: &OfferedSide<'_>,
fee: u64,
spends: &mut Spends,
actions: &mut Vec<Action>,
offered_coin_ids: &mut Vec<Bytes32>,
settlement_ph: Bytes32,
) -> Result<()> {
let xch_needed = offered.offer_xch.saturating_add(fee);
if xch_needed > 0 {
for coin in select_xch(&offered.xch_coins, xch_needed)? {
spends.add(coin);
offered_coin_ids.push(coin.coin_id());
}
}
if offered.offer_xch > 0 {
actions.push(Action::send(
Id::Xch,
settlement_ph,
offered.offer_xch,
Memos::None,
));
}
Ok(())
}
fn add_offered_cats(
offered: &OfferedSide<'_>,
spends: &mut Spends,
actions: &mut Vec<Action>,
offered_coin_ids: &mut Vec<Bytes32>,
settlement_ph: Bytes32,
) -> Result<()> {
for (asset_id, amount) in &offered.offer_cats {
for cat in select_cats(&offered.cat_coins, *asset_id, *amount)? {
spends.add(cat);
offered_coin_ids.push(cat.coin.coin_id());
}
actions.push(Action::send(
Id::Existing(*asset_id),
settlement_ph,
*amount,
Memos::None,
));
}
Ok(())
}
fn build_requested(
ctx: &mut SpendContext,
requested: &RequestedSide,
nonce: Bytes32,
) -> Result<(RequestedPayments, AssetInfo)> {
let payee = requested.payee_puzzle_hash;
let hint = ctx.hint(payee)?;
let mut payments = RequestedPayments::new();
let mut asset_info = AssetInfo::new();
if requested.xch > 0 {
payments.xch.push(NotarizedPayment::new(
nonce,
vec![Payment::new(payee, requested.xch, hint)],
));
}
for (asset_id, amount) in &requested.cats {
if *amount == 0 {
return Err(Error::invalid(
"requested CAT amount must be greater than zero",
));
}
payments.cats.insert(
*asset_id,
vec![NotarizedPayment::new(
nonce,
vec![Payment::new(payee, *amount, hint)],
)],
);
asset_info.insert_cat(*asset_id, CatAssetInfo::new(None))?;
}
for (launcher_id, nft_info) in &requested.nfts {
payments.nfts.insert(
*launcher_id,
vec![NotarizedPayment::new(
nonce,
vec![Payment::new(payee, 1, hint)],
)],
);
asset_info.insert_nft(*launcher_id, *nft_info)?;
}
Ok((payments, asset_info))
}
fn select_xch(coins: &[Coin], need: u64) -> Result<Vec<Coin>> {
let mut sorted: Vec<Coin> = coins.to_vec();
sorted.sort_by_key(|coin| std::cmp::Reverse(coin.amount));
let mut sum = 0u64;
let mut chosen = Vec::new();
for coin in sorted {
if sum >= need {
break;
}
sum = sum.saturating_add(coin.amount);
chosen.push(coin);
}
if sum < need {
return Err(Error::invalid(format!(
"insufficient XCH to offer: need {need}, have {sum}"
)));
}
Ok(chosen)
}
fn select_cats(coins: &[Cat], asset_id: Bytes32, need: u64) -> Result<Vec<Cat>> {
let mut sorted: Vec<Cat> = coins
.iter()
.filter(|cat| cat.info.asset_id == asset_id)
.copied()
.collect();
sorted.sort_by_key(|cat| std::cmp::Reverse(cat.coin.amount));
let mut sum = 0u64;
let mut chosen = Vec::new();
for cat in sorted {
if sum >= need {
break;
}
sum = sum.saturating_add(cat.coin.amount);
chosen.push(cat);
}
if sum < need {
return Err(Error::invalid(format!(
"insufficient CAT {asset_id} to offer: need {need}, have {sum}"
)));
}
Ok(chosen)
}
#[cfg(test)]
mod tests {
use std::marker::PhantomData;
use super::*;
use chia_sdk_test::Simulator;
use crate::test_support::owner_keys;
use crate::types::{OfferedSide, RequestedSide};
fn empty_offered<'a>(change: Bytes32) -> OfferedSide<'a> {
OfferedSide {
change_puzzle_hash: change,
owner_keys: IndexMap::new(),
xch_coins: vec![],
cat_coins: vec![],
nfts: vec![],
offer_xch: 0,
offer_cats: vec![],
_pd: PhantomData,
}
}
#[test]
fn make_build_rejects_offering_nothing() {
let mut ctx = SpendContext::new();
let requested = RequestedSide {
payee_puzzle_hash: Bytes32::default(),
xch: 1,
cats: vec![],
nfts: vec![],
};
let err =
make_build(&mut ctx, empty_offered(Bytes32::default()), requested, 0).unwrap_err();
assert!(matches!(&err, Error::InvalidInput(m) if m.contains("offer at least one")));
}
#[test]
fn make_build_rejects_requesting_nothing() {
let mut ctx = SpendContext::new();
let mut offered = empty_offered(Bytes32::default());
offered.offer_xch = 1;
let requested = RequestedSide {
payee_puzzle_hash: Bytes32::default(),
xch: 0,
cats: vec![],
nfts: vec![],
};
let err = make_build(&mut ctx, offered, requested, 0).unwrap_err();
assert!(matches!(&err, Error::InvalidInput(m) if m.contains("request at least one")));
}
#[test]
fn make_build_rejects_insufficient_offered_funds() {
let mut ctx = SpendContext::new();
let mut offered = empty_offered(Bytes32::default());
offered.offer_xch = 1_000; let requested = RequestedSide {
payee_puzzle_hash: Bytes32::default(),
xch: 1,
cats: vec![],
nfts: vec![],
};
let err = make_build(&mut ctx, offered, requested, 0).unwrap_err();
assert!(matches!(&err, Error::InvalidInput(m) if m.contains("insufficient XCH to offer")));
}
#[test]
fn make_build_rejects_zero_requested_cat_amount() {
let mut sim = Simulator::new();
let mut ctx = SpendContext::new();
let maker = sim.bls(1_000_000);
let offered = OfferedSide {
change_puzzle_hash: maker.puzzle_hash,
owner_keys: owner_keys(&maker),
xch_coins: vec![maker.coin],
cat_coins: vec![],
nfts: vec![],
offer_xch: 100_000,
offer_cats: vec![],
_pd: PhantomData,
};
let requested = RequestedSide {
payee_puzzle_hash: maker.puzzle_hash,
xch: 0,
cats: vec![(Bytes32::from([7; 32]), 0)],
nfts: vec![],
};
let err = make_build(&mut ctx, offered, requested, 0).unwrap_err();
assert!(matches!(&err, Error::InvalidInput(m) if m.contains("greater than zero")));
}
}