diff --git a/cli/Cargo.toml b/cli/Cargo.toml index fb9ea842..fe99b048 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -39,10 +39,6 @@ package = "sapio-bitcoin" version = "0.28.0" features = ['use-serde', 'rand', 'base64'] -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'rand', 'use-schemars', 'serde'] [dependencies.sapio] path = "../sapio" diff --git a/cli/src/contracts/request.rs b/cli/src/contracts/request.rs index 43ced0b1..da41ac4d 100644 --- a/cli/src/contracts/request.rs +++ b/cli/src/contracts/request.rs @@ -23,7 +23,7 @@ use sapio_base::{ }; use sapio_wasm_plugin::{ host::{plugin_handle::ModuleLocator, PluginHandle, WasmPluginHandle}, - CreateArgs, API, + CreateArgs, API, OrdinalsInfo, }; use schemars::JsonSchema; use serde::*; @@ -73,6 +73,8 @@ pub struct Bind { pub outpoint: Option, pub use_txn: Option, pub compiled: Compiled, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub ordinals_info: Option } pub type BindReturn = Program; #[derive(Serialize, Deserialize, JsonSchema)] @@ -271,6 +273,7 @@ impl Bind { use_txn, compiled, outpoint, + ordinals_info } = self; let use_txn = use_txn .map(|buf| base64::decode(buf.as_bytes())) @@ -285,6 +288,7 @@ impl Bind { emulator.clone(), "mock".try_into()?, Arc::new(MapEffectDB::default()), + ordinals_info ); let mut tx = ctx .template() diff --git a/cli/src/main.rs b/cli/src/main.rs index 9287734c..bb46eda0 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -499,6 +499,7 @@ async fn bind_command( client_auth: bitcoincore_rpc_async::Auth, ) -> Result> { let use_mock = args.is_present("mock"); + let ordinals_info = None; let use_base64 = args.is_present("base64_psbt"); let outpoint: Option = args .value_of("outpoint") @@ -520,5 +521,6 @@ async fn bind_command( outpoint, use_txn, compiled, + ordinals_info })) } diff --git a/ctv_emulators/Cargo.toml b/ctv_emulators/Cargo.toml index f4aa3079..9bc6fb04 100644 --- a/ctv_emulators/Cargo.toml +++ b/ctv_emulators/Cargo.toml @@ -27,11 +27,6 @@ package = "sapio-bitcoin" version = "0.28.0-rc.3" features = ['use-serde', 'rand'] -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'rand', 'use-schemars', 'serde'] - [dependencies.sapio-base] path = "../sapio-base" version = "0.2.0" diff --git a/ctv_emulators/src/msgs.rs b/ctv_emulators/src/msgs.rs index 7330a065..d0ad784b 100644 --- a/ctv_emulators/src/msgs.rs +++ b/ctv_emulators/src/msgs.rs @@ -7,6 +7,7 @@ use super::*; use bitcoin::consensus::encode::{Decodable, Encodable}; use miniscript::serde; +use sapio_base::miniscript; use serde::de::Visitor; use serde::de::*; use serde::*; diff --git a/emulator-trait/Cargo.toml b/emulator-trait/Cargo.toml index 2b09670a..9d8f1fae 100644 --- a/emulator-trait/Cargo.toml +++ b/emulator-trait/Cargo.toml @@ -22,10 +22,6 @@ package = "sapio-bitcoin" version = "0.28.0" features = ['use-serde'] -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] [dependencies.sapio-base] path = "../sapio-base" diff --git a/examples/dcf_mining_pool/Cargo.toml b/examples/dcf_mining_pool/Cargo.toml index 063d793e..154c60bf 100644 --- a/examples/dcf_mining_pool/Cargo.toml +++ b/examples/dcf_mining_pool/Cargo.toml @@ -33,9 +33,3 @@ version = "0.2.0" [dependencies.sapio-ctv-emulator-trait] path = "../../emulator-trait" version = "0.2.0" - -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'rand', 'use-schemars', 'serde'] -optional = true diff --git a/examples/dcf_mining_pool/src/miner_payout.rs b/examples/dcf_mining_pool/src/miner_payout.rs index d4d93d34..0f380499 100644 --- a/examples/dcf_mining_pool/src/miner_payout.rs +++ b/examples/dcf_mining_pool/src/miner_payout.rs @@ -152,8 +152,7 @@ impl MiningPayout { for pay in v.iter() { builder = builder.add_output(pay.0, pay.1.as_compilable(), None)?; } - builder = builder.add_fees(self.fee_sats_per_tx)?; - return builder.into(); + return builder.add_fees(self.fee_sats_per_tx)?.into(); } else { let pay = Box::new(PayoutBundle { contracts: v, diff --git a/plugin-example/Cargo.toml b/plugin-example/Cargo.toml index 560354ad..c671bce9 100644 --- a/plugin-example/Cargo.toml +++ b/plugin-example/Cargo.toml @@ -16,4 +16,6 @@ members = ["treepay" , "nft-sale" , "nft-auction" , "clause-module" - , "clause-module-trampoline"] + , "clause-module-trampoline" + , "ordinal-example" + , "ordinal-inscription"] diff --git a/plugin-example/batching-trait/Cargo.toml b/plugin-example/batching-trait/Cargo.toml index 0c00a911..664397d1 100644 --- a/plugin-example/batching-trait/Cargo.toml +++ b/plugin-example/batching-trait/Cargo.toml @@ -35,11 +35,6 @@ version = "0.2.0" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true [dependencies.sapio-wasm-plugin] diff --git a/plugin-example/clause-module-trampoline/Cargo.toml b/plugin-example/clause-module-trampoline/Cargo.toml index 25c73d4e..fbea3609 100644 --- a/plugin-example/clause-module-trampoline/Cargo.toml +++ b/plugin-example/clause-module-trampoline/Cargo.toml @@ -47,11 +47,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/clause-module-trampoline/src/plugin.rs b/plugin-example/clause-module-trampoline/src/plugin.rs index d909564c..1cba6120 100644 --- a/plugin-example/clause-module-trampoline/src/plugin.rs +++ b/plugin-example/clause-module-trampoline/src/plugin.rs @@ -63,10 +63,11 @@ impl Callable for Wrapper { amount: ctx.funds(), network: ctx.network, effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(), + ordinals_info: ctx.get_ordinals().clone() }, arguments: self.g.clone(), }; - self.v.call(ctx.path(), &create_args) + self.v.clone().call(ctx.path(), &create_args) } } diff --git a/plugin-example/clause-module/Cargo.toml b/plugin-example/clause-module/Cargo.toml index e3b40be7..16ad4901 100644 --- a/plugin-example/clause-module/Cargo.toml +++ b/plugin-example/clause-module/Cargo.toml @@ -47,11 +47,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/coin_pool/Cargo.toml b/plugin-example/coin_pool/Cargo.toml index cac0ecd4..c70bd844 100644 --- a/plugin-example/coin_pool/Cargo.toml +++ b/plugin-example/coin_pool/Cargo.toml @@ -47,11 +47,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/fedpeg/Cargo.toml b/plugin-example/fedpeg/Cargo.toml index 2bd3e001..6c5acf5e 100644 --- a/plugin-example/fedpeg/Cargo.toml +++ b/plugin-example/fedpeg/Cargo.toml @@ -47,12 +47,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/hanukkiah/Cargo.toml b/plugin-example/hanukkiah/Cargo.toml index c002c96b..fc51c708 100644 --- a/plugin-example/hanukkiah/Cargo.toml +++ b/plugin-example/hanukkiah/Cargo.toml @@ -46,12 +46,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/helloworld/Cargo.toml b/plugin-example/helloworld/Cargo.toml index bfc86668..bb0bd33b 100644 --- a/plugin-example/helloworld/Cargo.toml +++ b/plugin-example/helloworld/Cargo.toml @@ -47,12 +47,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/jamesob-vault/Cargo.toml b/plugin-example/jamesob-vault/Cargo.toml index f0183c2e..927944ca 100644 --- a/plugin-example/jamesob-vault/Cargo.toml +++ b/plugin-example/jamesob-vault/Cargo.toml @@ -49,13 +49,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" - -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/nft-auction/Cargo.toml b/plugin-example/nft-auction/Cargo.toml index c653983c..7faf4096 100644 --- a/plugin-example/nft-auction/Cargo.toml +++ b/plugin-example/nft-auction/Cargo.toml @@ -48,12 +48,6 @@ version = "0.2.0" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/nft-auction/src/plugin.rs b/plugin-example/nft-auction/src/plugin.rs index 04b9cb12..4cd85b14 100644 --- a/plugin-example/nft-auction/src/plugin.rs +++ b/plugin-example/nft-auction/src/plugin.rs @@ -22,7 +22,6 @@ use schemars::*; use serde::*; use std::convert::TryFrom; - use std::sync::Arc; /// # Dutch Auction Data @@ -145,7 +144,7 @@ impl NFTDutchAuction { let amt = ctx.funds(); // first, let's get the module that should be used to 're-mint' this NFT // to the new owner - let minting_module = self + let mut minting_module = self .main .data .minting_module @@ -162,6 +161,7 @@ impl NFTDutchAuction { amount: ctx.funds(), network: ctx.network, effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(), + ordinals_info: ctx.get_ordinals().clone(), }, arguments: mint_impl::Versions::Mint_NFT_Trait_Version_0_1_0(mint_data), }; diff --git a/plugin-example/nft-sale/Cargo.toml b/plugin-example/nft-sale/Cargo.toml index 17ec3b78..c197d541 100644 --- a/plugin-example/nft-sale/Cargo.toml +++ b/plugin-example/nft-sale/Cargo.toml @@ -51,12 +51,6 @@ version = "0.2.0" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [dependencies.sapio-wasm-plugin] path = "../../plugins" diff --git a/plugin-example/nft-sale/src/plugin.rs b/plugin-example/nft-sale/src/plugin.rs index 2b4ef4fe..96782a7e 100644 --- a/plugin-example/nft-sale/src/plugin.rs +++ b/plugin-example/nft-sale/src/plugin.rs @@ -53,10 +53,10 @@ impl SimpleNFTSale { let amt = ctx.funds(); // first, let's get the module that should be used to 're-mint' this NFT // to the new owner - let minting_module = + let mut minting_module = self.0.data.minting_module.as_ref().ok_or_else(|| { CompilationError::TerminateWith("Must Provide Module Hash".into()) - })?; + })?.clone(); // let's make a copy of the old nft metadata.. let mut mint_data = self.0.data.clone(); // and change the owner to the buyer @@ -68,6 +68,7 @@ impl SimpleNFTSale { amount: ctx.funds(), network: ctx.network, effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(), + ordinals_info: ctx.get_ordinals().clone(), }, arguments: mint_impl::Versions::Mint_NFT_Trait_Version_0_1_0(mint_data), }; diff --git a/plugin-example/nft-trait/Cargo.toml b/plugin-example/nft-trait/Cargo.toml index 7bb68080..399633c4 100644 --- a/plugin-example/nft-trait/Cargo.toml +++ b/plugin-example/nft-trait/Cargo.toml @@ -15,7 +15,7 @@ serde_derive = "1.0" [dependencies.simp-pack] path = "../../simp-pack" -version = "0.1.0" +version = "0.2.4" [dependencies.schemars] version = "0.8.0" features = ['impl_json_schema'] @@ -45,9 +45,3 @@ version = "0.2.0" [dependencies.sapio-ctv-emulator-trait] path = "../../emulator-trait" version = "0.2.0" - -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true diff --git a/plugin-example/nft/Cargo.toml b/plugin-example/nft/Cargo.toml index 952a1c88..994148a2 100644 --- a/plugin-example/nft/Cargo.toml +++ b/plugin-example/nft/Cargo.toml @@ -49,12 +49,6 @@ version = "0.2.0" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [dependencies.sapio-wasm-plugin] path = "../../plugins" diff --git a/plugin-example/nft/src/plugin.rs b/plugin-example/nft/src/plugin.rs index 93127198..9e9aa4ad 100644 --- a/plugin-example/nft/src/plugin.rs +++ b/plugin-example/nft/src/plugin.rs @@ -72,7 +72,7 @@ impl SellableNFT for SimpleNFT { fn sell(self, mut ctx: Context, sale: Sell) { if let Sell::MakeSale { sale_info_partial, - which_sale, + mut which_sale, } = sale { let sale_info = sale_info_partial.fill(self.data.clone()); @@ -83,6 +83,7 @@ impl SellableNFT for SimpleNFT { amount: ctx.funds(), network: ctx.network, effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(), + ordinals_info: ctx.get_ordinals().clone(), }, arguments: sale_impl::Versions::NFT_Sale_Trait_Version_0_1_0(sale_info.clone()), }; diff --git a/plugin-example/op_return_chain/Cargo.toml b/plugin-example/op_return_chain/Cargo.toml index 0000adb4..34f5e278 100644 --- a/plugin-example/op_return_chain/Cargo.toml +++ b/plugin-example/op_return_chain/Cargo.toml @@ -47,12 +47,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/ordinal-example/Cargo.toml b/plugin-example/ordinal-example/Cargo.toml new file mode 100644 index 00000000..6c5d501a --- /dev/null +++ b/plugin-example/ordinal-example/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "sapio-wasm-ordinal-example" +version = "0.1.0" +license = "MPL-2.0" +authors = ["Jeremy Rubin "] +edition = "2021" +repository = "https://github.com/sapio-lang/sapio" +homepage = "https://sapio-lang.org" +description = "An Example Sapio Application" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] +path = "src/plugin.rs" +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[dependencies] +serde_json = "1.0" +serde = "1.0" +serde_derive = "1.0" + + +[dependencies.schemars] +version = "0.8.0" +features = ['impl_json_schema'] +[dependencies.bitcoin] +package = "sapio-bitcoin" +version = "0.28.0" +features = ['use-serde'] +[dependencies.sapio] +path = "../../sapio" +version = "0.2.0" + +[dependencies.batching-trait] +path = "../batching-trait" +version = "0.1.0" + +[dependencies.sapio-base] +path = "../../sapio-base" +version = "0.2.0" +[dependencies.sapio-contrib] +path = "../../sapio-contrib" +version = "0.2.0" + + +[dependencies.sapio-ctv-emulator-trait] +path = "../../emulator-trait" +version = "0.2.0" + + +[dependencies.sapio-wasm-plugin] +path = "../../plugins" +version = "0.2.0" +features = ["client"] + +[dependencies.sapio-wasm-nft-trait] +path = "../nft-trait" +version = "0.1.0" diff --git a/plugin-example/ordinal-example/README.md b/plugin-example/ordinal-example/README.md new file mode 100644 index 00000000..acabbf6d --- /dev/null +++ b/plugin-example/ordinal-example/README.md @@ -0,0 +1,6 @@ +# Sapio Vault Example + +This crate can be compiled with `wasm-pack build`. The `*.wasm` artifact will +be created in the `pkg` directory, not in `target`. + +Feel free to modify this code to experiment with creating your own Sapio plugins. diff --git a/plugin-example/ordinal-example/src/logo.png b/plugin-example/ordinal-example/src/logo.png new file mode 100644 index 00000000..ff1bcc1c Binary files /dev/null and b/plugin-example/ordinal-example/src/logo.png differ diff --git a/plugin-example/ordinal-example/src/plugin.rs b/plugin-example/ordinal-example/src/plugin.rs new file mode 100644 index 00000000..0c605a5a --- /dev/null +++ b/plugin-example/ordinal-example/src/plugin.rs @@ -0,0 +1,146 @@ +use std::collections::BTreeMap; + +use bitcoin::util::amount::Amount; +use bitcoin::XOnlyPublicKey; +use sapio::contract::empty; +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_plugin::*; +use schemars::*; +use serde::*; +/// # SimpleOrdinal +/// A really Ordinal Bearing Contract +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct SimpleOrdinal { + ordinal: u64, + #[schemars(with = "bitcoin::hashes::sha256::Hash")] + owner: XOnlyPublicKey, +} +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct Sell { + purchaser: bitcoin::Address, + amount: AmountF64, + change: AmountF64, + fee: AmountF64, +} + +#[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 ords = ctx + .get_ordinals() + .as_ref() + .ok_or_else(|| CompilationError::OrdinalsError("Missing Ordinals Info".into()))?; + let mut index = 0; + for (a, b) in ords.0.iter() { + if (*a..*b).contains(&Ordinal(self.ordinal)) { + index += self.ordinal - a.0; + break; + } else { + index += b.0 - a.0 + } + } + let mut t = ctx.template(); + // TODO: Check Index calculation + if index != 0 { + t = t.add_output(Amount::from_sat(index - 1), &self.owner, None)?; + } + let buyer = Compiled::from_address(sale.purchaser, None); + t = t.add_output(Amount::from_sat(501), &buyer, None)?; + let remaining = t.ctx().funds(); + t = t.add_amount(sale.amount.into()); + t = t.add_output(remaining + sale.amount.into(), &self.owner, None)?; + t = t.add_sequence(); + t = t.add_amount(sale.change.into()); + t = t.add_output(sale.change.into(), &buyer, None)?; + t = t.add_amount(sale.fee.into()); + t.add_fees(sale.fee.into())?.into() + } else { + empty() + } + } +} +impl StatefulArgumentsTrait for Sale {} + +/// # The SimpleNFT Contract +impl Contract for SimpleOrdinal { + // Ordinals... only good for selling? + 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.0.iter().any(|(a, b)| (*a..*b).contains(&Ordinal(self.ordinal))) { + Ok(Amount::from_sat(1 + 500)) + } else { + Err(CompilationError::OrdinalsError( + "Missing Intended Ordinal".into(), + )) + } + } +} + +impl SimpleOrdinal { + /// # signed + /// Get the current owners signature. + #[guard] + fn signed(self, _ctx: Context) { + Clause::Key(self.owner.clone()) + } +} +fn default_coerce( + k: ::StatefulArguments, +) -> Result { + Ok(k) +} + +REGISTER![SimpleOrdinal, "logo.png"]; diff --git a/plugin-example/ordinal-inscription/Cargo.toml b/plugin-example/ordinal-inscription/Cargo.toml new file mode 100644 index 00000000..5b1e4e82 --- /dev/null +++ b/plugin-example/ordinal-inscription/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "sapio-wasm-ordinal-inscription" +version = "0.1.0" +license = "MPL-2.0" +authors = ["Jeremy Rubin "] +edition = "2021" +repository = "https://github.com/sapio-lang/sapio" +homepage = "https://sapio-lang.org" +description = "An Example Sapio Application" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] +path = "src/plugin.rs" +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[dependencies] +serde_json = "1.0" +serde = "1.0" +serde_derive = "1.0" + + +[dependencies.schemars] +version = "0.8.0" +features = ['impl_json_schema'] +[dependencies.bitcoin] +package = "sapio-bitcoin" +version = "0.28.0" +features = ['use-serde'] +[dependencies.sapio] +path = "../../sapio" +version = "0.2.0" + +[dependencies.batching-trait] +path = "../batching-trait" +version = "0.1.0" + +[dependencies.sapio-base] +path = "../../sapio-base" +version = "0.2.0" +[dependencies.sapio-contrib] +path = "../../sapio-contrib" +version = "0.2.0" + + +[dependencies.sapio-ctv-emulator-trait] +path = "../../emulator-trait" +version = "0.2.0" + + +[dependencies.sapio-wasm-plugin] +path = "../../plugins" +version = "0.2.0" +features = ["client"] + +[dependencies.sapio-wasm-nft-trait] +path = "../nft-trait" +version = "0.1.0" diff --git a/plugin-example/ordinal-inscription/README.md b/plugin-example/ordinal-inscription/README.md new file mode 100644 index 00000000..acabbf6d --- /dev/null +++ b/plugin-example/ordinal-inscription/README.md @@ -0,0 +1,6 @@ +# Sapio Vault Example + +This crate can be compiled with `wasm-pack build`. The `*.wasm` artifact will +be created in the `pkg` directory, not in `target`. + +Feel free to modify this code to experiment with creating your own Sapio plugins. diff --git a/plugin-example/ordinal-inscription/src/logo.png b/plugin-example/ordinal-inscription/src/logo.png new file mode 100644 index 00000000..d3bd711d Binary files /dev/null and b/plugin-example/ordinal-inscription/src/logo.png differ diff --git a/plugin-example/ordinal-inscription/src/plugin.rs b/plugin-example/ordinal-inscription/src/plugin.rs new file mode 100644 index 00000000..bdd80c1d --- /dev/null +++ b/plugin-example/ordinal-inscription/src/plugin.rs @@ -0,0 +1,109 @@ +use bitcoin::util::amount::Amount; +use bitcoin::Address; +use bitcoin::XOnlyPublicKey; +use sapio::contract::CompilationError; +use sapio::contract::Compiled; +use sapio::contract::Contract; +use sapio::contract::StatefulArgumentsTrait; +use sapio::ordinals::OrdinalPlanner; +use sapio::util::amountrange::AmountU64; +use sapio::*; +use sapio_base::Clause; +use sapio_wasm_plugin::*; +use schemars::*; +use serde::*; +/// # SimpleInscription +/// A really Ordinal Bearing Contract +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct InscribingStep { + #[schemars(with = "bitcoin::hashes::sha256::Hash")] + owner: XOnlyPublicKey, + data: Vec, + content_type: String, +} +impl InscribingStep { + /// # signed + /// Get the current owners signature. + #[guard] + fn signed(self, _ctx: Context) { + Clause::Key(self.owner) + } + + #[guard] + fn inscription(self, _ctx: Context) { + let insc = sapio_base::miniscript::ord::Inscription::new( + Some(self.content_type.as_bytes().into()), + Some(self.data.clone()), + ); + Clause::Inscribe(Box::new(insc), Box::new(Clause::Trivial)) + } +} + +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct Reveal { + fee: AmountU64, + alternative: Option
, +} +impl Reveal { + fn parse( + k: ::StatefulArguments, + ) -> Result { + Ok(k) + } +} +impl Default for Reveal { + fn default() -> Self { + Self { + fee: Amount::from_sat(500).into(), + alternative: None, + } + } +} +// ASSUMES 500 sats after Ord are "dust" +impl InscribingStep { + #[continuation( + guarded_by = "[Self::signed, Self::inscription]", + web_api, + coerce_args = "Reveal::parse" + )] + fn reveal(self, ctx: Context, reveal: Reveal) { + let ord = ctx + .get_ordinals() + .as_ref() + .ok_or_else(|| CompilationError::OrdinalsError("Missing Ordinals Info".into()))? + .0[0] + .0; + let funds = ctx.funds(); + if funds < Amount::from(reveal.fee) + ord.padding() + Amount::ONE_SAT { + return Err(CompilationError::OutOfFunds); + } + let send_with = funds - reveal.fee.into(); + let tmpl = ctx.template(); + if let Some(address) = reveal.alternative { + tmpl.add_output(send_with, &Compiled::from_address(address, None), None) + } else { + tmpl.add_output(send_with, &self.owner, None) + }? + .add_fees(reveal.fee.into())? + .into() + } +} +impl StatefulArgumentsTrait for Reveal {} + +/// # The SimpleInscription Contract +impl Contract for InscribingStep { + declare! {updatable, Self::reveal} + + fn ensure_amount(&self, ctx: Context) -> Result { + // Optional if we want to require ordinal info provided -- we can + // happily track ordinals abstractly with some future patches. + let ords = ctx + .get_ordinals() + .as_ref() + .ok_or_else(|| CompilationError::OrdinalsError("Missing Ordinals Info".into()))?; + Ok(ords.total()) + } +} + + +REGISTER![InscribingStep, "logo.png"]; diff --git a/plugin-example/payment_pool/Cargo.toml b/plugin-example/payment_pool/Cargo.toml index 6abcf5d0..af025f4d 100644 --- a/plugin-example/payment_pool/Cargo.toml +++ b/plugin-example/payment_pool/Cargo.toml @@ -47,12 +47,6 @@ path = "src/payment_pool.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/staker/Cargo.toml b/plugin-example/staker/Cargo.toml index 5491eb1d..41a16c51 100644 --- a/plugin-example/staker/Cargo.toml +++ b/plugin-example/staker/Cargo.toml @@ -47,12 +47,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/trampolinepay/Cargo.toml b/plugin-example/trampolinepay/Cargo.toml index 1f977025..97db8c29 100644 --- a/plugin-example/trampolinepay/Cargo.toml +++ b/plugin-example/trampolinepay/Cargo.toml @@ -43,12 +43,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/trampolinepay/src/plugin.rs b/plugin-example/trampolinepay/src/plugin.rs index b9b776a9..6fa86260 100644 --- a/plugin-example/trampolinepay/src/plugin.rs +++ b/plugin-example/trampolinepay/src/plugin.rs @@ -34,13 +34,14 @@ pub struct TrampolinePay { impl TrampolinePay { #[then] fn expand(self, mut ctx: Context) { - let contract = self.handle.call( + let contract = self.handle.clone().call( &ctx.derive_str(Arc::new("plugin_trampoline".into()))?.path(), &CreateArgs { context: ContextualArguments { amount: ctx.funds(), network: ctx.network, effects: unsafe { ctx.get_effects_internal() }.as_ref().clone(), + ordinals_info: ctx.get_ordinals().clone(), }, arguments: batching_trait::Versions::BatchingTraitVersion0_1_1(self.data.clone()), }, diff --git a/plugin-example/treepay/Cargo.toml b/plugin-example/treepay/Cargo.toml index 23cac13b..5b4f9024 100644 --- a/plugin-example/treepay/Cargo.toml +++ b/plugin-example/treepay/Cargo.toml @@ -44,12 +44,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugin-example/treepay/src/plugin.rs b/plugin-example/treepay/src/plugin.rs index 696f5096..af199f4e 100644 --- a/plugin-example/treepay/src/plugin.rs +++ b/plugin-example/treepay/src/plugin.rs @@ -100,8 +100,7 @@ impl TreePay { builder = builder.set_sequence(0, timelock)?; } } - builder = builder.add_fees(self.fee_sats_per_tx)?; - return builder.into(); + return builder.add_fees(self.fee_sats_per_tx)?.into(); } else { let pay = Box::new(PayThese { contracts: v, diff --git a/plugin-example/vault/Cargo.toml b/plugin-example/vault/Cargo.toml index eff2d323..b2f2d19a 100644 --- a/plugin-example/vault/Cargo.toml +++ b/plugin-example/vault/Cargo.toml @@ -46,13 +46,6 @@ path = "src/plugin.rs" path = "../../emulator-trait" version = "0.2.0" - -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml index 27a41132..5ddeed4b 100644 --- a/plugins/Cargo.toml +++ b/plugins/Cargo.toml @@ -13,7 +13,7 @@ description = "Libarary for building client/host sapio plugin bindings" [features] default = ["client"] host = ["wasmer", "wasmer-cache", "tokio"] -client = ["miniscript"] +client = [] [dependencies] schemars = "0.8.0" @@ -60,11 +60,5 @@ path = "src/lib.rs" path = "../emulator-trait" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] -optional = true - [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/plugins/src/client/api/emulator.rs b/plugins/src/client/api/emulator.rs index 232394d2..826d87ab 100644 --- a/plugins/src/client/api/emulator.rs +++ b/plugins/src/client/api/emulator.rs @@ -7,6 +7,7 @@ //! Host based Emulator use crate::client::ext::*; use bitcoin::hashes::Hash; +use sapio_base::miniscript; use sapio_ctv_emulator_trait::CTVEmulator; use std::ffi::CString; use std::os::raw::c_char; diff --git a/plugins/src/client/plugin.rs b/plugins/src/client/plugin.rs index ebca721f..63915f47 100644 --- a/plugins/src/client/plugin.rs +++ b/plugins/src/client/plugin.rs @@ -73,6 +73,7 @@ where network, amount, effects, + ordinals_info }, } = serde_json::from_slice(s.to_bytes()).map_err(CompilationError::DeserializationError)?; // TODO: In theory, these trampoline bounds are robust/serialization safe... @@ -105,6 +106,7 @@ where path, // TODO: load database? Arc::new(effects), + ordinals_info ); let converted = Self::try_from(arguments)?; converted.call(ctx) diff --git a/sapio-base/Cargo.toml b/sapio-base/Cargo.toml index e8f75726..9480568e 100644 --- a/sapio-base/Cargo.toml +++ b/sapio-base/Cargo.toml @@ -19,7 +19,7 @@ serde_derive = "1.0" [dependencies.miniscript] package = "sapio-miniscript" -version = "^7.0.1" +version = "7.0.2-alpha.0" features = ['compiler', 'use-serde', 'use-schemars', 'serde'] [dependencies.bitcoin] diff --git a/sapio-base/src/lib.rs b/sapio-base/src/lib.rs index c1325baa..4563ba76 100644 --- a/sapio-base/src/lib.rs +++ b/sapio-base/src/lib.rs @@ -10,6 +10,7 @@ pub mod util; use bitcoin::XOnlyPublicKey; pub use util::CTVHash; +pub use miniscript; pub mod plugin_args; pub mod simp; diff --git a/sapio-base/src/plugin_args.rs b/sapio-base/src/plugin_args.rs index 92b4e21e..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}; @@ -48,4 +49,25 @@ pub struct ContextualArguments { /// # Effects to augment compilations with #[serde(skip_serializing_if = "MapEffectDB::skip_serializing", default)] pub effects: MapEffectDB, + + /// # the ranges of ordinals held in the input + #[serde(skip_serializing_if = "Option::is_none", default)] + 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-contrib/Cargo.toml b/sapio-contrib/Cargo.toml index 4ebe0c94..4749cfc3 100644 --- a/sapio-contrib/Cargo.toml +++ b/sapio-contrib/Cargo.toml @@ -33,12 +33,6 @@ path = "../sapio-base" version = "0.2.0" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] - - [dependencies.sapio-ctv-emulator-trait] path = "../emulator-trait" version = "0.2.0" diff --git a/sapio-contrib/src/contracts/channel.rs b/sapio-contrib/src/contracts/channel.rs index 4af2d939..709af1f3 100644 --- a/sapio-contrib/src/contracts/channel.rs +++ b/sapio-contrib/src/contracts/channel.rs @@ -75,6 +75,7 @@ mod tests { std::sync::Arc::new(CTVAvailable), "root".try_into().unwrap(), Default::default(), + None ); Compilable::compile(&x, ctx.derive_str(Arc::new("X".into())).unwrap()).ok(); Compilable::compile(&y, ctx.derive_str(Arc::new("Y".into())).unwrap()).ok(); diff --git a/sapio-contrib/src/contracts/derivatives/dlc.rs b/sapio-contrib/src/contracts/derivatives/dlc.rs index 3ce77b84..8a7c6a20 100644 --- a/sapio-contrib/src/contracts/derivatives/dlc.rs +++ b/sapio-contrib/src/contracts/derivatives/dlc.rs @@ -252,6 +252,7 @@ mod tests { Arc::new(CTVAvailable), EffectPath::try_from("dlc").unwrap(), Arc::new(Default::default()), + None ); let _r = d.compile(ctx).unwrap(); } diff --git a/sapio-contrib/src/contracts/hanukkah.rs b/sapio-contrib/src/contracts/hanukkah.rs index cc895510..b71727a9 100644 --- a/sapio-contrib/src/contracts/hanukkah.rs +++ b/sapio-contrib/src/contracts/hanukkah.rs @@ -151,11 +151,11 @@ impl Hanukkiah2Night { let size = txn.estimate_tx_size(); let fees = self.feerate_per_byte * size; txn = txn.add_amount(fees); - txn = txn.add_fees(fees)?; let candle_time = AbsTime::try_from(self.night_time.get() + 24 * 60 * 60 * (self.night as u32 - 1))? .into(); - txn.set_lock_time(candle_time)?.into() + txn = txn.set_lock_time(candle_time)?.into(); + txn.add_fees(fees)?.into() } } impl Hanukkiah2 { @@ -181,8 +181,8 @@ impl Hanukkiah2 { let size = txn.estimate_tx_size(); let fees = self.feerate_per_byte * size; txn = txn.add_amount(fees); - txn = txn.add_fees(fees)?; - txn.into() + let fee_paying_txn = txn.add_fees(fees)?; + fee_paying_txn.into() } } impl Contract for Hanukkiah2 { diff --git a/sapio-contrib/src/contracts/op_return_chain.rs b/sapio-contrib/src/contracts/op_return_chain.rs index 181f76d1..17f72843 100644 --- a/sapio-contrib/src/contracts/op_return_chain.rs +++ b/sapio-contrib/src/contracts/op_return_chain.rs @@ -43,8 +43,9 @@ impl ChainReturn { )] fn next_chain(self, ctx: sapio::Context, o: UpdateTypes) { let mut tmpl = ctx.template(); + let mut pay_fees = (Amount::ZERO); if let UpdateTypes::AddData { data, fees } = o { - tmpl = tmpl.spend_amount(fees.into())?; + pay_fees += fees.into(); tmpl = tmpl.add_output( Amount::from_sat(0), &Compiled::from_op_return(data.as_str().as_bytes())?, @@ -58,7 +59,8 @@ impl ChainReturn { let funds = tmpl.ctx().funds(); tmpl = tmpl.add_output(funds, &self.pk, None)?; } - tmpl.into() + + tmpl.add_fees(pay_fees.into())?.into() } } diff --git a/sapio-contrib/src/contracts/vault.rs b/sapio-contrib/src/contracts/vault.rs index 5a5ee867..1dae5002 100644 --- a/sapio-contrib/src/contracts/vault.rs +++ b/sapio-contrib/src/contracts/vault.rs @@ -217,6 +217,7 @@ mod test { Arc::new(CTVAvailable), EffectPath::try_from("dlc").unwrap(), Arc::new(v.context.effects), + None ); Vault::try_from(v.arguments)?.compile(ctx)?; Ok(()) diff --git a/sapio-psbt/Cargo.toml b/sapio-psbt/Cargo.toml index c44ca98b..8510d40b 100644 --- a/sapio-psbt/Cargo.toml +++ b/sapio-psbt/Cargo.toml @@ -21,7 +21,8 @@ package = "sapio-bitcoin" version = "0.28.0" features = ['use-serde', 'rand', 'base64'] + [dependencies.miniscript] package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'rand', 'use-schemars', 'serde'] +version = "7.0.2-alpha.0" +features = ['compiler', 'use-serde', 'use-schemars', 'serde'] \ No newline at end of file diff --git a/sapio/Cargo.toml b/sapio/Cargo.toml index 9fa30d20..756a1fd3 100644 --- a/sapio/Cargo.toml +++ b/sapio/Cargo.toml @@ -11,10 +11,6 @@ description = "A programming framework for bitcoin smart contracts." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.1" -features = ['compiler', 'use-serde', 'use-schemars', 'serde'] [features] # used to enable some niceties if compiling on a nightly compiler diff --git a/sapio/src/contract/abi/object/bind.rs b/sapio/src/contract/abi/object/bind.rs index 8a345c8e..66644ca6 100644 --- a/sapio/src/contract/abi/object/bind.rs +++ b/sapio/src/contract/abi/object/bind.rs @@ -6,30 +6,25 @@ //! binding Object to a specific UTXO use super::descriptors::*; - pub use crate::contract::abi::studio::*; use crate::contract::object::Object; use crate::contract::object::ObjectError; use crate::template::Template; - -use ::miniscript::*; - use bitcoin::hashes::sha256::Hash as Sha256; - use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin::util::taproot::TaprootBuilder; use bitcoin::util::taproot::TaprootSpendInfo; use bitcoin::OutPoint; - +use miniscript::*; use sapio_base::effects::EffectPath; - +use sapio_base::miniscript; use sapio_base::serialization_helpers::SArc; use sapio_base::txindex::TxIndex; use sapio_ctv_emulator_trait::CTVEmulator; - use std::collections::BTreeMap; use std::rc::Rc; use std::sync::Arc; + impl Object { /// bind_psbt attaches and `Object` to a specific UTXO, returning a /// Vector of PSBTs and transaction metadata. diff --git a/sapio/src/contract/abi/object/descriptors.rs b/sapio/src/contract/abi/object/descriptors.rs index c245daae..5898b479 100644 --- a/sapio/src/contract/abi/object/descriptors.rs +++ b/sapio/src/contract/abi/object/descriptors.rs @@ -7,13 +7,11 @@ //! Wrapper for supported descriptor types pub use crate::contract::abi::studio::*; - -use ::miniscript::*; - use bitcoin::PublicKey; use bitcoin::Script; use bitcoin::XOnlyPublicKey; - +use miniscript::*; +use sapio_base::miniscript; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/sapio/src/contract/abi/object/error.rs b/sapio/src/contract/abi/object/error.rs index cfce5d63..92b4058c 100644 --- a/sapio/src/contract/abi/object/error.rs +++ b/sapio/src/contract/abi/object/error.rs @@ -7,12 +7,9 @@ //! Errors during object construction pub use crate::contract::abi::studio::*; - -use ::miniscript::{self, *}; - use bitcoin::util::taproot::TaprootBuilderError; - -use sapio_base::txindex::TxIndexError; +use miniscript::*; +use sapio_base::{miniscript, txindex::TxIndexError}; use sapio_ctv_emulator_trait::EmulatorError; /// Error types that can arise when constructing an Object diff --git a/sapio/src/contract/abi/object/mod.rs b/sapio/src/contract/abi/object/mod.rs index be482877..8787fcb2 100644 --- a/sapio/src/contract/abi/object/mod.rs +++ b/sapio/src/contract/abi/object/mod.rs @@ -10,33 +10,28 @@ pub mod error; pub use error::*; pub mod bind; pub mod descriptors; -pub use descriptors::*; -use sapio_base::simp::CompiledObjectLT; -use sapio_base::simp::SIMPAttachableAt; -use sapio_base::Clause; -use serde_json::Value; - use crate::contract::abi::continuation::ContinuationPoint; - use crate::contract::CompilationError; use crate::template::Template; use crate::util::amountrange::AmountRange; use crate::util::extended_address::ExtendedAddress; -use ::miniscript::*; use bitcoin::hashes::sha256; - use bitcoin::util::amount::Amount; - +pub use descriptors::*; use sapio_base::effects::EffectPath; use sapio_base::effects::PathFragment; +use sapio_base::miniscript::*; use sapio_base::serialization_helpers::SArc; - +use sapio_base::simp::CompiledObjectLT; +use sapio_base::simp::SIMPAttachableAt; use sapio_base::simp::SIMPError; +use sapio_base::Clause; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::collections::BTreeMap; - use std::sync::Arc; + /// Metadata for Object, arbitrary KV set. #[derive(Serialize, Deserialize, Clone, JsonSchema, Debug, PartialEq, Eq, Default)] pub struct ObjectMetadata { diff --git a/sapio/src/contract/abi/studio/mod.rs b/sapio/src/contract/abi/studio/mod.rs index 5095bc87..807601ce 100644 --- a/sapio/src/contract/abi/studio/mod.rs +++ b/sapio/src/contract/abi/studio/mod.rs @@ -9,12 +9,12 @@ use crate::contract::abi::continuation::ContinuationPoint; use crate::contract::object::ObjectMetadata; use crate::template::output::OutputMeta; use crate::template::TemplateMetadata; -use ::miniscript::*; use bitcoin::consensus::serialize; use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin::OutPoint; -use sapio_base::effects::EffectPath; +use miniscript::*; use sapio_base::serialization_helpers::SArc; +use sapio_base::{effects::EffectPath, miniscript}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; diff --git a/sapio/src/contract/compiler/mod.rs b/sapio/src/contract/compiler/mod.rs index 49d8ea95..96f463b5 100644 --- a/sapio/src/contract/compiler/mod.rs +++ b/sapio/src/contract/compiler/mod.rs @@ -15,18 +15,16 @@ use crate::contract::actions::conditional_compile::CCILWrapper; use crate::contract::actions::CallableAsFoF; use crate::contract::TxTmplIt; use crate::util::amountrange::AmountRange; - -use ::miniscript::*; use bitcoin::schnorr::TweakedPublicKey; use bitcoin::XOnlyPublicKey; +use miniscript::*; use sapio_base::effects::EffectDB; use sapio_base::effects::EffectPath; use sapio_base::effects::PathFragment; - +use sapio_base::miniscript; use sapio_base::serialization_helpers::SArc; use sapio_base::Clause; use std::collections::{BTreeMap, BTreeSet}; - use std::sync::Arc; mod cache; mod util; diff --git a/sapio/src/contract/compiler/util.rs b/sapio/src/contract/compiler/util.rs index 41a96719..fd8c9f83 100644 --- a/sapio/src/contract/compiler/util.rs +++ b/sapio/src/contract/compiler/util.rs @@ -6,11 +6,12 @@ //! utility functions for compiler -use ::miniscript::descriptor::TapTree; -use ::miniscript::*; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::XOnlyPublicKey; +use miniscript::descriptor::TapTree; +use miniscript::*; +use sapio_base::miniscript; use std::cmp::Reverse; use std::collections::BinaryHeap; use std::sync::Arc; diff --git a/sapio/src/contract/context.rs b/sapio/src/contract/context.rs index 49089b14..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; @@ -19,6 +21,7 @@ use std::convert::TryInto; use std::collections::HashSet; +use std::mem; use std::sync::Arc; /// Context is used to track statet during compilation such as remaining value. @@ -32,9 +35,32 @@ pub struct Context { path: Arc, already_derived: HashSet, effects: Arc, + ordinals_info: Option, +} + +fn allocate_ordinals(a: Amount, ords: &OrdinalsInfo) -> [OrdinalsInfo; 2] { + let mut amt = a.as_sat(); + 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].0.push((start, end)) + } else { + if sats != 0 { + ret[0].0.push((start, Ordinal(start.0 + sats))); + } + ret[1].0.push((Ordinal(start.0 + sats), end)) + } + } + ret } impl Context { + /// Borrow the Ordinals Info + pub fn get_ordinals(&self) -> &Option { + &self.ordinals_info + } /// create a context instance. Should only happen *once* at the very top /// level. pub fn new( @@ -43,6 +69,7 @@ impl Context { emulator: Arc, path: EffectPath, effects: Arc, + ordinals_info: Option, ) -> Self { Context { available_funds, @@ -52,6 +79,7 @@ impl Context { path: Arc::new(path), already_derived: Default::default(), effects, + ordinals_info, } } /// Get this Context's effect database, for clients @@ -94,6 +122,7 @@ impl Context { network: self.network, already_derived: Default::default(), effects: self.effects.clone(), + ordinals_info: self.ordinals_info.clone(), }) } } @@ -107,6 +136,7 @@ impl Context { network: self.network, already_derived: self.already_derived.clone(), effects: self.effects.clone(), + ordinals_info: self.ordinals_info.clone(), } } @@ -141,6 +171,12 @@ impl Context { network: self.network, already_derived: self.already_derived.clone(), effects: self.effects.clone(), + ordinals_info: self.ordinals_info.as_ref().map(|o| { + let mut a = allocate_ordinals(amount, o); + let mut v = OrdinalsInfo(vec![]); + mem::swap(&mut a[0], &mut v); + v + }), }) } } @@ -150,6 +186,13 @@ impl Context { Err(CompilationError::OutOfFunds) } else { self.available_funds -= amount; + + self.ordinals_info = self.ordinals_info.as_ref().map(|o| { + let mut a = allocate_ordinals(amount, o); + let mut v = OrdinalsInfo(vec![]); + mem::swap(&mut a[1], &mut v); + v + }); Ok(self) } } diff --git a/sapio/src/contract/error.rs b/sapio/src/contract/error.rs index 08350ca2..63b99117 100644 --- a/sapio/src/contract/error.rs +++ b/sapio/src/contract/error.rs @@ -11,6 +11,7 @@ use crate::contract::object::ObjectError; use sapio_base::effects::EffectDBError; use sapio_base::effects::EffectPath; use sapio_base::effects::ValidFragmentError; +use sapio_base::miniscript; use sapio_base::plugin_args::CreateArgs; use sapio_base::simp::SIMPError; use sapio_ctv_emulator_trait::EmulatorError; @@ -100,6 +101,8 @@ pub enum CompilationError { ModuleFailedAPICheck(String), /// CompError ModuleCompilationErrorUnsendable(String), + /// Issue in the Ordinals System + OrdinalsError(String), /// Error while serializing SerializationError(serde_json::Error), /// Error while deserializing diff --git a/sapio/src/contract/macros.rs b/sapio/src/contract/macros.rs index b60f33ae..37ee9812 100644 --- a/sapio/src/contract/macros.rs +++ b/sapio/src/contract/macros.rs @@ -127,12 +127,12 @@ pub fn get_schema_for() -> Arc macro_rules! web_api { {$name:ident,$type:ty,{}} => { $crate::contract::macros::paste!{ - const [] : Option<&'static dyn Fn() -> std::sync::Arc<$crate::schemars::schema::RootSchema>> = Some(&|| $crate::contract::macros::get_schema_for::<$type>()); + const [] : Option<&'static dyn Fn() -> std::sync::Arc> = Some(&|| $crate::contract::macros::get_schema_for::<$type>()); } }; {$name:ident,$type:ty} => { $crate::contract::macros::paste!{ - const [] : Option<&'static dyn Fn() -> std::sync::Arc<$crate::schemars::schema::RootSchema>> = None; + const [] : Option<&'static dyn Fn() -> std::sync::Arc> = None; } } } diff --git a/sapio/src/lib.rs b/sapio/src/lib.rs index 78845392..c32a54fe 100644 --- a/sapio/src/lib.rs +++ b/sapio/src/lib.rs @@ -8,6 +8,7 @@ #![cfg_attr(feature = "nightly", feature(associated_type_defaults))] #![deny(missing_docs)] +pub use sapio_base::miniscript; #[macro_use] pub mod contract; pub mod template; @@ -17,3 +18,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(); + } + } + } +} diff --git a/sapio/src/template/builder.rs b/sapio/src/template/builder.rs index 3ea29f92..48d582c8 100644 --- a/sapio/src/template/builder.rs +++ b/sapio/src/template/builder.rs @@ -21,10 +21,15 @@ use sapio_base::timelocks::*; use sapio_base::CTVHash; use sapio_base::Clause; use std::convert::TryFrom; +use std::marker::PhantomData; +/// State Type Tag for NotAddingFees +pub struct NotAddingFees; +/// State Type Tag for AddingFees (forces fees to be added last) +pub struct AddingFees; /// Builder can be used to interactively put together a transaction template before /// finalizing into a Template. -pub struct Builder { +pub struct BuilderState { guards: Vec, // TODO: Should be Comitted/Uncomitted if not CTV sequences: Vec>, @@ -37,12 +42,16 @@ pub struct Builder { min_feerate: Option, // Metadata Fields: metadata: TemplateMetadata, + _pd: PhantomData, } -impl Builder { +/// Start state of a Builder +pub type Builder = BuilderState; + +impl BuilderState { /// Creates a new transaction template with 1 input and no outputs. - pub fn new(ctx: Context) -> Builder { - Builder { + pub fn new(ctx: Context) -> BuilderState { + Self { guards: Vec::new(), sequences: vec![None], inputs: vec![InputMetadata::default()], @@ -53,6 +62,7 @@ impl Builder { fees: Amount::from_sat(0), min_feerate: None, ctx, + _pd: Default::default(), } } @@ -61,19 +71,6 @@ impl Builder { &self.ctx } - /// reduce the amount availble in the builder's context - pub fn spend_amount(mut self, amount: Amount) -> Result { - self.ctx = self.ctx.spend_amount(amount)?; - Ok(self) - } - - /// reduce the amount availble in the builder's context, and add to the fees - pub fn add_fees(self, amount: Amount) -> Result { - let mut c = self.spend_amount(amount)?; - c.fees += amount; - Ok(c) - } - /// Creates a new Output, forcing the compilation of the compilable object and defaulting /// metadata if not provided to blank. pub fn add_output( @@ -223,6 +220,33 @@ impl Builder { self.guards.push(guard); self } +} + +impl BuilderState { + /// reduce the amount availble in the builder's context + pub fn spend_amount(mut self, amount: Amount) -> Result { + self.ctx = self.ctx.spend_amount(amount)?; + Ok(self) + } + /// reduce the amount availble in the builder's context, and add to the fees + pub fn add_fees(self, amount: Amount) -> Result, CompilationError> { + let mut s = BuilderState { + _pd: Default::default(), + guards: self.guards, + sequences: self.sequences, + outputs: self.outputs, + inputs: self.inputs, + version: self.version, + lock_time: self.lock_time, + ctx: self.ctx, + fees: self.fees, + min_feerate: self.min_feerate, + metadata: self.metadata, + }; + let mut c = s.spend_amount(amount)?; + c.fees += amount; + Ok(c) + } /// Creates a transaction from a Builder. /// Generally, should not be called directly. @@ -320,8 +344,9 @@ impl Builder { } } } -impl From for Template { - fn from(t: Builder) -> Template { + +impl From> for Template { + fn from(t: BuilderState) -> Template { let tx = t.get_tx(); Template { guards: t.guards, @@ -337,8 +362,8 @@ impl From for Template { } } -impl From for crate::contract::TxTmplIt { - fn from(t: Builder) -> Self { +impl From> for crate::contract::TxTmplIt { + fn from(t: BuilderState) -> Self { // t.into() // works too, but prefer the explicit form so we know what we get concretely Ok(Box::new(std::iter::once(Ok(t.into())))) } diff --git a/sapio/src/util/extended_address.rs b/sapio/src/util/extended_address.rs index ea69aa05..8808e4d9 100644 --- a/sapio/src/util/extended_address.rs +++ b/sapio/src/util/extended_address.rs @@ -10,7 +10,8 @@ use crate::contract::object::ObjectError; use bitcoin::{Address, Script, XOnlyPublicKey}; -use miniscript::{Descriptor, DescriptorTrait}; +use crate::miniscript::{Descriptor, DescriptorTrait}; +use sapio_base::miniscript; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 00f1f13f..81373e37 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -16,11 +16,6 @@ serde_derive = "1.0" tokio = { version = "1", features = ["full"] } bitcoincore-rpc-async = "4.0.1-alpha.2" -[dependencies.miniscript] -package = "sapio-miniscript" -version = "^7.0.0" -features = ['compiler', 'use-serde', 'rand', 'use-schemars', 'serde'] - [dependencies.bitcoin] package = "sapio-bitcoin" version = "0.28.0"