use bitcoin::{ScriptBuf, Txid};
use sidestr_core::assets::AssetView;
use sidestr_core::document::ChainDocument;
use sidestr_core::records::tally_text;
use crate::coins::{mature, Coin};
use crate::compose::{build_outputs, OutputsRequest};
use crate::error::{Error, Result};
use crate::key::SpendSigner;
use crate::policy::SpendPolicy;
use crate::spend::{dust_threshold, resolve_to, Spend};
pub const CARRIER: u64 = 330;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Sorted {
pub carriers: Vec<(Coin, u64)>,
pub plain: Vec<Coin>,
pub other: Vec<Coin>,
}
pub fn sort_coins(coins: &[Coin], view: &AssetView, asset: Option<&Txid>) -> Sorted {
let mut s = Sorted::default();
for c in coins {
match view.carried(&c.outpoint) {
None => s.plain.push(c.clone()),
Some(carry) => match asset {
Some(a) if carry.len() == 1 && carry.contains_key(a) => {
s.carriers.push((c.clone(), carry[a]))
}
_ => s.other.push(c.clone()),
},
}
}
s
}
pub fn plain_coins(coins: &[Coin], view: &AssetView) -> Vec<Coin> {
sort_coins(coins, view, None).plain
}
pub fn balance_of(coins: &[Coin], view: &AssetView, asset: &Txid) -> u64 {
coins
.iter()
.filter_map(|c| view.carried(&c.outpoint).and_then(|m| m.get(asset)))
.sum()
}
#[derive(Debug, Clone, Copy)]
pub struct TransferRequest<'a> {
pub chain: &'a ChainDocument,
pub coins: &'a [Coin],
pub view: &'a AssetView,
pub tip_height: u32,
pub asset: Txid,
pub to: &'a str,
pub amount: u64,
pub memos: &'a [String],
pub fee: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Transfer {
pub spend: Spend,
pub asset_change: u64,
}
fn check_memos(memos: &[String]) -> Result<()> {
for m in memos {
if m.starts_with("issue:") || m.starts_with("tally:") || m.starts_with("pool:") {
return Err(Error::Asset(format!(
"a memo may not be an assets record: {}",
m.chars().take(20).collect::<String>()
)));
}
}
Ok(())
}
pub fn build_transfer(
req: &TransferRequest<'_>,
signer: &dyn SpendSigner,
policy: &dyn SpendPolicy,
) -> Result<Transfer> {
if req.amount == 0 {
return Err(Error::BadAmount);
}
check_memos(req.memos)?;
let dest = resolve_to(req.to, &req.chain.address_prefix)?.script;
let me = signer.script();
let sorted = sort_coins(req.coins, req.view, Some(&req.asset));
let mut carriers: Vec<(Coin, u64)> = sorted
.carriers
.into_iter()
.filter(|(c, _)| c.is_mature(req.tip_height))
.collect();
carriers.sort_by_key(|(_, n)| core::cmp::Reverse(*n));
let mut picked = Vec::new();
let mut have = 0u64;
for (c, n) in carriers {
if have >= req.amount {
break;
}
have = have.saturating_add(n);
picked.push(c);
}
if have < req.amount {
return Err(Error::Asset(format!(
"holds {have} of the asset, {} asked",
req.amount
)));
}
let change = have - req.amount;
let mut outputs = vec![(dest.clone(), CARRIER.max(dust_threshold(&dest)))];
let mut assigns = vec![(0u32, req.amount)];
if change > 0 {
outputs.push((me.clone(), CARRIER));
assigns.push((1, change));
}
let mut records = vec![tally_text(Some(&req.asset), &assigns)?];
records.extend(req.memos.iter().cloned());
let spend = build_outputs(
&OutputsRequest {
chain: req.chain,
coins: &sorted.plain,
required: &picked,
tip_height: req.tip_height,
outputs: &outputs,
records: &records,
fee: req.fee,
},
signer,
policy,
)?;
verify(req.view, &spend, &req.asset, &assigns)?;
Ok(Transfer {
spend,
asset_change: change,
})
}
#[derive(Debug, Clone, Copy)]
pub struct IssueRequest<'a> {
pub chain: &'a ChainDocument,
pub coins: &'a [Coin],
pub view: &'a AssetView,
pub tip_height: u32,
pub ticker: &'a str,
pub decimals: u8,
pub supply: u64,
pub to: Option<&'a str>,
pub fee: Option<u64>,
}
pub fn build_issue(
req: &IssueRequest<'_>,
signer: &dyn SpendSigner,
policy: &dyn SpendPolicy,
) -> Result<Spend> {
let issue = format!("issue:{}:{}", req.ticker, req.decimals);
if sidestr_core::records::parse_issue(&issue).is_none() {
return Err(Error::Asset(format!(
"{issue} is not an issue record: a ticker is 1 to 8 of A-Z0-9, decimals 0 to 8"
)));
}
let to: ScriptBuf = match req.to {
Some(t) => resolve_to(t, &req.chain.address_prefix)?.script,
None => signer.script(),
};
let tally = tally_text(None, &[(0, req.supply)])?;
let plain = plain_coins(&mature(req.coins, req.tip_height), req.view);
let spend = build_outputs(
&OutputsRequest {
chain: req.chain,
coins: &plain,
required: &[],
tip_height: req.tip_height,
outputs: &[(to, CARRIER)],
records: &[issue, tally],
fee: req.fee,
},
signer,
policy,
)?;
let mut carried_in = Default::default();
let out = req
.view
.check(&spend.tx, &mut carried_in)
.map_err(Error::Asset)?;
if out.get(&0).and_then(|c| c.get(&spend.txid)) != Some(&req.supply) {
return Err(Error::Asset("the issue does not read back as built".into()));
}
Ok(spend)
}
fn verify(view: &AssetView, spend: &Spend, asset: &Txid, assigns: &[(u32, u64)]) -> Result<()> {
let mut carried_in = Default::default();
let out = view
.check(&spend.tx, &mut carried_in)
.map_err(Error::Asset)?;
if carried_in.keys().any(|a| a != asset) {
return Err(Error::Asset("an input carries another asset".into()));
}
for (v, n) in assigns {
if out.get(v).and_then(|c| c.get(asset)) != Some(n) {
return Err(Error::Asset(format!(
"output {v} does not read back as {n}"
)));
}
}
Ok(())
}