diff --git a/plugin-example/ordinal-example/src/plugin.rs b/plugin-example/ordinal-example/src/plugin.rs index 550570bb..0c605a5a 100644 --- a/plugin-example/ordinal-example/src/plugin.rs +++ b/plugin-example/ordinal-example/src/plugin.rs @@ -1,26 +1,21 @@ -use bitcoin::hashes::hex::ToHex; -use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; +use std::collections::BTreeMap; + use bitcoin::util::amount::Amount; use bitcoin::XOnlyPublicKey; use sapio::contract::empty; -use sapio::contract::object::ObjectMetadata; +use sapio::contract::Compilable; use sapio::contract::CompilationError; use sapio::contract::Compiled; use sapio::contract::Contract; use sapio::contract::StatefulArgumentsTrait; +use sapio::ordinals::OrdinalPlanner; +use sapio::ordinals::OrdinalSpec; use sapio::util::amountrange::AmountF64; use sapio::*; use sapio_base::Clause; -use sapio_wasm_nft_trait::*; -use sapio_wasm_plugin::client::*; -use sapio_wasm_plugin::plugin_handle::PluginHandle; use sapio_wasm_plugin::*; use schemars::*; use serde::*; -use std::convert::TryFrom; -use std::convert::TryInto; -use std::sync::Arc; /// # SimpleOrdinal /// A really Ordinal Bearing Contract #[derive(JsonSchema, Serialize, Deserialize)] @@ -39,22 +34,57 @@ pub struct Sell { #[derive(JsonSchema, Serialize, Deserialize, Default)] pub struct Sale(Option); +fn multimap( + v: [(T, U); N], +) -> BTreeMap> { + let mut ret = BTreeMap::new(); + for (t, u) in v.into_iter() { + ret.entry(t.clone()).or_insert(vec![]).push(u) + } + ret +} // ASSUMES 500 sats after Ord are "dust" impl SimpleOrdinal { + #[continuation(guarded_by = "[Self::signed]", web_api, coerce_args = "default_coerce")] + fn sell_with_planner(self, ctx: Context, opt_sale: Sale) { + if let Sale(Some(sale)) = opt_sale { + if let Some(ords) = ctx.get_ordinals().clone() { + let plan = ords.output_plan(&OrdinalSpec { + payouts: vec![sale.amount.into()], + payins: vec![Amount::from(sale.amount) + sale.fee.into() + sale.change.into()], + fees: sale.fee.into(), + ordinals: [Ordinal(self.ordinal)].into(), + })?; + let buyer: &dyn Compilable = &Compiled::from_address(sale.purchaser, None); + return plan + .build_plan( + ctx, + multimap([ + (sale.amount.into(), (&self.owner, None)), + (sale.change.into(), (buyer, None)), + ]), + [(Ordinal(self.ordinal), (buyer, None))].into(), + (&self.owner, None), + )? + .into(); + } + } + empty() + } #[continuation(guarded_by = "[Self::signed]", web_api, coerce_args = "default_coerce")] fn sell(self, ctx: Context, opt_sale: Sale) { if let Sale(Some(sale)) = opt_sale { - let o = ctx + let ords = ctx .get_ordinals() .as_ref() .ok_or_else(|| CompilationError::OrdinalsError("Missing Ordinals Info".into()))?; let mut index = 0; - for (a, b) in o.iter() { - if (*a..*b).contains(&self.ordinal) { - index += (self.ordinal) - a; + for (a, b) in ords.0.iter() { + if (*a..*b).contains(&Ordinal(self.ordinal)) { + index += self.ordinal - a.0; break; } else { - index += b - a + index += b.0 - a.0 } } let mut t = ctx.template(); @@ -82,14 +112,14 @@ impl StatefulArgumentsTrait for Sale {} /// # The SimpleNFT Contract impl Contract for SimpleOrdinal { // Ordinals... only good for selling? - declare! {updatable, Self::sell} + declare! {updatable, Self::sell, Self::sell_with_planner} fn ensure_amount(&self, ctx: Context) -> Result { let ords = ctx .get_ordinals() .as_ref() .ok_or_else(|| CompilationError::OrdinalsError("Missing Ordinals Info".into()))?; - if ords.iter().any(|(a, b)| (*a..*b).contains(&self.ordinal)) { + if ords.0.iter().any(|(a, b)| (*a..*b).contains(&Ordinal(self.ordinal))) { Ok(Amount::from_sat(1 + 500)) } else { Err(CompilationError::OrdinalsError( diff --git a/sapio-base/src/plugin_args.rs b/sapio-base/src/plugin_args.rs index 7069798f..b4850f04 100644 --- a/sapio-base/src/plugin_args.rs +++ b/sapio-base/src/plugin_args.rs @@ -6,6 +6,7 @@ //! arguments for passing into a sapio module use crate::effects::MapEffectDB; +use bitcoin::Amount; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -51,5 +52,22 @@ pub struct ContextualArguments { /// # the ranges of ordinals held in the input #[serde(skip_serializing_if = "Option::is_none", default)] - pub ordinals_info: Option> + pub ordinals_info: Option, } + +/// Struct to contain Ordinal ID +#[derive( + Serialize, Deserialize, Eq, Ord, PartialEq, PartialOrd, Clone, Copy, Debug, JsonSchema, +)] +pub struct Ordinal(pub u64); + +impl Ordinal { + /// How much padding in sats to require + /// TODO: Flexible padding + pub fn padding(&self) -> Amount { + Amount::from_sat(500) + } +} +/// Struct to contain Ordinal Spans +#[derive(Serialize, Deserialize, Eq, Ord, PartialEq, PartialOrd, Clone, Debug, JsonSchema)] +pub struct OrdinalsInfo(pub Vec<(Ordinal, Ordinal)>); diff --git a/sapio/src/contract/context.rs b/sapio/src/contract/context.rs index 137c9254..4261f2d7 100644 --- a/sapio/src/contract/context.rs +++ b/sapio/src/contract/context.rs @@ -7,6 +7,8 @@ //! general non-parameter compilation state required by all contracts use super::{Amount, Compilable, CompilationError, Compiled}; use crate::contract::compiler::InternalCompilerTag; +use crate::ordinals::Ordinal; +use crate::ordinals::OrdinalsInfo; use bitcoin::Network; @@ -33,22 +35,22 @@ pub struct Context { path: Arc, already_derived: HashSet, effects: Arc, - ordinals_info: Option>, + ordinals_info: Option, } -fn allocate_ordinals(a: Amount, ords: &Vec<(u64, u64)>) -> [Vec<(u64, u64)>; 2] { +fn allocate_ordinals(a: Amount, ords: &OrdinalsInfo) -> [OrdinalsInfo; 2] { let mut amt = a.as_sat(); - let mut ret = [vec![], vec![]]; - for (start, end) in ords.iter().copied() { - let sats = end - start; + let mut ret = [OrdinalsInfo(vec![]), OrdinalsInfo(vec![])]; + for (start, end) in ords.0.iter().copied() { + let sats = end.0 - start.0; if sats <= amt { amt -= sats; - ret[0].push((start, end)) + ret[0].0.push((start, end)) } else { if sats != 0 { - ret[0].push((start, start + sats)); + ret[0].0.push((start, Ordinal(start.0 + sats))); } - ret[1].push((start + sats, end)) + ret[1].0.push((Ordinal(start.0 + sats), end)) } } ret @@ -56,7 +58,7 @@ fn allocate_ordinals(a: Amount, ords: &Vec<(u64, u64)>) -> [Vec<(u64, u64)>; 2] impl Context { /// Borrow the Ordinals Info - pub fn get_ordinals(&self) -> &Option> { + pub fn get_ordinals(&self) -> &Option { &self.ordinals_info } /// create a context instance. Should only happen *once* at the very top @@ -67,7 +69,7 @@ impl Context { emulator: Arc, path: EffectPath, effects: Arc, - ordinals_info: Option>, + ordinals_info: Option, ) -> Self { Context { available_funds, @@ -171,7 +173,7 @@ impl Context { effects: self.effects.clone(), ordinals_info: self.ordinals_info.as_ref().map(|o| { let mut a = allocate_ordinals(amount, o); - let mut v = vec![]; + let mut v = OrdinalsInfo(vec![]); mem::swap(&mut a[0], &mut v); v }), @@ -187,7 +189,7 @@ impl Context { self.ordinals_info = self.ordinals_info.as_ref().map(|o| { let mut a = allocate_ordinals(amount, o); - let mut v = vec![]; + let mut v = OrdinalsInfo(vec![]); mem::swap(&mut a[1], &mut v); v }); diff --git a/sapio/src/lib.rs b/sapio/src/lib.rs index 78845392..187c4fb4 100644 --- a/sapio/src/lib.rs +++ b/sapio/src/lib.rs @@ -17,3 +17,4 @@ pub use sapio_base; pub use sapio_macros; pub use sapio_macros::*; pub use schemars; +pub mod ordinals; diff --git a/sapio/src/ordinals/mod.rs b/sapio/src/ordinals/mod.rs new file mode 100644 index 00000000..db70f5ed --- /dev/null +++ b/sapio/src/ordinals/mod.rs @@ -0,0 +1,288 @@ +// Copyright Judica, Inc 2021 +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Functionality for Ordinals + +use crate::{ + contract::{Compilable, CompilationError, TxTmplIt}, + template::{ + builder::{AddingFees, BuilderState}, + Builder, OutputMeta, + }, + util::amountrange::{AmountF64, AmountU64}, + Context, +}; +use bitcoin::Amount; +pub use sapio_base::plugin_args::Ordinal; +pub use sapio_base::plugin_args::OrdinalsInfo; +use serde_derive::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet, VecDeque}; + +/// Struct for a payout plan +pub struct OrdinalSpec { + /// List of payout amounts + pub payouts: Vec, + /// Incoming Payments + // TODO: Optional Ordinal info? + pub payins: Vec, + /// Fees available + pub fees: Amount, + /// List of Ordinals + pub ordinals: BTreeSet, +} + +impl OrdinalSpec { + fn total(&self) -> Amount { + self.payin_sum() + + self.fees + + self.payouts.iter().fold(Amount::ZERO, |a, b| a + *b) + + self + .ordinals + .iter() + .map(|m| m.padding() + Amount::ONE_SAT) + .sum() + } + + fn payin_sum(&self) -> Amount { + self.payins.iter().fold(Amount::ZERO, |a, b| a + *b) + } +} + +/// Plan Step represents one step in a Builder Plan +#[derive(Eq, Ord, PartialEq, PartialOrd)] +pub enum PlanStep { + /// Must be Last, the amount of fees + Fee(Amount), + /// Change to send to a change addr + Change(Amount), + /// Payout to some value carrying contract + Payout(Amount), + /// Payout to an ordinal contract + Ordinal(Ordinal), + /// PayIn via a separate input + PayIn(Amount), +} +/// The steps to follow to construct a transaction +pub struct Plan(Vec); + +impl Plan { + /// Turn a plan into a txn... + pub fn build_plan( + self, + ctx: Context, + mut bs: BTreeMap)>>, + mut os: BTreeMap)>, + (change, change_meta): (&dyn Compilable, Option), + ) -> Result, CompilationError> { + let mut tmpl = ctx.template(); + for step in self.0 { + match step { + PlanStep::Fee(f) => { + return tmpl.add_fees(f.into()); + } + PlanStep::Change(amt) => { + tmpl = tmpl.add_output(amt.into(), change, change_meta.clone())?; + } + PlanStep::Payout(amt) => { + let vs = bs + .get_mut(&amt.into()) + .ok_or(CompilationError::OrdinalsError( + "No Place for payout".into(), + ))?; + let (contract, metadata) = vs.pop().ok_or(CompilationError::OrdinalsError( + "No Place for payout".into(), + ))?; + tmpl = tmpl.add_output(amt.into(), contract, metadata)?; + } + PlanStep::Ordinal(o) => { + tmpl = { + let (contract, metadata) = os + .remove(&o) + .ok_or(CompilationError::OrdinalsError("No Place for Ord".into()))?; + tmpl.add_output( + o.padding() + Amount::from_sat(1), + contract, + metadata.clone(), + )? + } + } + PlanStep::PayIn(a) => { + tmpl = tmpl.add_sequence().add_amount(a); + } + } + } + Err(CompilationError::OrdinalsError("Poorly Formed Plan".into())) + } +} + +/// Generates an Output Plan +pub trait OrdinalPlanner { + /// Computes the total sats in an OrdinalsInfo + fn total(&self) -> Amount; + /// Generates an Output Plan which details how Change/Payouts/Fees/Ordinals + fn output_plan(&self, spec: &OrdinalSpec) -> Result; +} +impl OrdinalPlanner for OrdinalsInfo { + /// Computes the total sats in an OrdinalsInfo + fn total(&self) -> Amount { + Amount::from_sat(self.0.iter().map(|(a, b)| b.0 - a.0).sum()) + } + /// Generates an Output Plan which details how Change/Payouts/Fees/Ordinals + fn output_plan(&self, spec: &OrdinalSpec) -> Result { + if self.total() < spec.total() { + return Err(CompilationError::OutOfFunds); + } else { + let mut payouts: VecDeque<_> = spec.payouts.iter().cloned().collect(); + { + let v = payouts.make_contiguous(); + v.sort(); + } + let info_master = { + let mut info = self + .0 + .iter() + .cloned() + .enumerate() + .map(|(a, b)| (b, a)) + .collect::>(); + info[..].sort(); + info + }; + let order = { + let mut order = vec![]; + let mut info = info_master.clone(); + let mut it = info.iter_mut().peekable(); + let mut info_idx = 0; + 'ordscan: for ord in spec.ordinals.iter() { + while info_idx < info.len() { + if *ord >= info[info_idx].0 .0 && *ord < info[info_idx].0 .1 { + order.push((info[info_idx].1, (*ord))); + // Add the padding here to prevent bugs + + let mut unfilled = ord.padding().as_sat() + 1; + while unfilled != 0 { + let ((lower, upper), _) = &mut info[info_idx]; + let available = upper.0 - lower.0; + lower.0 += std::cmp::min(available, unfilled); + unfilled = unfilled.saturating_sub(available); + if unfilled > 0 { + info_idx += 1; // advance the global index whenever exhausted + } + } + + continue 'ordscan; + } + info_idx += 1 + } + return Err(CompilationError::OrdinalsError("Ordinal Not Found".into())); + } + order[..].sort(); + order + }; + let mut info = info_master.clone(); + let mut info_idx = 0; + let mut base_instructions = vec![]; + for payin in &spec.payins { + base_instructions.push(PlanStep::PayIn(*payin)); + } + for (idx, ord) in order { + let mut total = 0; + + // Fast Forward to the required IDX and count how many sats + while info_idx < idx { + let ((lower, upper), _) = &mut info[info_idx]; + total += upper.0 - lower.0; + info_idx += 1; + } + + // if ord != lower, pull out those sats + let ((lower, upper), _) = &mut info[info_idx]; + total += ord.0 - lower.0; + + // Add a Payout covering the total... this is NP Hard? + // Greedily pick the largest and loop + // TODO: Solver? + greedy_assign_outs(total, &mut payouts, &mut base_instructions); + + // Advance lower bound past the end + + let mut unfilled = ord.padding().as_sat() + 1; + while unfilled != 0 { + let ((lower, upper), _) = &mut info[info_idx]; + let available = upper.0 - lower.0; + lower.0 += std::cmp::min(available, unfilled); + unfilled = unfilled.saturating_sub(available); + if unfilled > 0 { + info_idx += 1; // advance the global index whenever exhausted + } + } + + base_instructions.push(PlanStep::Ordinal(ord)); + } + let mut total = 0; + + // Fast Forward to the required IDX and count how many sats remain + while info_idx < info.len() { + let ((lower, upper), _) = &mut info[info_idx]; + total += upper.0 - lower.0; + info_idx += 1; + } + + total += spec.payin_sum().as_sat(); + + // Add a Payout covering the total + if total < spec.fees.as_sat() { + return Err(CompilationError::OrdinalsError( + "Failed to save enough for Fees".into(), + )); + } + + greedy_assign_outs(total, &mut payouts, &mut base_instructions); + if payouts.len() > 0 { + return Err(CompilationError::OrdinalsError( + "Failed to assign all payouts".into(), + )); + } + + // Reserve fees now! + total -= spec.fees.as_sat(); + // Whatever is left can become Change... + base_instructions.push(PlanStep::Change(Amount::from_sat(total))); + + base_instructions.push(PlanStep::Fee(spec.fees.into())); + + Ok(Plan(base_instructions)) + } + } +} + +fn greedy_assign_outs( + mut total: u64, + payouts: &mut VecDeque, + base_instructions: &mut Vec, +) { + while total > 0 { + match payouts.binary_search(&Amount::from_sat(total)) { + Ok(i) => { + base_instructions.push(PlanStep::Payout(Amount::from_sat(total))); + payouts.remove(i); + total = 0; + } + Err(i) => { + if i == 0 { + // Best we can do is make this change, total smaller than any payout + // Must be change and not fee because we still have ordinals + base_instructions.push(PlanStep::Change(Amount::from_sat(total))); + total = 0; + } + + let v = payouts.remove(i - 1).expect("Valid given not 0"); + base_instructions.push(PlanStep::Payout(v.into())); + total -= v.as_sat(); + } + } + } +}