Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: escrowable capabilities #121

Merged
merged 9 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions macros/src/escrow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use darling::FromDeriveInput;
use proc_macro2::TokenStream;
use quote::quote;
use syn::Expr;

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(escrow), supports(struct_named))]
pub struct EscrowMeta {
pub storage_key: Option<Expr>,
pub id: Expr,
pub state: Option<Expr>,

pub generics: syn::Generics,
pub ident: syn::Ident,

// crates
#[darling(rename = "crate", default = "crate::default_crate_name")]
pub me: syn::Path,
#[darling(default = "crate::default_near_sdk")]
pub near_sdk: syn::Path,
}

pub fn expand(meta: EscrowMeta) -> Result<TokenStream, darling::Error> {
let EscrowMeta {
storage_key,
id,
state,

ident,
generics,

me,
near_sdk: _near_sdk,
} = meta;

let (imp, ty, wher) = generics.split_for_impl();

let root = storage_key.map(|storage_key| {
quote! {
fn root() -> #me::slot::Slot<()> {
#me::slot::Slot::root(#storage_key)
}
}
});

let state = state
.map(|state| quote! { #state })
.unwrap_or_else(|| quote! { () });

Ok(quote! {
impl #imp #me::escrow::EscrowInternal for #ident #ty #wher {
type Id = #id;
type State = #state;

#root
}
})
}
13 changes: 13 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use proc_macro::TokenStream;
use syn::{parse_macro_input, AttributeArgs, DeriveInput, Item};

mod approval;
mod escrow;
mod migrate;
mod owner;
mod pause;
Expand Down Expand Up @@ -264,3 +265,15 @@ pub fn event(attr: TokenStream, item: TokenStream) -> TokenStream {
pub fn derive_upgrade(input: TokenStream) -> TokenStream {
make_derive(input, upgrade::expand)
}

/// Creates a managed, lazily-loaded `Escrow` implementation for the targeted
/// `#[near_bindgen]` struct.
///
/// Fields include:
/// - `id` - the type required for id, must be `borsh::BorshSerialize` & `serde::Serialize`, for events
/// - `state` - the type required for id, must be `borsh::BorshSerialize` & `borsh::BorshSerialize`
/// - `storage_key` Storage prefix for escrow data (optional, default: `b"~es"`)
#[proc_macro_derive(Escrow, attributes(escrow))]
pub fn derive_escrow(input: TokenStream) -> TokenStream {
make_derive(input, escrow::expand)
}
246 changes: 246 additions & 0 deletions src/escrow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
//! Escrow pattern implements locking functionality over some arbitrary storage key.
//!
//! Upon locking something, it adds a flag in the store that some item on some `id` is locked with some `state`.
//! This allows you to verify if an item is locked, and add some additional functionality to unlock the item.
//!
//! The crate exports a [derive macro](near_sdk_contract_tools_macros::Escrow)
//! that derives a default implementation for escrow.
//!
//! # Safety
//! The state for this contract is stored under the [root][EscrowInternal::root], make sure you dont
//! accidentally collide these storage entries in your contract.
//! You can change the key this is stored under by providing [storage_key] to the macro.
use crate::{event, standard::nep297::Event};
use crate::{slot::Slot, DefaultStorageKey};
use near_sdk::{
borsh::BorshSerialize,
borsh::{self, BorshDeserialize},
env::panic_str,
require,
serde::Serialize,
BorshStorageKey,
};

const ESCROW_ALREADY_LOCKED_MESSAGE: &str = "Already locked";
const ESCROW_NOT_LOCKED_MESSAGE: &str = "Lock required";
const ESCROW_UNLOCK_HANDLER_FAILED_MESSAGE: &str = "Unlock handler failed";

#[derive(BorshSerialize, BorshStorageKey)]
enum StorageKey<'a, T> {
Locked(&'a T),
}

/// Emit the state of an escrow lock and whether it was locked or unlocked
#[event(
standard = "x-escrow",
version = "1.0.0",
crate = "crate",
macros = "crate"
)]
pub struct Lock<Id: Serialize, State: Serialize> {
/// The identifier for a lock
pub id: Id,
/// If the lock was locked or unlocked, and any state along with it
pub locked: Option<State>,
}

/// Inner storage modifiers and functionality required for escrow to succeed
pub trait EscrowInternal {
/// Identifier over which the escrow exists
type Id: BorshSerialize;
/// State stored inside the lock
type State: BorshSerialize + BorshDeserialize;

/// Retrieve the state root
fn root() -> Slot<()> {
Slot::root(DefaultStorageKey::Escrow)
}

/// Inner function to retrieve the slot keyed by it's `Self::Id`
fn locked_slot(&self, id: &Self::Id) -> Slot<Self::State> {
Self::root().field(StorageKey::Locked(id))
}

/// Read the state from the slot
fn get_locked(&self, id: &Self::Id) -> Option<Self::State> {
self.locked_slot(id).read()
}

/// Set the state at `id` to `locked`
fn set_locked(&mut self, id: &Self::Id, locked: &Self::State) {
self.locked_slot(id).write(locked);
}

/// Clear the state at `id`
fn set_unlocked(&mut self, id: &Self::Id) {
self.locked_slot(id).remove();
}
}

/// Some escrowable capabilities, with a simple locking/unlocking mechanism
/// If you add additional `Approve` capabilities here, you can make use of a step-wise locking system.
pub trait Escrow {
/// Identifier over which the escrow exists
type Id: BorshSerialize;
/// State stored inside the lock
type State: BorshSerialize + BorshDeserialize;

/// Lock some `Self::State` by it's `Self::Id` within the store
fn lock(&mut self, id: &Self::Id, state: &Self::State);

/// Unlock and release some `Self::State` by it's `Self::Id`
///
/// Optionally, you can provide a handler which would allow you to inject logic if you should unlock or not.
fn unlock(&mut self, id: &Self::Id, unlock_handler: impl FnOnce(&Self::State) -> bool);

/// Check if the item is locked
fn is_locked(&self, id: &Self::Id) -> bool;
}

impl<T> Escrow for T
where
T: EscrowInternal,
{
type Id = <Self as EscrowInternal>::Id;
type State = <Self as EscrowInternal>::State;

fn lock(&mut self, id: &Self::Id, state: &Self::State) {
require!(self.get_locked(id).is_none(), ESCROW_ALREADY_LOCKED_MESSAGE);

self.set_locked(id, state);
}

fn unlock(&mut self, id: &Self::Id, unlock_handler: impl FnOnce(&Self::State) -> bool) {
let lock = self
.get_locked(id)
.unwrap_or_else(|| panic_str(ESCROW_NOT_LOCKED_MESSAGE));

if unlock_handler(&lock) {
self.set_unlocked(id);
} else {
panic_str(ESCROW_UNLOCK_HANDLER_FAILED_MESSAGE)
}
}

fn is_locked(&self, id: &Self::Id) -> bool {
self.get_locked(id).is_some()
}
dndll marked this conversation as resolved.
Show resolved Hide resolved
}

/// A wrapper trait allowing all implementations of `State` and `Id` that implement [`serde::Serialize`]
/// to emit an event on success if they want to.
pub trait EventEmittedOnEscrow<Id: Serialize, State: Serialize> {
/// Optionally implement an event on success of lock
fn lock_emit(&mut self, id: &Id, state: &State);
/// Optionally implement an event on success of unlock
fn unlock_emit(&mut self, id: &Id, unlock_handler: impl FnOnce(&State) -> bool);
}

impl<T> EventEmittedOnEscrow<<T as Escrow>::Id, <T as Escrow>::State> for T
where
T: Escrow + EscrowInternal,
<T as Escrow>::Id: Serialize,
<T as Escrow>::State: Serialize,
{
fn lock_emit(&mut self, id: &<T as Escrow>::Id, state: &<T as Escrow>::State) {
self.lock(id, state);
Lock {
id: id.to_owned(),
locked: Some(state),
}
.emit();
}

fn unlock_emit(
&mut self,
id: &<T as Escrow>::Id,
unlock_handler: impl FnOnce(&<T as Escrow>::State) -> bool,
) {
self.unlock(id, unlock_handler);
Lock::<_, <T as Escrow>::State> { id, locked: None }.emit();
}
}

#[cfg(test)]
mod tests {
use super::Escrow;
use crate::escrow::EscrowInternal;
use near_sdk::{
near_bindgen, test_utils::VMContextBuilder, testing_env, AccountId, Balance, VMContext,
ONE_YOCTO,
};
use near_sdk_contract_tools_macros::Escrow;

const ID: u64 = 1;
const IS_NOT_READY: bool = false;

#[derive(Escrow)]
#[escrow(id = "u64", state = "bool", crate = "crate")]
#[near_bindgen]
struct Contract {}

#[near_bindgen]
impl Contract {
#[init]
pub fn new() -> Self {
Self {}
}
}

fn alice() -> AccountId {
"alice".parse().unwrap()
}

fn get_context(attached_deposit: Balance, signer: Option<AccountId>) -> VMContext {
VMContextBuilder::new()
.signer_account_id(signer.clone().unwrap_or_else(alice))
.predecessor_account_id(signer.unwrap_or_else(alice))
.attached_deposit(attached_deposit)
.is_view(false)
.build()
}

#[test]
fn test_can_lock() {
testing_env!(get_context(ONE_YOCTO, None));
let mut contract = Contract::new();

contract.lock(&ID, &IS_NOT_READY);
assert!(contract.get_locked(&ID).is_some());
}

#[test]
#[should_panic(expected = "Already locked")]
fn test_cannot_lock_twice() {
testing_env!(get_context(ONE_YOCTO, None));
let mut contract = Contract::new();

contract.lock(&ID, &IS_NOT_READY);
contract.lock(&ID, &IS_NOT_READY);
}

#[test]
fn test_can_unlock() {
testing_env!(get_context(ONE_YOCTO, None));
let mut contract = Contract::new();

let is_ready = true;
contract.lock(&ID, &is_ready);
contract.unlock(&ID, |readiness| readiness == &is_ready);

assert!(contract.get_locked(&ID).is_none());
}

#[test]
#[should_panic(expected = "Unlock handler failed")]
fn test_cannot_unlock_until_ready() {
testing_env!(get_context(ONE_YOCTO, None));
let mut contract = Contract::new();

let is_ready = true;
contract.lock(&ID, &IS_NOT_READY);
contract.unlock(&ID, |readiness| readiness == &is_ready);
encody marked this conversation as resolved.
Show resolved Hide resolved

assert!(contract.get_locked(&ID).is_none());
}
}
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub enum DefaultStorageKey {
Pause,
/// Default storage key for [`rbac::RbacInternal::root`].
Rbac,
/// Default storage key for [`escrow::Escrow::root`]
Escrow,
}

impl IntoStorageKey for DefaultStorageKey {
Expand All @@ -39,13 +41,15 @@ impl IntoStorageKey for DefaultStorageKey {
DefaultStorageKey::Owner => b"~o".to_vec(),
DefaultStorageKey::Pause => b"~p".to_vec(),
DefaultStorageKey::Rbac => b"~r".to_vec(),
DefaultStorageKey::Escrow => b"~es".to_vec(),
}
}
}

pub mod standard;

pub mod approval;
pub mod escrow;
pub mod fast_account_id;
pub mod migrate;
pub mod owner;
Expand Down
Loading
Loading