Skip to content

Commit

Permalink
Escrow (#121)
Browse files Browse the repository at this point in the history
* feat: escrowable capabilities

* feat: add getter for locked state

* docs(escrow): add some documentation

* test(escrow): add integration test for macro

* test(escrow): add workspace test for macro

* test(escrow): add integration test for escrow macro

* fix: some clippy lints, also optional macro props

* feat(escrow): optionally implement events
allows types that implement `serde::Serialize` to emit events

---------

Co-authored-by: Jacob Lindahl <encody@users.noreply.github.com>
  • Loading branch information
dndll and encody authored Sep 13, 2023
1 parent 690ec86 commit d4b8777
Show file tree
Hide file tree
Showing 9 changed files with 678 additions and 2 deletions.
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()
}
}

/// 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);

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

0 comments on commit d4b8777

Please sign in to comment.