Skip to content

Commit

Permalink
project structure; ownership & utils
Browse files Browse the repository at this point in the history
  • Loading branch information
encody committed May 10, 2022
1 parent dbdd2c0 commit 1be19ac
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.tabSize": 4
}
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "near-contract-tools"
version = "0.1.0"
edition = "2021"
authors = ["Jacob Lindahl <[email protected]>"]
license = "GPL-3.0"
categories = ["wasm"]
description = """
Helpful functions and macros for developing smart contracts on NEAR Protocol.
"""

[dependencies]
near-sdk = "4.0.0-pre.8"
1 change: 1 addition & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tab_spaces = 4
16 changes: 16 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pub mod ownership;
pub mod utils;

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
near_sdk::testing_env!(near_sdk::test_utils::VMContextBuilder::new()
.attached_deposit(near_sdk::ONE_NEAR)
.build());

// near_sdk::testing_env!();
let result = 2 + 2;
assert_eq!(result, 4);
}
}
197 changes: 197 additions & 0 deletions src/ownership.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use near_sdk::{
borsh::{self, BorshDeserialize, BorshSerialize},
collections::LazyOption,
env, require, AccountId, IntoStorageKey,
};

use crate::utils::prefix_key;

/// State for contract ownership management
///
/// # Examples
///
/// ```
/// use near_contract_tools::ownership::Ownership;
///
/// struct Contract {
/// // ...
/// pub ownership: Ownership,
/// }
/// ```
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Ownership {
pub owner: Option<AccountId>,
pub proposed_owner: LazyOption<AccountId>,
}

impl Ownership {
/// Creates a new Ownership using the specified storage key prefix.
///
/// # Examples
///
/// ```
/// use near_contract_tools::ownership::Ownership;
///
/// let ownership = Ownership::new(
/// b"o",
/// near_sdk::env::predecessor_account_id(),
/// );
/// ```
pub fn new<S>(storage_key_prefix: S, owner_id: AccountId) -> Self
where
S: IntoStorageKey,
{
let k = storage_key_prefix.into_storage_key();

Self {
owner: Some(owner_id),
proposed_owner: LazyOption::new(prefix_key(&k, b"p"), None),
}
}

/// Requires the predecessor to be the owner
///
/// # Examples
///
/// ```
/// use near_contract_tools::ownership::Ownership;
///
/// let ownership = Ownership::new(
/// b"o",
/// near_sdk::env::predecessor_account_id(),
/// );
/// ownership.assert_owner();
/// ```
pub fn assert_owner(&self) {
require!(
&env::predecessor_account_id()
== self
.owner
.as_ref()
.unwrap_or_else(|| env::panic_str("No owner")),
"Owner only"
);
}

/// Removes the contract's owner. Can only be called by the current owner.
///
/// # Examples
///
/// ```
/// use near_contract_tools::ownership::Ownership;
///
/// let owner_id = near_sdk::env::predecessor_account_id();
/// let mut ownership = Ownership::new(
/// b"o",
/// owner_id.clone(),
/// );
/// assert_eq!(ownership.owner, Some(owner_id));
/// ownership.renounce_owner();
/// assert_eq!(ownership.owner, None);
/// ```
pub fn renounce_owner(&mut self) {
self.assert_owner();
self.owner = None;
self.proposed_owner.remove();
}

/// Prepares the contract to change owners, setting the proposed owner to
/// the provided account ID. Can only be called by the current owner.
///
/// # Examples
///
/// ```
/// use near_contract_tools::ownership::Ownership;
///
/// let mut ownership = Ownership::new(
/// b"o",
/// near_sdk::env::predecessor_account_id(),
/// );
/// let proposed_owner: near_sdk::AccountId = "account".parse().unwrap();
/// assert_eq!(ownership.proposed_owner.get(), None);
/// ownership.propose_owner(Some(proposed_owner.clone()));
/// assert_eq!(ownership.proposed_owner.get(), Some(proposed_owner));
/// ```
pub fn propose_owner(&mut self, account_id: Option<AccountId>) {
self.assert_owner();
if let Some(a) = account_id {
self.proposed_owner.set(&a);
} else {
self.proposed_owner.remove();
}
}

/// Sets new owner equal to proposed owner. Can only be called by proposed
/// owner.
///
/// # Examples
///
/// ```
/// use near_contract_tools::ownership::Ownership;
///
/// let owner_id = "account".parse().unwrap();
/// let mut ownership = Ownership::new(
/// b"o",
/// owner_id,
/// );
/// let proposed_owner = near_sdk::env::predecessor_account_id();
/// ownership.proposed_owner.set(&proposed_owner);
/// ownership.accept_owner();
/// assert_eq!(ownership.owner, Some(proposed_owner));
/// ```
pub fn accept_owner(&mut self) {
let proposed_owner = self
.proposed_owner
.take()
.unwrap_or_else(|| env::panic_str("No proposed owner"));
require!(
&env::predecessor_account_id() == &proposed_owner,
"Proposed owner only"
);
self.owner = Some(proposed_owner);
}
}

pub trait Ownable {
fn own_get_owner(&self) -> Option<AccountId>;
fn own_get_proposed_owner(&self) -> Option<AccountId>;
fn own_renounce_owner(&mut self);
fn own_propose_owner(&mut self, account_id: Option<AccountId>);
fn own_accept_owner(&mut self);
}

#[macro_export]
macro_rules! impl_ownership {
($contract: ident, $ownership: ident) => {
use $crate::ownership::Ownable;

#[near_bindgen]
impl Ownable for $contract {
fn own_get_owner(&self) -> Option<AccountId> {
self.$ownership.owner.clone()
}

fn own_get_proposed_owner(&self) -> Option<AccountId> {
self.$ownership.proposed_owner.get()
}

#[payable]
fn own_renounce_owner(&mut self) {
assert_one_yocto();
self.$ownership.renounce_owner()
}

#[payable]
fn own_propose_owner(&mut self, account_id: Option<AccountId>) {
assert_one_yocto();
self.$ownership.propose_owner(account_id);
}

#[payable]
fn own_accept_owner(&mut self) {
assert_one_yocto();
self.$ownership.accept_owner();
}
}
};
}
67 changes: 67 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use near_sdk::{env, require, Promise};

/// Concatenate bytes to form a key. Useful for generating storage keys.
///
/// # Examples
///
/// ```
/// use near_contract_tools::utils::prefix_key;
///
/// assert_eq!(prefix_key(b"p", b"key"), b"pkey".to_vec());
/// ```
pub fn prefix_key(prefix: &dyn AsRef<[u8]>, key: &dyn AsRef<[u8]>) -> Vec<u8> {
[prefix.as_ref(), key.as_ref()].concat()
}

/// Calculates the storage fee of an action, given an initial storage amount,
/// and refunds the predecessor a portion of the attached deposit if necessary.
/// Returns refund Promise if refund was applied.
///
/// # Examples
///
/// ```
/// use near_contract_tools::utils::apply_storage_fee_and_refund;
///
/// let initial_storage_usage = near_sdk::env::storage_usage();
/// let additional_fees = 0;
///
/// // Action that consumes storage.
/// near_sdk::env::storage_write(b"key", b"value");
///
/// near_sdk::testing_env!(near_sdk::test_utils::VMContextBuilder::new()
/// .attached_deposit(near_sdk::ONE_NEAR)
/// .build());
/// // Attached deposit must cover storage fee or this function will panic
/// apply_storage_fee_and_refund(initial_storage_usage, additional_fees);
/// ```
pub fn apply_storage_fee_and_refund(
initial_storage_usage: u64,
additional_fees: u128,
) -> Option<Promise> {
// Storage consumption after storage event
let storage_usage_end = env::storage_usage();

// Storage fee incurred by storage event, clamped >= 0
let storage_fee = u128::from(storage_usage_end.saturating_sub(initial_storage_usage))
* env::storage_byte_cost();

let total_required_deposit = storage_fee + additional_fees;

let attached_deposit = env::attached_deposit();

require!(
attached_deposit >= total_required_deposit,
format!(
"Insufficient deposit: attached {attached_deposit} yoctoNEAR < required {total_required_deposit} yoctoNEAR ({storage_fee} storage + {additional_fees} additional)",
)
);

let refund = attached_deposit - total_required_deposit;

// Send refund transfer if required
if refund > 0 {
Some(Promise::new(env::predecessor_account_id()).transfer(refund))
} else {
None
}
}

0 comments on commit 1be19ac

Please sign in to comment.