diff --git a/.gitignore b/.gitignore index 52ad2a16..9c8b08d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Dependency directories node_modules/ +crates/warp-contracts/target # Optional eslint cache .eslintcache diff --git a/crates/warp-contracts/Cargo.lock b/crates/warp-contracts/Cargo.lock new file mode 100644 index 00000000..d27e081b --- /dev/null +++ b/crates/warp-contracts/Cargo.lock @@ -0,0 +1,214 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "warp-contracts" +version = "0.1.0" +dependencies = [ + "js-sys", + "serde", + "serde-wasm-bindgen", + "warp-contracts-core", + "warp-contracts-macro", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "warp-contracts-core" +version = "0.1.0" +dependencies = [ + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "warp-contracts-macro" +version = "0.1.0" +dependencies = [ + "quote", + "syn", + "warp-contracts-core", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "web-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +dependencies = [ + "js-sys", + "wasm-bindgen", +] diff --git a/crates/warp-contracts/Cargo.toml b/crates/warp-contracts/Cargo.toml new file mode 100644 index 00000000..e6fec770 --- /dev/null +++ b/crates/warp-contracts/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "warp-contracts" +version = "0.1.1" +edition = "2021" +description = "Warp WASM contract utils for rust contracts" +license = "MIT" +documentation = "https://academy.warp.cc/docs/sdk/advanced/wasm" +homepage = "https://warp.cc" +repository = "https://github.com/warp-contracts/warp" +keywords = ["warp", "smart-contract", "SmartWeave", "web3"] +categories = ["api-bindings", "development-tools::ffi", "finance", "wasm"] + +[dependencies] +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +js-sys = { workspace = true } +serde = { workspace = true } +serde-wasm-bindgen = { workspace = true } +warp-contracts-macro = { version = "0.1.1", path = "warp-contracts-macro" } +warp-contracts-core = { version = "0.1.1", path = "warp-contracts-core" } + +[features] +debug = ["warp-contracts-core/debug"] + +[workspace] +members = ["warp-contracts-macro", "warp-contracts-core"] + +[workspace.dependencies] +wasm-bindgen = "=0.2.84" +wasm-bindgen-futures = { version = "=0.4.34" } +js-sys = "=0.3.61" +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "=0.5.0" +web-sys = { version = "=0.3.61" } diff --git a/crates/warp-contracts/README.md b/crates/warp-contracts/README.md new file mode 100644 index 00000000..e5ac2b55 --- /dev/null +++ b/crates/warp-contracts/README.md @@ -0,0 +1,23 @@ +# Warp contracts + +`warp-contracts` is an inherent part of [Warp SDK](https://github.com/warp-contracts/warp). This library allows for smooth integration with Warp implementation of SmartWeave protocol. + +| Feature | Yes/No | +| ---------------------- | ----------- | +| Sandboxing | ✅ | +| Foreign contract read | ✅ | +| Foreign contract view | ✅ | +| Foreign contract write | ✅ | +| Arweave.utils | Soon | +| Evolve | ✅ | +| Logging from contract | ✅ | +| Transaction data (1) | ✅ | +| Block data (2) | ✅ | +| Contract data (3) | ✅ | +| Gas metering | ✅ | + +Legend: +- `Soon` - the technology is already there, we just need to find some time to add the missing features :-) +- `(1)` - access current transaction data (id, owner, etc.) +- `(2)` - access current block data (indep_hash, height, timestamp) +- `(3)` - access current contract data (id, owner) diff --git a/crates/warp-contracts/src/README.md b/crates/warp-contracts/src/README.md new file mode 100644 index 00000000..dadde3be --- /dev/null +++ b/crates/warp-contracts/src/README.md @@ -0,0 +1,4 @@ +# contract_utils module + +This is a module with boilerplate code for each SmartWeave RUST contract. +**Please don't modify it unless you 100% know what you are doing!** diff --git a/crates/warp-contracts/src/foreign_call.rs b/crates/warp-contracts/src/foreign_call.rs new file mode 100644 index 00000000..83a05fcb --- /dev/null +++ b/crates/warp-contracts/src/foreign_call.rs @@ -0,0 +1,65 @@ +use super::js_imports::SmartWeave; +use serde::de::DeserializeOwned; +use serde::Serialize; +use serde_wasm_bindgen::from_value; +use core::fmt::Debug; +use warp_contracts_core::{ + handler_result::{ViewResult, WriteResult}, + methods::to_json_value, + warp_result::{transmission::from_json, WarpResult}, +}; + +pub async fn read_foreign_contract_state( + contract_address: &str, +) -> Result { + match SmartWeave::read_contract_state(contract_address).await { + Ok(s) => match from_value::(s) { + Ok(v) => Ok(v), + Err(e) => Err(format!("{e:?}")), + }, + Err(e) => Err(format!("{e:?}")), + } +} + +pub async fn view_foreign_contract_state< + V: DeserializeOwned + Debug, + I: Serialize, + E: DeserializeOwned + Debug, +>( + contract_address: &str, + input: I, +) -> ViewResult { + let input = match to_json_value(&input) { + Ok(v) => v, + Err(e) => return ViewResult::RuntimeError(format!("{e:?}")), + }; + match SmartWeave::view_contract_state(contract_address, input).await { + Ok(s) => match from_json::(s) { + WarpResult::WriteSuccess() => { + ViewResult::RuntimeError("got WriteResponse for view call".to_owned()) + } + v => v.into(), + }, + Err(e) => ViewResult::RuntimeError(format!("{e:?}")), + } +} + +pub async fn write_foreign_contract( + contract_address: &str, + input: I, +) -> WriteResult<(), E> { + let input = match to_json_value(&input) { + Ok(v) => v, + Err(e) => return WriteResult::RuntimeError(format!("{e:?}")), + }; + let write_result = SmartWeave::write(contract_address, input).await; + match write_result { + Ok(s) => match from_json::<(), E>(s) { + WarpResult::ViewSuccess(_) => { + WriteResult::RuntimeError("got ViewResponse for write call".to_owned()) + } + v => v.into(), + }, + Err(e) => WriteResult::RuntimeError(format!("{e:?}")), + } +} diff --git a/crates/warp-contracts/src/js_imports.rs b/crates/warp-contracts/src/js_imports.rs new file mode 100644 index 00000000..f1c379b2 --- /dev/null +++ b/crates/warp-contracts/src/js_imports.rs @@ -0,0 +1,96 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen] + pub type Block; + + #[wasm_bindgen(static_method_of = Block, js_name = indep_hash)] + pub fn indep_hash() -> String; + + #[wasm_bindgen(static_method_of = Block, js_name = height)] + pub fn height() -> i32; + + #[wasm_bindgen(static_method_of = Block, js_name = timestamp)] + pub fn timestamp() -> i32; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen] + pub type Contract; + + #[wasm_bindgen(static_method_of = Contract, js_name = contractId)] + pub fn id() -> String; + + #[wasm_bindgen(static_method_of = Contract, js_name = contractOwner)] + pub fn owner() -> String; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen] + pub type Transaction; + + #[wasm_bindgen(static_method_of = Transaction, js_name = id)] + pub fn id() -> String; + + #[wasm_bindgen(static_method_of = Transaction, js_name = owner)] + pub fn owner() -> String; + + #[wasm_bindgen(static_method_of = Transaction, js_name = target)] + pub fn target() -> String; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen] + pub type KV; + + #[wasm_bindgen(catch, static_method_of = KV, js_name = kvGet)] + pub async fn get(key: &str) -> Result; + + #[wasm_bindgen(catch, static_method_of = KV, js_name = kvPut)] + pub async fn put(key: &str, value: JsValue) -> Result<(), JsValue>; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen] + pub type SmartWeave; + + #[wasm_bindgen(catch, static_method_of = SmartWeave, js_name = readContractState)] + pub async fn read_contract_state(contract_id: &str) -> Result; + + #[wasm_bindgen(catch, static_method_of = SmartWeave, js_name = viewContractState)] + pub async fn view_contract_state(contract_id: &str, input: JsValue) + -> Result; + + #[wasm_bindgen(catch, static_method_of = SmartWeave, js_name = write)] + pub async fn write(contract_id: &str, input: JsValue) -> Result; + + #[wasm_bindgen(static_method_of = SmartWeave, js_name = refreshState)] + pub async fn refresh_state(); + + #[wasm_bindgen(static_method_of = SmartWeave, js_name = caller)] + pub fn caller() -> String; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen] + pub type Vrf; + + #[wasm_bindgen(static_method_of = Vrf, js_name = value)] + pub fn value() -> String; + + #[wasm_bindgen(static_method_of = Vrf, js_name = randomInt)] + pub fn randomInt(max_value: i32) -> i32; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + pub fn log(s: &str); +} diff --git a/crates/warp-contracts/src/kv_operations.rs b/crates/warp-contracts/src/kv_operations.rs new file mode 100644 index 00000000..63b3592e --- /dev/null +++ b/crates/warp-contracts/src/kv_operations.rs @@ -0,0 +1,31 @@ +use super::js_imports::KV; +use serde::de::DeserializeOwned; +use serde::Serialize; +use serde_wasm_bindgen::from_value; +use warp_contracts_core::{handler_result::ViewResult, methods::to_json_value}; + +#[derive(Debug)] +pub enum KvError { + NotFound, +} + +pub async fn kv_get(key: &str) -> ViewResult { + match KV::get(key).await { + Ok(a) if !a.is_null() => match from_value(a) { + Ok(v) => ViewResult::Success(v), + Err(e) => ViewResult::RuntimeError(format!("{e:?}")), + }, + Ok(_) => ViewResult::ContractError(KvError::NotFound), + Err(e) => ViewResult::RuntimeError(format!("{e:?}")), + } +} + +pub async fn kv_put(key: &str, value: T) -> Result<(), String> { + match to_json_value(&value) { + Ok(v) => match KV::put(key, v).await { + Ok(_) => Ok(()), + Err(e) => Err(format!("{e:?}")), + }, + Err(e) => Err(format!("{:?}", e)), + } +} diff --git a/crates/warp-contracts/src/lib.rs b/crates/warp-contracts/src/lib.rs new file mode 100644 index 00000000..bcef513d --- /dev/null +++ b/crates/warp-contracts/src/lib.rs @@ -0,0 +1,8 @@ +pub mod foreign_call; +pub mod js_imports; +pub mod kv_operations; +pub use warp_contracts_core::handler_result; +pub use warp_contracts_core::methods; +pub use warp_contracts_core::optional_cell; +pub use warp_contracts_core::warp_result; +pub use warp_contracts_macro::warp_contract; diff --git a/crates/warp-contracts/warp-contracts-core/Cargo.toml b/crates/warp-contracts/warp-contracts-core/Cargo.toml new file mode 100644 index 00000000..79d7899a --- /dev/null +++ b/crates/warp-contracts/warp-contracts-core/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "warp-contracts-core" +version = "0.1.1" +edition = "2021" +description = "warp-contracts helper crate" +license = "MIT" +documentation = "https://academy.warp.cc/docs/sdk/advanced/wasm" +homepage = "https://warp.cc" +repository = "https://github.com/warp-contracts/warp" +keywords = ["warp", "smart-contract", "SmartWeave", "web3"] +categories = ["api-bindings", "development-tools::ffi", "finance", "wasm"] + +[dependencies] +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +serde = { workspace = true } +serde-wasm-bindgen = { workspace = true } +web-sys = { workspace = true, optional = true } + +[features] +debug = ["web-sys/console"] diff --git a/crates/warp-contracts/warp-contracts-core/README.md b/crates/warp-contracts/warp-contracts-core/README.md new file mode 100644 index 00000000..5f200356 --- /dev/null +++ b/crates/warp-contracts/warp-contracts-core/README.md @@ -0,0 +1,4 @@ +# warp-contract-core + +Helper library for warp-contracts and warp-contracts-macro. See the `warp-contracts` documentation for more details. + diff --git a/crates/warp-contracts/warp-contracts-core/src/detail.rs b/crates/warp-contracts/warp-contracts-core/src/detail.rs new file mode 100644 index 00000000..d778d976 --- /dev/null +++ b/crates/warp-contracts/warp-contracts-core/src/detail.rs @@ -0,0 +1,20 @@ +// What we try to achieve here is setting common lifetime for State and Future objects +// but using higher-ranked trait bounds (https://doc.rust-lang.org/reference/trait-bounds.html#higher-ranked-trait-bounds), +// i.e. impossible to specify on the call side (see use of this trait). +// The effect is somewhat opposite to 'static lifetime, the lifetime shorter than anything passed by the user. +// Inspired by https://stackoverflow.com/a/63558160/3021277 +pub trait BorrowingFn<'a, S, A, V> { + type Fut: core::future::Future + 'a; + fn call(self, state: &'a S, action: A) -> Self::Fut; +} + +impl<'a, Fu: 'a, F, S: 'a, A, V> BorrowingFn<'a, S, A, V> for F +where + F: FnOnce(&'a S, A) -> Fu, + Fu: core::future::Future + 'a, +{ + type Fut = Fu; + fn call(self, state: &'a S, action: A) -> Fu { + self(state, action) + } +} diff --git a/crates/warp-contracts/warp-contracts-core/src/handler_result.rs b/crates/warp-contracts/warp-contracts-core/src/handler_result.rs new file mode 100644 index 00000000..039f7aaa --- /dev/null +++ b/crates/warp-contracts/warp-contracts-core/src/handler_result.rs @@ -0,0 +1,13 @@ +#[derive(Debug)] +pub enum ViewResult { + Success(View), + ContractError(Error), + RuntimeError(String), +} + +#[derive(Debug)] +pub enum WriteResult { + Success(State), + ContractError(Error), + RuntimeError(String), +} diff --git a/crates/warp-contracts/warp-contracts-core/src/lib.rs b/crates/warp-contracts/warp-contracts-core/src/lib.rs new file mode 100644 index 00000000..8f9364bb --- /dev/null +++ b/crates/warp-contracts/warp-contracts-core/src/lib.rs @@ -0,0 +1,5 @@ +mod detail; +pub mod handler_result; +pub mod methods; +pub mod optional_cell; +pub mod warp_result; diff --git a/crates/warp-contracts/warp-contracts-core/src/methods.rs b/crates/warp-contracts/warp-contracts-core/src/methods.rs new file mode 100644 index 00000000..d6083b7f --- /dev/null +++ b/crates/warp-contracts/warp-contracts-core/src/methods.rs @@ -0,0 +1,185 @@ +use crate::{ + detail::BorrowingFn, + handler_result::{ViewResult, WriteResult}, + optional_cell::{CloneContents, OptionalCell}, + warp_result::transmission::Transmission, + warp_result::WarpResult, +}; +use core::fmt::Debug; +use core::future::Future; +use serde::{de::DeserializeOwned, Serialize}; +use serde_wasm_bindgen::from_value; +use wasm_bindgen::JsValue; + +pub async fn write_async( + state: &OptionalCell, + interaction: JsValue, + write_contract_method: Fun, +) -> JsValue +where + State: Clone + Debug + Serialize, + Action: DeserializeOwned + Debug, + Error: Serialize, + Fun: FnOnce(State, Action) -> Fut, + Fut: Future>, +{ + let result = match parse_input(state, interaction) { + Err(value) => return value, + Ok(action) => write_contract_method(state.clone_contents(), action).await, + }; + + map_write_result(result, state) +} + +pub fn write_sync( + s: &OptionalCell, + interaction: JsValue, + write_contract_method: Fun, +) -> JsValue +where + State: Clone + Debug + Serialize, + Action: DeserializeOwned + Debug, + Error: Serialize, + Fun: FnOnce(State, Action) -> WriteResult, +{ + let result = match parse_input(s, interaction) { + Err(value) => return value, + Ok(action) => write_contract_method(s.clone_contents(), action), + }; + + map_write_result(result, s) +} + +// We need a dedicated trait BorrowingFn to attach the same lifetime +// that is not possible to specify by the caller of the view_async to both &State and Future +pub async fn view_async( + s: &OptionalCell, + interaction: JsValue, + view_contract_method: Fun, +) -> JsValue +where + State: Clone, + Action: DeserializeOwned + core::fmt::Debug, + View: Serialize + Debug, + Error: Serialize + Debug, + Fun: for<'a> BorrowingFn<'a, State, Action, ViewResult>, +{ + let result = match parse_input(s, interaction) { + Err(value) => return value, + Ok(action) => { + view_contract_method + .call(s.cell.borrow().as_ref().unwrap(), action) + .await + } + }; + + to_json_value::>(&WarpResult::from(result).into()).unwrap() +} + +pub fn view_sync( + s: &OptionalCell, + interaction: JsValue, + view_contract_method: Fun, +) -> JsValue +where + State: Clone, + Action: DeserializeOwned + Debug, + View: Serialize, + Error: Serialize, + Fun: FnOnce(&State, Action) -> ViewResult, +{ + let result = match parse_input(s, interaction) { + Err(value) => return value, + Ok(action) => view_contract_method(s.cell.borrow().as_ref().unwrap(), action), + }; + + to_json_value::>(&WarpResult::from(result).into()).unwrap() +} + +fn map_write_result( + result: WriteResult, + state: &OptionalCell, +) -> JsValue +where + State: Clone + Debug + Serialize, + Error: Serialize, +{ + if let WriteResult::Success(new_state) = result { + state.cell.replace(Some(new_state)); + to_json_value::>(&WarpResult::WriteSuccess().into()).unwrap() + } else { + to_json_value::>(&WarpResult::from(result).into()).unwrap() + } +} + +fn parse_input( + state: &OptionalCell, + interaction: JsValue, +) -> Result +where + Action: DeserializeOwned + core::fmt::Debug, + State: Clone, +{ + let action = from_value(interaction); + if action.is_err() { + return Err(runtime_error(format!( + "Error while parsing input {}", + action.unwrap_err() + ))); + } + if state.is_empty() { + return Err(runtime_error(format!( + "initState MUST be called before interaction can take place" + ))); + } + + Ok(action.unwrap()) +} + +pub fn init_state( + state: &OptionalCell, + init_state: &JsValue, +) -> Option { + match from_value(init_state.clone()) { + Ok(parsed_state) => { + state.cell.replace(Some(parsed_state)); + None + } + Err(e) => { + let ret = format!("failed to parse init state {:?}", e); + #[cfg(feature = "debug")] + { + web_sys::console::log_1(&JsValue::from_str(&ret)); + } + Option::from(ret) + } + } +} + +pub fn current_state(state: &OptionalCell) -> JsValue { + // not sure if that's deterministic - which is very important for the execution network. + // TODO: perf - according to docs: + // "This is unlikely to be super speedy so it's not recommended for large payload" + // - we should minimize calls to serde_wasm_bindgen::to_json_value + if state.is_empty() { + runtime_error( + "contract state not initialized. please run initState method first".to_owned(), + ) + } else { + match to_json_value(state.cell.borrow().as_ref().unwrap()) { + Ok(v) => v, + Err(e) => runtime_error(format!("failed to serialize return value {:?}", e)), + } + } +} + +pub fn to_json_value( + value: &T, +) -> core::result::Result { + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + value.serialize(&serializer) +} + +pub fn runtime_error(error_message: String) -> JsValue { + to_json_value::>(&WarpResult::RuntimeError(error_message).into()).unwrap() +} diff --git a/crates/warp-contracts/warp-contracts-core/src/optional_cell.rs b/crates/warp-contracts/warp-contracts-core/src/optional_cell.rs new file mode 100644 index 00000000..336b59b5 --- /dev/null +++ b/crates/warp-contracts/warp-contracts-core/src/optional_cell.rs @@ -0,0 +1,35 @@ +use core::cell::RefCell; + +/// Tiny wrapper over RefCell that: +/// - allow for uninitialized values (by storing Option) +/// - allow static usage (by implementing Sync and providing const constructor) +/// +/// Because WASM is single threaded, we don't need to worry much about inter-thread communication +pub struct OptionalCell { + pub cell: RefCell>, +} + +impl OptionalCell { + pub const fn empty() -> OptionalCell { + OptionalCell { + cell: RefCell::new(None), + } + } + pub fn is_empty(&self) -> bool { + self.cell.borrow().is_none() + } +} + +// Add clone_content to OptionalCell only if T is known to implement Clone. +// In particular to to use is_empty on OptionalCell trait Clone is not required on T. +pub trait CloneContents { + fn clone_contents(&self) -> T; +} + +impl CloneContents for OptionalCell { + fn clone_contents(&self) -> T { + self.cell.borrow().as_ref().unwrap().clone() + } +} + +unsafe impl Sync for OptionalCell {} diff --git a/crates/warp-contracts/warp-contracts-core/src/warp_result.rs b/crates/warp-contracts/warp-contracts-core/src/warp_result.rs new file mode 100644 index 00000000..2be1b8a8 --- /dev/null +++ b/crates/warp-contracts/warp-contracts-core/src/warp_result.rs @@ -0,0 +1,130 @@ +use crate::handler_result::{ViewResult, WriteResult}; + +pub enum WarpResult { + WriteSuccess(), + ViewSuccess(View), + ContractError(Error), + RuntimeError(String), +} + +impl From> for WarpResult { + fn from(value: ViewResult) -> Self { + match value { + ViewResult::Success(v) => WarpResult::ViewSuccess(v), + ViewResult::ContractError(e) => WarpResult::ContractError(e), + ViewResult::RuntimeError(e) => WarpResult::RuntimeError(e), + } + } +} + +impl From> for WarpResult { + fn from(value: WriteResult) -> Self { + match value { + WriteResult::Success(_) => WarpResult::WriteSuccess(), + WriteResult::ContractError(e) => WarpResult::ContractError(e), + WriteResult::RuntimeError(e) => WarpResult::RuntimeError(e), + } + } +} + +impl From> for ViewResult { + fn from(value: WarpResult) -> Self { + match value { + WarpResult::WriteSuccess() => unreachable!(), + WarpResult::ViewSuccess(v) => ViewResult::Success(v), + WarpResult::ContractError(e) => ViewResult::ContractError(e), + WarpResult::RuntimeError(e) => ViewResult::RuntimeError(e), + } + } +} + +impl From> for WriteResult<(), E> { + fn from(value: WarpResult) -> Self { + match value { + WarpResult::WriteSuccess() => WriteResult::Success(()), + WarpResult::ViewSuccess(_) => unreachable!(), + WarpResult::ContractError(e) => WriteResult::ContractError(e), + WarpResult::RuntimeError(e) => WriteResult::RuntimeError(e), + } + } +} + +// Module defining format as expected by SDK, covers (de)serialization. +// We have this separated from WarpResult above (visible to contract code) to +// keep WarpResult interface clean +pub mod transmission { + use super::WarpResult; + use core::fmt::Debug; + use serde::{de::DeserializeOwned, Deserialize, Serialize}; + use wasm_bindgen::JsValue; + + #[derive(Serialize, Deserialize)] + pub struct ErrorResult { + #[serde(rename = "errorMessage")] + error_message: String, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct Transmission { + #[serde(rename = "type")] + result_type: String, + result: Option, + error: Option, + #[serde(rename = "errorMessage")] + error_message: Option, + } + + impl From> for WarpResult { + fn from(value: Transmission) -> Self { + match value.result_type.as_str() { + "ok" if value.result.is_none() => WarpResult::WriteSuccess(), + "ok" => WarpResult::ViewSuccess(value.result.unwrap()), + "error" if value.error.is_some() => WarpResult::ContractError(value.error.unwrap()), + "exception" if value.error_message.is_some() => { + WarpResult::RuntimeError(value.error_message.unwrap()) + } + _ => WarpResult::RuntimeError(format!("failed to parse response {:?}", value)), + } + } + } + + impl From> for Transmission { + fn from(value: WarpResult) -> Self { + let mut res = Transmission { + result_type: "".to_owned(), + result: None, + error: None, + error_message: None, + }; + match value { + WarpResult::WriteSuccess() => { + res.result_type = "ok".to_owned(); + } + WarpResult::ViewSuccess(v) => { + res.result_type = "ok".to_owned(); + res.result = Some(v); + } + WarpResult::ContractError(e) => { + res.result_type = "error".to_owned(); + res.error = Some(e); + } + WarpResult::RuntimeError(e) => { + res.result_type = "exception".to_owned(); + res.error_message = Some(e); + } + }; + res + } + } + + pub fn from_json(warp_result: JsValue) -> WarpResult + where + V: DeserializeOwned, + E: DeserializeOwned, + { + match serde_wasm_bindgen::from_value::>(warp_result) { + Ok(t) => t.into(), + Err(e) => WarpResult::RuntimeError(format!("{e:?}")), + } + } +} diff --git a/crates/warp-contracts/warp-contracts-macro/Cargo.toml b/crates/warp-contracts/warp-contracts-macro/Cargo.toml new file mode 100644 index 00000000..4e00be57 --- /dev/null +++ b/crates/warp-contracts/warp-contracts-macro/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "warp-contracts-macro" +version = "0.1.1" +edition = "2021" +description = "warp_contract macro definition" +license = "MIT" +documentation = "https://academy.warp.cc/docs/sdk/advanced/wasm" +homepage = "https://warp.cc" +repository = "https://github.com/warp-contracts/warp" +keywords = ["warp", "smart-contract", "SmartWeave", "web3"] +categories = ["api-bindings", "development-tools::ffi", "finance", "wasm"] + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0" +syn = { version = "1.0", features = ["extra-traits"] } +warp-contracts-core = { version = "0.1.1", path = "../warp-contracts-core" } diff --git a/crates/warp-contracts/warp-contracts-macro/README.md b/crates/warp-contracts/warp-contracts-macro/README.md new file mode 100644 index 00000000..2c27ba5b --- /dev/null +++ b/crates/warp-contracts/warp-contracts-macro/README.md @@ -0,0 +1,3 @@ +# warp-contracts-macro + +Implementation of the #[warp_contract] attribute. See the `warp-contracts` documentation for more information. diff --git a/crates/warp-contracts/warp-contracts-macro/src/lib.rs b/crates/warp-contracts/warp-contracts-macro/src/lib.rs new file mode 100644 index 00000000..b6987c9c --- /dev/null +++ b/crates/warp-contracts/warp-contracts-macro/src/lib.rs @@ -0,0 +1,8 @@ +use proc_macro::TokenStream; + +mod warp_contract_macro; + +#[proc_macro_attribute] +pub fn warp_contract(attr: TokenStream, input: TokenStream) -> TokenStream { + warp_contract_macro::warp_contract(attr, input) +} diff --git a/crates/warp-contracts/warp-contracts-macro/src/warp_contract_macro.rs b/crates/warp-contracts/warp-contracts-macro/src/warp_contract_macro.rs new file mode 100644 index 00000000..ac7c35d0 --- /dev/null +++ b/crates/warp-contracts/warp-contracts-macro/src/warp_contract_macro.rs @@ -0,0 +1,133 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, FnArg, ItemFn}; + +pub(crate) fn warp_contract(attr: TokenStream, input: TokenStream) -> TokenStream { + let is_write = match attr.to_string().as_str() { + "write" => true, + "view" => false, + _ => panic!( + "warp_contract macro requires exactly one attribute: \ + 'write' for method changing state or 'view' for view state method \ + e.g. #[warp_contract(write)], #[warp_contract(view)]" + ), + }; + let ast = parse_macro_input!(input as ItemFn); + let ast_clone = ast.clone(); + let fun_name = ast.sig.ident; + let is_async = ast.sig.asyncness.is_some(); + let (await_spec, write_core_method, view_core_method) = if is_async { + ( + quote! { .await }, + quote! { ::warp_contracts::methods::write_async }, + quote! { ::warp_contracts::methods::view_async }, + ) + } else { + ( + quote! {}, + quote! { ::warp_contracts::methods::write_sync }, + quote! { ::warp_contracts::methods::view_sync }, + ) + }; + + let inputs = ast.sig.inputs; + if inputs.len() != 2 { + panic!("two arguments expected for warp contract handle method, the first one representing state object and the second representing interaction description"); + } + let state_type = match inputs.first().unwrap() { + FnArg::Receiver(_) => panic!("self not allowed in warp contract handle function"), + FnArg::Typed(t) => &t.ty, + }; + if is_write { + quote! { + /* + Note: in order do optimize communication between host and the WASM module, + we're storing the state inside the WASM module (for the time of state evaluation). + This allows to reduce the overhead of passing the state back and forth + between the host and module with each contract interaction. + In case of bigger states this overhead can be huge. + Same approach has been implemented for the AssemblyScript version. + + So the flow (from the SDK perspective) is: + 1. SDK calls exported WASM module function "initState" (with lastly cached state or initial state, + if cache is empty) - which initializes the state in the WASM module. + 2. SDK calls "handle" function for each of the interaction. + If given interaction was modifying the state - it is updated inside the WASM module + - but not returned to host. + 3. Whenever SDK needs to know the current state (eg. in order to perform + caching or to simply get its value after evaluating all of the interactions) + - it calls WASM's module "currentState" function. + + The handle function by default does not return the new state - + it only updates it in the WASM module. + The handle function returns a value only in case of error + or calling a "view" function. + + In the future this might also allow to enhance the inner-contracts communication + - e.g. if the execution network will store the state of the contracts - as the WASM contract module memory + - it would allow to read other contract's state "directly" from WASM module memory. + */ + static __WARP_CONTRACT_STATE: ::warp_contracts::optional_cell::OptionalCell<#state_type> = + ::warp_contracts::optional_cell::OptionalCell::empty(); + + const _: () = { + use ::core::{cell::RefCell, option::Option}; + use ::serde::{Deserialize, Serialize}; + use ::serde_wasm_bindgen::from_value; + use ::warp_contracts::{ + optional_cell::OptionalCell, + warp_result::{transmission::Transmission, WarpResult}, + js_imports::log, + methods::* + }; + use ::wasm_bindgen::prelude::*; + + #[wasm_bindgen(js_name = warpContractWrite)] + pub async fn __warp_contracts_generated_write(interaction: JsValue) -> JsValue { + #write_core_method(&__WARP_CONTRACT_STATE, interaction, #fun_name)#await_spec + } + + #[wasm_bindgen(js_name = initState)] + pub fn __warp_contracts_generated_init_state(state: &JsValue) -> Option { + init_state(&__WARP_CONTRACT_STATE, state) + } + + #[wasm_bindgen(js_name = currentState)] + pub fn __warp_contracts_generated_current_state() -> JsValue { + current_state(&__WARP_CONTRACT_STATE) + } + + #[wasm_bindgen(js_name = version)] + pub fn __warp_contracts_generated_version() -> i32 { + return 1; + } + + // Workaround for now to simplify type reading without as/loader or wasm-bindgen + // 1 = assemblyscript + // 2 = rust + // 3 = go + // 4 = swift + // 5 = c + #[wasm_bindgen(js_name = lang)] + pub fn __warp_contracts_generated_lang() -> i32 { + return 2; + } + () + }; + #ast_clone + } + .into() + } else { + quote! { + const _: () = { + use ::wasm_bindgen::prelude::*; + + #[wasm_bindgen(js_name = warpContractView)] + pub async fn __warp_contracts_generated_view(interaction: JsValue) -> JsValue { + #view_core_method(&__WARP_CONTRACT_STATE, interaction, #fun_name)#await_spec + } + }; + #ast_clone + }.into() + } +}