use super::resolver::NonFungibleTokenResolver;
use crate::non_fungible_token::core::receiver::ext_nft_receiver;
use crate::non_fungible_token::core::resolver::ext_nft_resolver;
use crate::non_fungible_token::core::NonFungibleTokenCore;
use crate::non_fungible_token::events::{NftMint, NftTransfer};
use crate::non_fungible_token::metadata::TokenMetadata;
use crate::non_fungible_token::token::{Token, TokenId};
use crate::non_fungible_token::utils::{refund_approved_account_ids, refund_deposit_to_account};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::{LookupMap, TreeMap, UnorderedSet};
use near_sdk::json_types::Base64VecU8;
use near_sdk::{
assert_one_yocto, env, require, AccountId, BorshStorageKey, Gas, IntoStorageKey,
PromiseOrValue, PromiseResult, StorageUsage,
};
use std::collections::HashMap;
const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(5_000_000_000_000);
const GAS_FOR_NFT_TRANSFER_CALL: Gas = Gas(25_000_000_000_000 + GAS_FOR_RESOLVE_TRANSFER.0);
#[derive(BorshDeserialize, BorshSerialize)]
pub struct NonFungibleToken {
pub owner_id: AccountId,
pub extra_storage_in_bytes_per_token: StorageUsage,
pub owner_by_id: TreeMap<TokenId, AccountId>,
pub token_metadata_by_id: Option<LookupMap<TokenId, TokenMetadata>>,
pub tokens_per_owner: Option<LookupMap<AccountId, UnorderedSet<TokenId>>>,
pub approvals_by_id: Option<LookupMap<TokenId, HashMap<AccountId, u64>>>,
pub next_approval_id_by_id: Option<LookupMap<TokenId, u64>>,
}
#[derive(BorshStorageKey, BorshSerialize)]
pub enum StorageKey {
TokensPerOwner { account_hash: Vec<u8> },
}
impl NonFungibleToken {
pub fn new<Q, R, S, T>(
owner_by_id_prefix: Q,
owner_id: AccountId,
token_metadata_prefix: Option<R>,
enumeration_prefix: Option<S>,
approval_prefix: Option<T>,
) -> Self
where
Q: IntoStorageKey,
R: IntoStorageKey,
S: IntoStorageKey,
T: IntoStorageKey,
{
let (approvals_by_id, next_approval_id_by_id) = if let Some(prefix) = approval_prefix {
let prefix: Vec<u8> = prefix.into_storage_key();
(
Some(LookupMap::new(prefix.clone())),
Some(LookupMap::new([prefix, "n".into()].concat())),
)
} else {
(None, None)
};
let mut this = Self {
owner_id,
extra_storage_in_bytes_per_token: 0,
owner_by_id: TreeMap::new(owner_by_id_prefix),
token_metadata_by_id: token_metadata_prefix.map(LookupMap::new),
tokens_per_owner: enumeration_prefix.map(LookupMap::new),
approvals_by_id,
next_approval_id_by_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_token_id = "a".repeat(64);
let tmp_owner_id = AccountId::new_unchecked("a".repeat(64));
self.owner_by_id.insert(&tmp_token_id, &tmp_owner_id);
if let Some(token_metadata_by_id) = &mut self.token_metadata_by_id {
token_metadata_by_id.insert(
&tmp_token_id,
&TokenMetadata {
title: Some("a".repeat(64)),
description: Some("a".repeat(64)),
media: Some("a".repeat(64)),
media_hash: Some(Base64VecU8::from("a".repeat(64).as_bytes().to_vec())),
copies: Some(1),
issued_at: None,
expires_at: None,
starts_at: None,
updated_at: None,
extra: None,
reference: None,
reference_hash: None,
},
);
}
if let Some(tokens_per_owner) = &mut self.tokens_per_owner {
let u = &mut UnorderedSet::new(StorageKey::TokensPerOwner {
account_hash: env::sha256(tmp_owner_id.as_bytes()),
});
u.insert(&tmp_token_id);
tokens_per_owner.insert(&tmp_owner_id, u);
}
if let Some(approvals_by_id) = &mut self.approvals_by_id {
let mut approvals = HashMap::new();
approvals.insert(tmp_owner_id.clone(), 1u64);
approvals_by_id.insert(&tmp_token_id, &approvals);
}
if let Some(next_approval_id_by_id) = &mut self.next_approval_id_by_id {
next_approval_id_by_id.insert(&tmp_token_id, &1u64);
}
self.extra_storage_in_bytes_per_token = env::storage_usage() - initial_storage_usage;
if let Some(next_approval_id_by_id) = &mut self.next_approval_id_by_id {
next_approval_id_by_id.remove(&tmp_token_id);
}
if let Some(approvals_by_id) = &mut self.approvals_by_id {
approvals_by_id.remove(&tmp_token_id);
}
if let Some(tokens_per_owner) = &mut self.tokens_per_owner {
let mut u = tokens_per_owner.remove(&tmp_owner_id).unwrap();
u.remove(&tmp_token_id);
}
if let Some(token_metadata_by_id) = &mut self.token_metadata_by_id {
token_metadata_by_id.remove(&tmp_token_id);
}
self.owner_by_id.remove(&tmp_token_id);
}
pub fn internal_transfer_unguarded(
&mut self,
#[allow(clippy::ptr_arg)] token_id: &TokenId,
from: &AccountId,
to: &AccountId,
) {
self.owner_by_id.insert(token_id, to);
if let Some(tokens_per_owner) = &mut self.tokens_per_owner {
let mut owner_tokens = tokens_per_owner.get(from).unwrap_or_else(|| {
env::panic_str("Unable to access tokens per owner in unguarded call.")
});
owner_tokens.remove(token_id);
if owner_tokens.is_empty() {
tokens_per_owner.remove(from);
} else {
tokens_per_owner.insert(from, &owner_tokens);
}
let mut receiver_tokens = tokens_per_owner.get(to).unwrap_or_else(|| {
UnorderedSet::new(StorageKey::TokensPerOwner {
account_hash: env::sha256(to.as_bytes()),
})
});
receiver_tokens.insert(token_id);
tokens_per_owner.insert(to, &receiver_tokens);
}
}
pub fn internal_transfer(
&mut self,
sender_id: &AccountId,
receiver_id: &AccountId,
#[allow(clippy::ptr_arg)] token_id: &TokenId,
approval_id: Option<u64>,
memo: Option<String>,
) -> (AccountId, Option<HashMap<AccountId, u64>>) {
let owner_id =
self.owner_by_id.get(token_id).unwrap_or_else(|| env::panic_str("Token not found"));
let approved_account_ids =
self.approvals_by_id.as_mut().and_then(|by_id| by_id.remove(token_id));
let sender_id = if sender_id != &owner_id {
let app_acc_ids =
approved_account_ids.as_ref().unwrap_or_else(|| env::panic_str("Unauthorized"));
let actual_approval_id = app_acc_ids.get(sender_id);
if actual_approval_id.is_none() {
env::panic_str("Sender not approved");
}
require!(
approval_id.is_none() || actual_approval_id == approval_id.as_ref(),
format!(
"The actual approval_id {:?} is different from the given approval_id {:?}",
actual_approval_id, approval_id
)
);
Some(sender_id)
} else {
None
};
require!(&owner_id != receiver_id, "Current and next owner must differ");
self.internal_transfer_unguarded(token_id, &owner_id, receiver_id);
NonFungibleToken::emit_transfer(&owner_id, receiver_id, token_id, sender_id, memo);
(owner_id, approved_account_ids)
}
fn emit_transfer(
owner_id: &AccountId,
receiver_id: &AccountId,
token_id: &str,
sender_id: Option<&AccountId>,
memo: Option<String>,
) {
NftTransfer {
old_owner_id: owner_id,
new_owner_id: receiver_id,
token_ids: &[token_id],
authorized_id: sender_id.filter(|sender_id| *sender_id == owner_id),
memo: memo.as_deref(),
}
.emit();
}
#[deprecated(since = "4.0.0", note = "mint is deprecated, please use internal_mint instead.")]
pub fn mint(
&mut self,
token_id: TokenId,
token_owner_id: AccountId,
token_metadata: Option<TokenMetadata>,
) -> Token {
assert_eq!(env::predecessor_account_id(), self.owner_id, "Unauthorized");
self.internal_mint(token_id, token_owner_id, token_metadata)
}
pub fn internal_mint(
&mut self,
token_id: TokenId,
token_owner_id: AccountId,
token_metadata: Option<TokenMetadata>,
) -> Token {
let token = self.internal_mint_with_refund(
token_id,
token_owner_id,
token_metadata,
Some(env::predecessor_account_id()),
);
NftMint { owner_id: &token.owner_id, token_ids: &[&token.token_id], memo: None }.emit();
token
}
pub fn internal_mint_with_refund(
&mut self,
token_id: TokenId,
token_owner_id: AccountId,
token_metadata: Option<TokenMetadata>,
refund_id: Option<AccountId>,
) -> Token {
let initial_storage_usage = refund_id.map(|account_id| (account_id, env::storage_usage()));
if self.token_metadata_by_id.is_some() && token_metadata.is_none() {
env::panic_str("Must provide metadata");
}
if self.owner_by_id.get(&token_id).is_some() {
env::panic_str("token_id must be unique");
}
let owner_id: AccountId = token_owner_id;
self.owner_by_id.insert(&token_id, &owner_id);
self.token_metadata_by_id
.as_mut()
.and_then(|by_id| by_id.insert(&token_id, token_metadata.as_ref().unwrap()));
if let Some(tokens_per_owner) = &mut self.tokens_per_owner {
let mut token_ids = tokens_per_owner.get(&owner_id).unwrap_or_else(|| {
UnorderedSet::new(StorageKey::TokensPerOwner {
account_hash: env::sha256(owner_id.as_bytes()),
})
});
token_ids.insert(&token_id);
tokens_per_owner.insert(&owner_id, &token_ids);
}
let approved_account_ids =
if self.approvals_by_id.is_some() { Some(HashMap::new()) } else { None };
if let Some((id, storage_usage)) = initial_storage_usage {
refund_deposit_to_account(env::storage_usage() - storage_usage, id)
}
Token { token_id, owner_id, metadata: token_metadata, approved_account_ids }
}
}
impl NonFungibleTokenCore for NonFungibleToken {
fn nft_transfer(
&mut self,
receiver_id: AccountId,
token_id: TokenId,
approval_id: Option<u64>,
memo: Option<String>,
) {
assert_one_yocto();
let sender_id = env::predecessor_account_id();
self.internal_transfer(&sender_id, &receiver_id, &token_id, approval_id, memo);
}
fn nft_transfer_call(
&mut self,
receiver_id: AccountId,
token_id: TokenId,
approval_id: Option<u64>,
memo: Option<String>,
msg: String,
) -> PromiseOrValue<bool> {
assert_one_yocto();
require!(env::prepaid_gas() > GAS_FOR_NFT_TRANSFER_CALL, "More gas is required");
let sender_id = env::predecessor_account_id();
let (old_owner, old_approvals) =
self.internal_transfer(&sender_id, &receiver_id, &token_id, approval_id, memo);
ext_nft_receiver::ext(receiver_id.clone())
.with_static_gas(env::prepaid_gas() - GAS_FOR_NFT_TRANSFER_CALL)
.nft_on_transfer(sender_id, old_owner.clone(), token_id.clone(), msg)
.then(
ext_nft_resolver::ext(env::current_account_id())
.with_static_gas(GAS_FOR_RESOLVE_TRANSFER)
.nft_resolve_transfer(old_owner, receiver_id, token_id, old_approvals),
)
.into()
}
fn nft_token(&self, token_id: TokenId) -> Option<Token> {
let owner_id = self.owner_by_id.get(&token_id)?;
let metadata = self.token_metadata_by_id.as_ref().and_then(|by_id| by_id.get(&token_id));
let approved_account_ids = self
.approvals_by_id
.as_ref()
.and_then(|by_id| by_id.get(&token_id).or_else(|| Some(HashMap::new())));
Some(Token { token_id, owner_id, metadata, approved_account_ids })
}
}
impl NonFungibleTokenResolver for NonFungibleToken {
fn nft_resolve_transfer(
&mut self,
previous_owner_id: AccountId,
receiver_id: AccountId,
token_id: TokenId,
approved_account_ids: Option<HashMap<AccountId, u64>>,
) -> bool {
let must_revert = match env::promise_result(0) {
PromiseResult::NotReady => env::abort(),
PromiseResult::Successful(value) => {
if let Ok(yes_or_no) = near_sdk::serde_json::from_slice::<bool>(&value) {
yes_or_no
} else {
true
}
}
PromiseResult::Failed => true,
};
if !must_revert {
return true;
}
if let Some(current_owner) = self.owner_by_id.get(&token_id) {
if current_owner != receiver_id {
return true;
}
} else {
if let Some(approved_account_ids) = approved_account_ids {
refund_approved_account_ids(previous_owner_id, &approved_account_ids);
}
return true;
};
self.internal_transfer_unguarded(&token_id, &receiver_id, &previous_owner_id);
if let Some(by_id) = &mut self.approvals_by_id {
if let Some(receiver_approvals) = by_id.get(&token_id) {
refund_approved_account_ids(receiver_id.clone(), &receiver_approvals);
}
if let Some(previous_owner_approvals) = approved_account_ids {
by_id.insert(&token_id, &previous_owner_approvals);
}
}
NonFungibleToken::emit_transfer(&receiver_id, &previous_owner_id, &token_id, None, None);
false
}
}