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

Simple NFT #129

Closed
wants to merge 10 commits into from
Binary file modified nft-simple/res/nft_simple.wasm
Binary file not shown.
110 changes: 80 additions & 30 deletions nft-simple/src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ use crate::*;

/// Price per 1 byte of storage from mainnet config after `0.18` release and protocol version `42`.
evgenykuzyakov marked this conversation as resolved.
Show resolved Hide resolved
/// It's 10 times lower than the genesis price.
const STORAGE_PRICE_PER_BYTE: Balance = 10_000_000_000_000_000_000;
pub(crate) const STORAGE_PRICE_PER_BYTE: Balance = 10_000_000_000_000_000_000;

pub(crate) fn prefix(account_id: &AccountId) -> Vec<u8> {
format!("o{}", account_id).into_bytes()
pub(crate) fn unique_prefix(account_id: &AccountId) -> Vec<u8> {
let mut prefix = Vec::with_capacity(33);
prefix.push(b'o');
prefix.extend(env::sha256(account_id.as_bytes()));
prefix
}

pub(crate) fn assert_one_yocto() {
Expand All @@ -24,6 +27,37 @@ pub(crate) fn assert_self() {
);
}

pub(crate) fn deposit_refund(storage_used: u64) {
let required_cost = STORAGE_PRICE_PER_BYTE * Balance::from(storage_used);
let attached_deposit = env::attached_deposit();

assert!(
required_cost <= attached_deposit,
"Requires to attach {} NEAR tokens to cover storage",
required_cost
);

let refund = attached_deposit - required_cost;
if refund > 0 {
Promise::new(env::predecessor_account_id()).transfer(refund);
}
}

pub(crate) fn bytes_for_approved_account_id(account_id: &AccountId) -> u64 {
account_id.len() as u64 + 4
evgenykuzyakov marked this conversation as resolved.
Show resolved Hide resolved
}

pub(crate) fn refund_approved_account_ids(
account_id: AccountId,
approved_account_ids: &HashSet<AccountId>,
) -> Promise {
let storage_released: u64 = approved_account_ids
.iter()
.map(bytes_for_approved_account_id)
.sum();
Promise::new(account_id).transfer(Balance::from(storage_released) * STORAGE_PRICE_PER_BYTE)
}

impl Contract {
pub(crate) fn assert_owner(&self) {
assert_eq!(
Expand All @@ -33,27 +67,17 @@ impl Contract {
);
}

pub(crate) fn assert_enough_storage(&self) {
assert!(
env::account_balance() + env::account_locked_balance()
>= Balance::from(
env::storage_usage()
+ self.total_supply * self.extra_storage_in_bytes_per_token
) * STORAGE_PRICE_PER_BYTE
);
}

pub(crate) fn internal_add_token_to_owner(
&mut self,
account_id: &AccountId,
token_id: &TokenId,
) {
let mut tokens_set = self
.accounts
.tokens_per_owner
.get(account_id)
.unwrap_or_else(|| UnorderedSet::new(prefix(account_id)));
.unwrap_or_else(|| UnorderedSet::new(unique_prefix(account_id)));
tokens_set.insert(token_id);
self.accounts.insert(account_id, &tokens_set);
self.tokens_per_owner.insert(account_id, &tokens_set);
}

pub(crate) fn internal_remove_token_from_owner(
Expand All @@ -62,14 +86,14 @@ impl Contract {
token_id: &TokenId,
) {
let mut tokens_set = self
.accounts
.tokens_per_owner
.get(account_id)
.expect("Token should be owned by the sender");
tokens_set.remove(token_id);
if tokens_set.is_empty() {
self.accounts.remove(account_id);
self.tokens_per_owner.remove(account_id);
} else {
self.accounts.insert(account_id, &tokens_set);
self.tokens_per_owner.insert(account_id, &tokens_set);
}
}

Expand All @@ -78,27 +102,53 @@ impl Contract {
sender_id: &AccountId,
receiver_id: &AccountId,
token_id: &TokenId,
enforce_owner_id: Option<&ValidAccountId>,
memo: Option<String>,
) {
) -> (AccountId, HashSet<AccountId>) {
let Token {
owner_id,
metadata,
approved_account_ids,
} = self.tokens_by_id.get(token_id).expect("Token not found");
if sender_id != &owner_id && !approved_account_ids.contains(sender_id) {
env::panic(b"Unauthorized");
}

if let Some(enforce_owner_id) = enforce_owner_id {
assert_eq!(
&owner_id,
enforce_owner_id.as_ref(),
"The token owner is different from enforced"
);
}

assert_ne!(
sender_id, receiver_id,
"Sender and receiver should be different"
&owner_id, receiver_id,
"The token owner and the receiver should be different"
);
let mut token = self.tokens.get(token_id).expect("Token not found");
assert_eq!(sender_id, &token.owner_id, "Sender doesn't own the token");
self.internal_remove_token_from_owner(sender_id, token_id);
self.internal_add_token_to_owner(receiver_id, token_id);
token.owner_id = receiver_id.clone();
self.tokens.insert(token_id, &token);

env::log(
format!(
"Transfer {} from {} to {}",
token_id, sender_id, receiver_id
"Transfer {} from @{} to @{}",
token_id, &owner_id, receiver_id
)
.as_bytes(),
);

self.internal_remove_token_from_owner(&owner_id, token_id);
self.internal_add_token_to_owner(receiver_id, token_id);

let token = Token {
owner_id: receiver_id.clone(),
metadata,
approved_account_ids: Default::default(),
};
self.tokens_by_id.insert(token_id, &token);

if let Some(memo) = memo {
env::log(format!("Memo: {}", memo).as_bytes());
}

(owner_id, approved_account_ids)
}
}
40 changes: 26 additions & 14 deletions nft-simple/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::collections::HashSet;

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::{LookupMap, UnorderedSet};
use near_sdk::collections::{LookupMap, UnorderedMap, UnorderedSet};
use near_sdk::json_types::ValidAccountId;
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, near_bindgen, AccountId, Balance, PanicOnDefault, Promise, StorageUsage};
Expand All @@ -21,16 +23,16 @@ pub type TokenId = String;
#[serde(crate = "near_sdk::serde")]
pub struct Token {
pub owner_id: AccountId,
pub meta: String,
pub metadata: String,
pub approved_account_ids: HashSet<AccountId>,
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
/// AccountID -> Account balance.
pub accounts: LookupMap<AccountId, UnorderedSet<TokenId>>,
pub tokens_per_owner: LookupMap<AccountId, UnorderedSet<TokenId>>,

pub tokens: LookupMap<TokenId, Token>,
pub tokens_by_id: UnorderedMap<TokenId, Token>,

pub owner_id: AccountId,

Expand All @@ -46,20 +48,30 @@ impl Contract {
pub fn new(owner_id: ValidAccountId) -> Self {
assert!(!env::state_exists(), "Already initialized");
let mut this = Self {
accounts: LookupMap::new(b"a".to_vec()),
tokens: LookupMap::new(b"t".to_vec()),
tokens_per_owner: LookupMap::new(b"a".to_vec()),
tokens_by_id: UnorderedMap::new(b"t".to_vec()),
owner_id: owner_id.into(),
total_supply: 0,
extra_storage_in_bytes_per_token: 0,
};

let initial_storage_usage = env::storage_usage();
let tmp_account_id = unsafe { String::from_utf8_unchecked(vec![b'a'; 64]) };
let mut u = UnorderedSet::new(prefix(&tmp_account_id));
u.insert(&tmp_account_id);
this.extra_storage_in_bytes_per_token = env::storage_usage() - initial_storage_usage
+ (tmp_account_id.len() - this.owner_id.len()) as u64;
this.accounts.remove(&tmp_account_id);
this.measure_min_token_storage_cost();

this
}

fn measure_min_token_storage_cost(&mut self) {
let initial_storage_usage = env::storage_usage();
let tmp_account_id = "a".repeat(64);
let u = UnorderedSet::new(unique_prefix(&tmp_account_id));
self.tokens_per_owner.insert(&tmp_account_id, &u);

let tokens_per_owner_entry_in_bytes = env::storage_usage() - initial_storage_usage;
let owner_id_extra_cost_in_bytes = (tmp_account_id.len() - self.owner_id.len()) as u64;

self.extra_storage_in_bytes_per_token =
tokens_per_owner_entry_in_bytes + owner_id_extra_cost_in_bytes;

self.tokens_per_owner.remove(&tmp_account_id);
}
}
15 changes: 10 additions & 5 deletions nft-simple/src/mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@ use crate::*;
#[near_bindgen]
impl Contract {
#[payable]
pub fn nft_mint(&mut self, token_id: TokenId, meta: String) {
assert_one_yocto();
pub fn nft_mint(&mut self, token_id: TokenId, metadata: String) {
let initial_storage_usage = env::storage_usage();
self.assert_owner();
let token = Token {
owner_id: self.owner_id.clone(),
mikedotexe marked this conversation as resolved.
Show resolved Hide resolved
meta,
metadata,
approved_account_ids: Default::default(),
};
assert!(
self.tokens.insert(&token_id, &token).is_none(),
self.tokens_by_id.insert(&token_id, &token).is_none(),
"Token already exists"
);
self.internal_add_token_to_owner(&token.owner_id, &token_id);
self.total_supply += 1;

self.assert_enough_storage();
let new_token_size_in_bytes = env::storage_usage() - initial_storage_usage;
let required_storage_in_bytes =
self.extra_storage_in_bytes_per_token + new_token_size_in_bytes;

deposit_refund(required_storage_in_bytes);
}
}
Loading