feat: introduce warp_contracts macro

gh-309
This commit is contained in:
robal
2023-03-11 00:10:15 +01:00
parent 0fae2a0674
commit f4f7b72ac6
22 changed files with 1054 additions and 4 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Dependency directories
node_modules/
crates/warp-contracts/target
# Optional eslint cache
.eslintcache

214
crates/warp-contracts/Cargo.lock generated Normal file
View File

@@ -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.1"
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.1"
dependencies = [
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "warp-contracts-macro"
version = "0.1.1"
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",
]

View File

@@ -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" }

View File

@@ -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)

View File

@@ -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!**

View File

@@ -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<T: DeserializeOwned>(
contract_address: &str,
) -> Result<T, String> {
match SmartWeave::read_contract_state(contract_address).await {
Ok(s) => match from_value::<T>(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<V, E> {
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::<V, E>(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<I: Serialize, E: DeserializeOwned + Debug>(
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:?}")),
}
}

View File

@@ -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<JsValue, JsValue>;
#[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<JsValue, JsValue>;
#[wasm_bindgen(catch, static_method_of = SmartWeave, js_name = viewContractState)]
pub async fn view_contract_state(contract_id: &str, input: JsValue)
-> Result<JsValue, JsValue>;
#[wasm_bindgen(catch, static_method_of = SmartWeave, js_name = write)]
pub async fn write(contract_id: &str, input: JsValue) -> Result<JsValue, JsValue>;
#[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);
}

View File

@@ -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<T: DeserializeOwned + Default>(key: &str) -> ViewResult<T, KvError> {
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<T: Serialize>(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)),
}
}

View File

@@ -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;

View File

@@ -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"]

View File

@@ -0,0 +1,4 @@
# warp-contract-core
Helper library for warp-contracts and warp-contracts-macro. See the `warp-contracts` documentation for more details.

View File

@@ -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<Output = V> + '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<Output = V> + 'a,
{
type Fut = Fu;
fn call(self, state: &'a S, action: A) -> Fu {
self(state, action)
}
}

View File

@@ -0,0 +1,13 @@
#[derive(Debug)]
pub enum ViewResult<View, Error> {
Success(View),
ContractError(Error),
RuntimeError(String),
}
#[derive(Debug)]
pub enum WriteResult<State, Error> {
Success(State),
ContractError(Error),
RuntimeError(String),
}

View File

@@ -0,0 +1,5 @@
mod detail;
pub mod handler_result;
pub mod methods;
pub mod optional_cell;
pub mod warp_result;

View File

@@ -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, Action, Error, Fun, Fut>(
state: &OptionalCell<State>,
interaction: JsValue,
write_contract_method: Fun,
) -> JsValue
where
State: Clone + Debug + Serialize,
Action: DeserializeOwned + Debug,
Error: Serialize,
Fun: FnOnce(State, Action) -> Fut,
Fut: Future<Output = WriteResult<State, Error>>,
{
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<State, Action, Error, Fun>(
s: &OptionalCell<State>,
interaction: JsValue,
write_contract_method: Fun,
) -> JsValue
where
State: Clone + Debug + Serialize,
Action: DeserializeOwned + Debug,
Error: Serialize,
Fun: FnOnce(State, Action) -> WriteResult<State, Error>,
{
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<State, Action, View, Error, Fun>(
s: &OptionalCell<State>,
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<View, Error>>,
{
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::<Transmission<View, Error>>(&WarpResult::from(result).into()).unwrap()
}
pub fn view_sync<State, Action, View, Error, Fun>(
s: &OptionalCell<State>,
interaction: JsValue,
view_contract_method: Fun,
) -> JsValue
where
State: Clone,
Action: DeserializeOwned + Debug,
View: Serialize,
Error: Serialize,
Fun: FnOnce(&State, Action) -> ViewResult<View, Error>,
{
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::<Transmission<View, Error>>(&WarpResult::from(result).into()).unwrap()
}
fn map_write_result<State, Error>(
result: WriteResult<State, Error>,
state: &OptionalCell<State>,
) -> JsValue
where
State: Clone + Debug + Serialize,
Error: Serialize,
{
if let WriteResult::Success(new_state) = result {
state.cell.replace(Some(new_state));
to_json_value::<Transmission<(), Error>>(&WarpResult::WriteSuccess().into()).unwrap()
} else {
to_json_value::<Transmission<(), Error>>(&WarpResult::from(result).into()).unwrap()
}
}
fn parse_input<State, Action>(
state: &OptionalCell<State>,
interaction: JsValue,
) -> Result<Action, JsValue>
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<S: DeserializeOwned>(
state: &OptionalCell<S>,
init_state: &JsValue,
) -> Option<String> {
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::debug_1(&JsValue::from_str(&ret));
}
Option::from(ret)
}
}
}
pub fn current_state<State: Serialize + Debug>(state: &OptionalCell<State>) -> 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<T: serde::ser::Serialize + ?Sized>(
value: &T,
) -> core::result::Result<JsValue, serde_wasm_bindgen::Error> {
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
value.serialize(&serializer)
}
pub fn runtime_error(error_message: String) -> JsValue {
to_json_value::<Transmission<(), ()>>(&WarpResult::RuntimeError(error_message).into()).unwrap()
}

View File

@@ -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<T> {
pub cell: RefCell<Option<T>>,
}
impl<T> OptionalCell<T> {
pub const fn empty() -> OptionalCell<T> {
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<T: Clone> {
fn clone_contents(&self) -> T;
}
impl<T: Clone> CloneContents<T> for OptionalCell<T> {
fn clone_contents(&self) -> T {
self.cell.borrow().as_ref().unwrap().clone()
}
}
unsafe impl<T> Sync for OptionalCell<T> {}

View File

@@ -0,0 +1,130 @@
use crate::handler_result::{ViewResult, WriteResult};
pub enum WarpResult<View, Error> {
WriteSuccess(),
ViewSuccess(View),
ContractError(Error),
RuntimeError(String),
}
impl<V, E> From<ViewResult<V, E>> for WarpResult<V, E> {
fn from(value: ViewResult<V, E>) -> Self {
match value {
ViewResult::Success(v) => WarpResult::ViewSuccess(v),
ViewResult::ContractError(e) => WarpResult::ContractError(e),
ViewResult::RuntimeError(e) => WarpResult::RuntimeError(e),
}
}
}
impl<S, V, E> From<WriteResult<S, E>> for WarpResult<V, E> {
fn from(value: WriteResult<S, E>) -> Self {
match value {
WriteResult::Success(_) => WarpResult::WriteSuccess(),
WriteResult::ContractError(e) => WarpResult::ContractError(e),
WriteResult::RuntimeError(e) => WarpResult::RuntimeError(e),
}
}
}
impl<V, E> From<WarpResult<V, E>> for ViewResult<V, E> {
fn from(value: WarpResult<V, E>) -> 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<V, E> From<WarpResult<V, E>> for WriteResult<(), E> {
fn from(value: WarpResult<V, E>) -> 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<View, Error> {
#[serde(rename = "type")]
result_type: String,
result: Option<View>,
error: Option<Error>,
#[serde(rename = "errorMessage")]
error_message: Option<String>,
}
impl<V: Debug, E: Debug> From<Transmission<V, E>> for WarpResult<V, E> {
fn from(value: Transmission<V, E>) -> 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<V, E> From<WarpResult<V, E>> for Transmission<V, E> {
fn from(value: WarpResult<V, E>) -> 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<V: Debug, E: Debug>(warp_result: JsValue) -> WarpResult<V, E>
where
V: DeserializeOwned,
E: DeserializeOwned,
{
match serde_wasm_bindgen::from_value::<Transmission<V, E>>(warp_result) {
Ok(t) => t.into(),
Err(e) => WarpResult::RuntimeError(format!("{e:?}")),
}
}
}

View File

@@ -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" }

View File

@@ -0,0 +1,3 @@
# warp-contracts-macro
Implementation of the #[warp_contract] attribute. See the `warp-contracts` documentation for more information.

View File

@@ -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)
}

View File

@@ -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<String> {
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()
}
}

View File

@@ -720,7 +720,7 @@ export const rustWasmImports = (swGlobal, wbindgenImports, wasmInstance, dtorVal
},
__wbg_log_3: function () {
__wbg_log: function () {
return logError(function (arg0, arg1) {
rawImports.console.log(getStringFromWasm0(arg0, arg1));
}, arguments)
@@ -737,14 +737,12 @@ export const rustWasmImports = (swGlobal, wbindgenImports, wasmInstance, dtorVal
return ret;
},
__wbg_log_7: function () {
__wbg_debug: function () {
return logError(function (arg0) {
console.log(getObject(arg0));
}, arguments)
},
__wbg_String: function () {
return logError(function (arg0, arg1) {
const ret = String(getObject(arg1));