diff --git a/Cargo.toml b/Cargo.toml index 72d47458..f177dfc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["sapio", "sapio-ws", "sapio-front", "sapio-contrib", "ctv_emulators", "sapio-base", "cli", "tools", "plugins", 'emulator-trait', 'examples/dcf_mining_pool'] +members = ["sapio", "sapio-ws", "sapio-front", "sapio-contrib", "ctv_emulators", "sapio-base", "cli", "tools", "plugins", 'emulator-trait', 'examples/dcf_mining_pool', 'sapio-trait'] exclude = ["plugin-example", "integration_tests"] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3eab34ad..2522bed6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -15,7 +15,7 @@ schemars = "0.8.0" serde_json = "1.0" serde = "1.0" serde_derive = "1.0" -clap = "3.0.0-beta.2" +clap = "=3.0.0-beta.2" base64 = "0.13.0" lazy_static = "1.4.0" bitcoincore-rpc-async = "3.0.1" diff --git a/cli/src/main.rs b/cli/src/main.rs index 3ade3367..c77c44ee 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -46,9 +46,10 @@ async fn main() -> Result<(), Box> { (version: "0.1.0 Beta") (author: "Jeremy Rubin ") (about: "Sapio CLI for Bitcoin Smart Contracts") - (@arg config: -c --config +takes_value #{1,2} {check_file} "Sets a custom config file") + (@arg config: -c --config +takes_value #{1,1} {check_file} "Sets a custom config file") (@arg debug: -d ... "Sets the level of debugging information") (@subcommand emulator => + (@setting SubcommandRequiredElseHelp) (about: "Make Requests to Emulator Servers") (@subcommand sign => (about: "Sign a PSBT") @@ -71,6 +72,7 @@ async fn main() -> Result<(), Box> { ) ) (@subcommand contract => + (@setting SubcommandRequiredElseHelp) (about: "Create or Manage a Contract") (@subcommand bind => (about: "Bind Contract to a specific UTXO") diff --git a/plugin-example/Cargo.toml b/plugin-example/Cargo.toml index 510a4a45..4d374488 100644 --- a/plugin-example/Cargo.toml +++ b/plugin-example/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["treepay"] +members = ["treepay", "trampolinepay", "batching-trait"] diff --git a/plugin-example/batching-trait/Cargo.toml b/plugin-example/batching-trait/Cargo.toml new file mode 100644 index 00000000..8e04da8e --- /dev/null +++ b/plugin-example/batching-trait/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "batching-trait" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +schemars = "0.8.0" +serde_json = "1.0" +serde = "1.0" +serde_derive = "1.0" + + +[dependencies.wasm-bindgen] +version = "0.2.69" + +[dependencies.bitcoin] +package = "sapio-bitcoin" +version = "^0.26.0" +features = ['use-serde', 'rand'] +[dependencies.sapio] +path = "../../sapio" +version = "0.1.0" + +[dependencies.sapio-base] +path = "../../sapio-base" +version = "0.1.0" + + +[dependencies.sapio-trait] +path="../../sapio-trait" +version = "0.1.0" + +[dependencies.sapio-ctv-emulator-trait] +path="../../emulator-trait" +version = "0.1.0" + +[dependencies.miniscript] +package = "sapio-miniscript" +version = "^5.1.0" +features = ['compiler', 'use-serde', 'rand', 'use-schemars', 'serde'] +optional = true + diff --git a/plugin-example/batching-trait/src/lib.rs b/plugin-example/batching-trait/src/lib.rs new file mode 100644 index 00000000..55b7df64 --- /dev/null +++ b/plugin-example/batching-trait/src/lib.rs @@ -0,0 +1,40 @@ +use sapio::contract::*; +use sapio::util::amountrange::*; +use sapio::*; +use sapio_trait::SapioJSONTrait; +use schemars::*; +use serde::*; +use serde_json::Value; +use std::collections::VecDeque; + +/// A payment to a specific address +#[derive(JsonSchema, Serialize, Deserialize, Clone)] +pub struct Payment { + /// The amount to send + #[serde(with = "bitcoin::util::amount::serde::as_btc")] + #[schemars(with = "f64")] + pub amount: bitcoin::util::amount::Amount, + /// # Address + /// The Address to send to + pub address: bitcoin::Address, +} +#[derive(Serialize, JsonSchema, Deserialize, Clone)] +pub struct BatchingTraitVersion0_1_1 { + pub payments: Vec, + #[serde(with = "bitcoin::util::amount::serde::as_sat")] + #[schemars(with = "u64")] + pub feerate_per_byte: bitcoin::util::amount::Amount, +} + +impl SapioJSONTrait for BatchingTraitVersion0_1_1 { + fn get_example_for_api_checking() -> Value { + #[derive(Serialize)] + enum Versions { + BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1), + } + serde_json::to_value(Versions::BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1 { + payments: vec![], + feerate_per_byte: bitcoin::util::amount::Amount::from_sat(0), + })).unwrap() + } +} diff --git a/plugin-example/trampolinepay/Cargo.toml b/plugin-example/trampolinepay/Cargo.toml new file mode 100644 index 00000000..9c63e649 --- /dev/null +++ b/plugin-example/trampolinepay/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "trampolinepay" +version = "0.1.0" +license = "MPL-2.0" +authors = ["Jeremy Rubin "] +edition = "2018" +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 + + + +[dependencies] +schemars = "0.8.0" +serde_json = "1.0" +serde = "1.0" +serde_derive = "1.0" + +[dependencies.bitcoin] +package = "sapio-bitcoin" +version = "^0.26.0" +features = ['use-serde', 'rand'] +[dependencies.sapio] +path = "../../sapio" +version = "0.1.0" + +[dependencies.batching-trait] +path = "../batching-trait" +version = "0.1.0" + +[dependencies.sapio-base] +path = "../../sapio-base" +version = "0.1.0" +[lib] +crate-type = ["cdylib", "rlib"] +path = "src/plugin.rs" + + + +[dependencies.sapio-ctv-emulator-trait] +path="../../emulator-trait" +version = "0.1.0" + +[dependencies.miniscript] +package = "sapio-miniscript" +version = "^5.1.0" +features = ['compiler', 'use-serde', 'rand', 'use-schemars', 'serde'] +optional = true + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[dependencies.sapio-wasm-plugin] +path = "../../plugins" +version = "0.1.0" +features = ["client"] + diff --git a/plugin-example/trampolinepay/README.md b/plugin-example/trampolinepay/README.md new file mode 100644 index 00000000..ebe5bcac --- /dev/null +++ b/plugin-example/trampolinepay/README.md @@ -0,0 +1,8 @@ +# Trampoline Pay + +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. + +Trampoline Pay demonstrates using the Traits API. \ No newline at end of file diff --git a/plugin-example/trampolinepay/src/logo.png b/plugin-example/trampolinepay/src/logo.png new file mode 100644 index 00000000..1c6fe79e Binary files /dev/null and b/plugin-example/trampolinepay/src/logo.png differ diff --git a/plugin-example/trampolinepay/src/plugin.rs b/plugin-example/trampolinepay/src/plugin.rs new file mode 100644 index 00000000..09656441 --- /dev/null +++ b/plugin-example/trampolinepay/src/plugin.rs @@ -0,0 +1,63 @@ +// 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/. + +#[deny(missing_docs)] +use batching_trait::BatchingTraitVersion0_1_1; +use bitcoin::util::amount::Amount; +use sapio::contract::*; +use sapio::util::amountrange::*; +use sapio::*; +use sapio_wasm_plugin::client::*; +use sapio_wasm_plugin::*; +use schemars::*; +use serde::*; +use std::collections::VecDeque; + +/// Documentation placed here will be visible to users! +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct TrampolinePay { + /// # Which Plugin to Use + /// Specify which contract plugin to call out to. + handle: SapioHostAPI, + /// # Data for the Contract + // Just do this to get the data... not always necessary (could be computed any way) + data: BatchingTraitVersion0_1_1, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +enum Versions { + /// # Batching Trait API + BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1), +} +impl TrampolinePay { + then! { + fn expand(self, ctx) { + let compiled = create_contract_by_key( + &self.handle.key, + serde_json::to_value(CreateArgs { + amount: ctx.funds(), + network: ctx.network, + arguments: Versions::BatchingTraitVersion0_1_1(self.data.clone()), + }) + .map_err(|_| CompilationError::TerminateCompilation)?, + Amount::from_sat(0), + ); + if let Some(contract) = compiled { + let mut builder = ctx.template(); + builder = builder.add_output(contract.amount_range.max(), &contract, None)?; + builder.into() + } else { + Err(CompilationError::TerminateCompilation) + } + } + } +} +impl Contract for TrampolinePay { + declare! {then, Self::expand} + declare! {non updatable} +} + +REGISTER![TrampolinePay, "logo.png"]; diff --git a/plugin-example/treepay/Cargo.toml b/plugin-example/treepay/Cargo.toml index 8f837a47..8d717148 100644 --- a/plugin-example/treepay/Cargo.toml +++ b/plugin-example/treepay/Cargo.toml @@ -29,6 +29,10 @@ features = ['use-serde', 'rand'] path = "../../sapio" version = "0.1.0" +[dependencies.batching-trait] +path = "../batching-trait" +version = "0.1.0" + [dependencies.sapio-base] path = "../../sapio-base" version = "0.1.0" diff --git a/plugin-example/treepay/LOG b/plugin-example/treepay/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/plugin-example/treepay/src/logo.png b/plugin-example/treepay/src/logo.png index 1d5bcec9..8163c93e 100644 Binary files a/plugin-example/treepay/src/logo.png and b/plugin-example/treepay/src/logo.png differ diff --git a/plugin-example/treepay/src/plugin.rs b/plugin-example/treepay/src/plugin.rs index cbd04b6e..16ffef43 100644 --- a/plugin-example/treepay/src/plugin.rs +++ b/plugin-example/treepay/src/plugin.rs @@ -4,6 +4,7 @@ // 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/. +use batching_trait::{BatchingTraitVersion0_1_1, Payment}; #[deny(missing_docs)] use sapio::contract::*; use sapio::util::amountrange::*; @@ -14,17 +15,6 @@ use schemars::*; use serde::*; use std::collections::VecDeque; -/// A payment to a specific address -#[derive(JsonSchema, Serialize, Deserialize, Clone)] -pub struct Payment { - /// The amount to send - #[serde(with = "bitcoin::util::amount::serde::as_btc")] - #[schemars(with = "f64")] - pub amount: bitcoin::util::amount::Amount, - /// # Address - /// The Address to send to - pub address: bitcoin::Address, -} /// Documentation placed here will be visible to users! #[derive(JsonSchema, Serialize, Deserialize)] pub struct TreePay { @@ -100,17 +90,34 @@ impl Contract for TreePay { } #[derive(Serialize, Deserialize, JsonSchema)] -enum Versions{ - Basic(TreePay), - Advanced(TreePay, - /// # A Ranomd Field For Example - u8), +enum Versions { + /// # Tree Pay + TreePay(TreePay), + /// # Advanced Tree Pay + Advanced( + TreePay, + /// # A Random Field For Example + u8, + ), + /// # Batching Trait API + BatchingTraitVersion0_1_1(BatchingTraitVersion0_1_1), +} +impl From for TreePay { + fn from(args: BatchingTraitVersion0_1_1) -> Self { + TreePay { + participants: args.payments, + radix: 4, + // estimate fees to be 4 outputs and 1 input + change + fee_sats_per_tx: args.feerate_per_byte * ((4 * 41) + 41 + 10), + } + } } impl From for TreePay { - fn from(v:Versions) -> TreePay { + fn from(v: Versions) -> TreePay { match v { - Versions::Basic(v) => v, + Versions::TreePay(v) => v, Versions::Advanced(v, _) => v, + Versions::BatchingTraitVersion0_1_1(v) => v.into(), } } } diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml index 4c6d42a9..f82f8b7a 100644 --- a/plugins/Cargo.toml +++ b/plugins/Cargo.toml @@ -21,11 +21,16 @@ serde_json = "1.0" serde = "1.0" serde_derive = "1.0" base64 = "0.13.0" +hex = "0.4.3" [dependencies.wasm-bindgen] version = "0.2.69" optional = true +[dependencies.sapio-trait] +version = "0.1.0" +path = "../sapio-trait" + [dependencies.directories] version = "3.0.1" optional = true diff --git a/plugins/src/client/api.rs b/plugins/src/client/api.rs index 8596ab0d..989ccdb0 100644 --- a/plugins/src/client/api.rs +++ b/plugins/src/client/api.rs @@ -7,6 +7,9 @@ ///! Wraps the external API with friendly methods use super::*; use bitcoin::Amount; +use core::convert::TryFrom; +use sapio_trait::SapioJSONTrait; +use std::marker::PhantomData; /// Print a &str to the parent's console. pub fn log(s: &str) { unsafe { @@ -53,6 +56,78 @@ pub fn lookup_module_name(key: &str) -> Option<[u8; 32]> { } } +#[derive(Serialize, Deserialize, JsonSchema)] +/// # Lookup Parameters +/// - either using a hash key (exact); or +/// - name (user configured) +pub enum LookupFrom { + /// # Provide the Hex Encoded Hash of the WASM Module + HashKey(String), + /// # Give a Configurable Name + Name(String), +} +impl LookupFrom { + fn to_key(&self) -> Option<[u8; 32]> { + match self { + LookupFrom::HashKey(hash) => { + let mut r = [0u8; 32]; + hex::decode_to_slice(hash, &mut r).ok()?; + Some(r) + } + LookupFrom::Name(name) => lookup_module_name(name), + } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(try_from = "SapioHostAPIVerifier")] +pub struct SapioHostAPI { + pub which_plugin: LookupFrom, + #[serde(skip, default)] + pub key: [u8; 32], + #[serde(skip, default)] + pub api: serde_json::Value, + #[serde(default, skip)] + _pd: PhantomData, +} + +use std::error::Error; +#[derive(Serialize, Deserialize, JsonSchema)] +/// # Helper for Serialization... +struct SapioHostAPIVerifier { + which_plugin: LookupFrom, + #[serde(default, skip)] + _pd: PhantomData, +} +impl TryFrom> for SapioHostAPI { + type Error = Box; + fn try_from(shapv: SapioHostAPIVerifier) -> Result, Box> { + let SapioHostAPIVerifier { which_plugin, _pd } = shapv; + let key = match which_plugin.to_key() { + Some(key) => key, + _ => { + return Err("Key Not Found".into()); + } + }; + let p = key.as_ptr() as i32; + let api = unsafe { + let api_buf = sapio_v1_wasm_plugin_get_api(p); + if api_buf == 0 { + return Err("API Pointer Null".into()); + } + let cs = { CString::from_raw(api_buf as *mut c_char) }; + serde_json::from_slice(cs.as_bytes())? + }; + T::check_trait_implemented_inner(&api)?; + Ok(SapioHostAPI { + which_plugin, + key, + api, + _pd, + }) + } +} + /// Given a human readable name, create a new contract instance pub fn create_contract(key: &str, args: Value, amt: Amount) -> Option { let key = lookup_module_name(key)?; diff --git a/plugins/src/client/ext.rs b/plugins/src/client/ext.rs index 3bb4045d..d68d28b0 100644 --- a/plugins/src/client/ext.rs +++ b/plugins/src/client/ext.rs @@ -21,8 +21,16 @@ extern "C" { json_len: i32, amt: u32, ) -> i32; + /// Get contract API by "trampolining" through the host to use another + /// plugin identified by key. + pub fn sapio_v1_wasm_plugin_get_api(key: i32) -> i32; /// lookup a plugin key from a human reable name. /// if ok == 1, result is valid. /// out is written and must be 32 bytes of writable memory. pub fn sapio_v1_wasm_plugin_lookup_module_name(name: i32, name_len: i32, out: i32, ok: i32); } + +#[no_mangle] +fn now() -> f64 { + 0.0 +} diff --git a/plugins/src/host/mod.rs b/plugins/src/host/mod.rs index 9d4185a3..ee566644 100644 --- a/plugins/src/host/mod.rs +++ b/plugins/src/host/mod.rs @@ -97,6 +97,11 @@ mod exports { ); } + /// Create an instance of a contract by "trampolining" through the host to use another + /// plugin identified by key. + pub fn sapio_v1_wasm_plugin_get_api(env: &HostEnvironment, key: i32) -> i32 { + wasm_plugin_action(env, key, Action::GetAPI) + } /// Create an instance of a contract by "trampolining" through the host to use another /// plugin identified by key. pub fn sapio_v1_wasm_plugin_create_contract( @@ -106,6 +111,22 @@ mod exports { json_len: i32, amt: u32, ) -> i32 { + wasm_plugin_action( + env, + key, + Action::Create { + json, + json_len, + amt, + }, + ) + } + enum Action { + Create { json: i32, json_len: i32, amt: u32 }, + GetAPI, + } + + fn wasm_plugin_action(env: &HostEnvironment, key: i32, action: Action) -> i32 { let env = env.lock().unwrap(); const KEY_LEN: usize = 32; let key = key as usize; @@ -121,43 +142,60 @@ mod exports { buf }) .to_string(); - - let mut v = vec![0u8; json_len as usize]; - for (src, dst) in env.memory_ref().unwrap().view() - [json as usize..(json + json_len) as usize] - .iter() - .map(Cell::get) - .zip(v.iter_mut()) - { - *dst = src; - } + let v = match action { + Action::GetAPI => None, + Action::Create { json, json_len, .. } => { + let mut v = vec![0u8; json_len as usize]; + for (src, dst) in env.memory_ref().unwrap().view() + [json as usize..(json + json_len) as usize] + .iter() + .map(Cell::get) + .zip(v.iter_mut()) + { + *dst = src; + } + Some(serde_json::from_str( + &String::from_utf8_lossy(&v).to_owned().to_string(), + )) + } + }; let emulator = env.emulator.clone(); let mmap = env.module_map.clone(); let typ = env.typ.clone(); let org = env.org.clone(); let proj = env.proj.clone(); let net = env.net; - let (tx, mut rx) = tokio::sync::oneshot::channel::(); + let (tx_json, mut rx_json) = + tokio::sync::oneshot::channel::>(); let handle = tokio::runtime::Handle::current(); handle.spawn(async move { - if let Ok(create_args) = - serde_json::from_str(&String::from_utf8_lossy(&v).to_owned().to_string()) - { + let plugin = WasmPluginHandle::new(typ, org, proj, &emulator, Some(&h), None, net, Some(mmap)) .await - .ok() - .and_then(|sph| sph.create(&create_args).ok()) - .map(|comp| tx.send(comp)); + .ok(); + match action { + Action::GetAPI => { + plugin + .and_then(|sph| sph.get_api().ok()) + .map(|api| tx_json.send(Ok(api))); + } + Action::Create { .. } => { + if let Some(Ok(create_args)) = v { + plugin + .and_then(|sph| sph.create(&create_args).ok()) + .map(|comp| tx_json.send(serde_json::to_value(comp))); + } + } } }); tokio::task::block_in_place(|| loop { - match rx.try_recv() { - Ok(comp) => { + match rx_json.try_recv() { + Ok(value) => { return (move || -> Result> { - let comp_s = serde_json::to_string(&comp)?; + let comp_s = serde_json::to_string(&value?)?; let bytes: i32 = env .allocate_wasm_bytes_ref() .unwrap() diff --git a/plugins/src/host/plugin_handle/wasm.rs b/plugins/src/host/plugin_handle/wasm.rs index 137587f3..00235450 100644 --- a/plugins/src/host/plugin_handle/wasm.rs +++ b/plugins/src/host/plugin_handle/wasm.rs @@ -126,6 +126,7 @@ impl WasmPluginHandle { sapio_v1_wasm_plugin_ctv_emulator_sign, sapio_v1_wasm_plugin_debug_log_string, sapio_v1_wasm_plugin_create_contract, + sapio_v1_wasm_plugin_get_api, sapio_v1_wasm_plugin_lookup_module_name ); diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs index 5a438e15..8d504657 100644 --- a/plugins/src/lib.rs +++ b/plugins/src/lib.rs @@ -21,39 +21,10 @@ where serde_json::from_str(&s).map_err(serde::de::Error::custom) } -#[derive(Serialize, Deserialize, JsonSchema)] -#[serde(remote = "bitcoin::Network")] -pub enum NetworkDef { - /// Classic Bitcoin - Bitcoin, - /// Bitcoin's testnet - Testnet, - /// Bitcoin's signet - Signet, - /// Bitcoin's regtest - Regtest, -} - -// We use json_wrapped_string to encode S to allow for a client to pass in -// CreateArgs without knowing the underlying type S. - -/// # Arguments For Creating this Contract -/// Provide this information to create an instance of a contract -#[derive(Serialize, Deserialize, JsonSchema)] -pub struct CreateArgs { - /// # The Main Contract Arguments - pub arguments: S, - #[serde(with = "NetworkDef")] - /// # The Network the contract should be created for. - pub network: bitcoin::Network, - #[serde(with = "bitcoin::util::amount::serde::as_btc")] - #[schemars(with = "f64")] - /// # The Amount of Funds Available to the Contract as Bitcoin. - pub amount: bitcoin::util::amount::Amount, -} - #[cfg(feature = "host")] pub mod host; #[cfg(feature = "client")] pub mod client; + +pub use sapio_base::plugin_args::*; diff --git a/sapio-base/src/lib.rs b/sapio-base/src/lib.rs index 6034e11e..6e366861 100644 --- a/sapio-base/src/lib.rs +++ b/sapio-base/src/lib.rs @@ -8,6 +8,7 @@ /// Extra functionality for working with Bitcoin types pub mod util; pub use util::CTVHash; +pub mod plugin_args; /// Helpers for making correct time locks pub mod timelocks; diff --git a/sapio-base/src/plugin_args.rs b/sapio-base/src/plugin_args.rs new file mode 100644 index 00000000..29bf5a5d --- /dev/null +++ b/sapio-base/src/plugin_args.rs @@ -0,0 +1,29 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Deserializer, Serialize}; +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(remote = "bitcoin::Network")] +pub enum NetworkDef { + /// Classic Bitcoin + Bitcoin, + /// Bitcoin's testnet + Testnet, + /// Bitcoin's signet + Signet, + /// Bitcoin's regtest + Regtest, +} + +/// # Arguments For Creating this Contract +/// Provide this information to create an instance of a contract +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct CreateArgs { + /// # The Main Contract Arguments + pub arguments: S, + #[serde(with = "NetworkDef")] + /// # The Network the contract should be created for. + pub network: bitcoin::Network, + #[serde(with = "bitcoin::util::amount::serde::as_btc")] + #[schemars(with = "f64")] + /// # The Amount of Funds Available to the Contract as Bitcoin. + pub amount: bitcoin::util::amount::Amount, +} diff --git a/sapio-trait/Cargo.toml b/sapio-trait/Cargo.toml new file mode 100644 index 00000000..06b4f31c --- /dev/null +++ b/sapio-trait/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "sapio-trait" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +schemars = "0.8.0" +serde_json = "1.0" +serde = "1.0" +serde_derive = "1.0" +[dependencies.jsonschema] +version = "0.12" +default-features = false + +[dependencies.bitcoin] +package = "sapio-bitcoin" +version = "^0.26.0" +features = ['use-serde', 'rand'] + +[dependencies.sapio-base] +path = "../sapio-base" +version = "0.1.0" diff --git a/sapio-trait/src/lib.rs b/sapio-trait/src/lib.rs new file mode 100644 index 00000000..08b11e4e --- /dev/null +++ b/sapio-trait/src/lib.rs @@ -0,0 +1,40 @@ +use bitcoin::*; +use jsonschema::JSONSchema; +use sapio_base::plugin_args::CreateArgs; +use schemars::*; +use serde::*; +use serde_json::Value; +use std::error::Error; +pub trait SapioAPIHandle { + fn get_api(&self) -> serde_json::Value; +} +impl SapioAPIHandle for serde_json::Value { + fn get_api(&self) -> Self { + self.clone() + } +} +pub trait SapioJSONTrait: JsonSchema + Serialize + for<'a> Deserialize<'a> { + fn get_example_for_api_checking() -> Value; + fn check_trait_implemented_inner(api: &dyn SapioAPIHandle) -> Result<(), Box> { + let tag = Self::get_example_for_api_checking(); + let japi = api.get_api(); + let compiled = JSONSchema::compile(&japi).map_err(|_| "Error Compiling Schema")?; + compiled + .validate(&serde_json::to_value(CreateArgs { + arguments: tag, + amount: Amount::from_sat(0), + network: Network::Bitcoin, + })?) + .map_err(|e| { + let mut s = String::from("Validation Errors:"); + for error in e { + s += &format!("\n - {}", error.to_string()); + } + s + })?; + Ok(()) + } + fn check_trait_implemented(api: &dyn SapioAPIHandle) -> bool { + Self::check_trait_implemented_inner(api).is_ok() + } +}