use crate::ft::{ FungibleTokenCore, FtTransfer, FungibleTokenResolver };
use near_sdk::borsh::{ self, BorshDeserialize, BorshSerialize };
use near_sdk::collections::LookupMap;
use near_sdk::json_types::U128;
use near_sdk::{
env,
ext_contract,
log,
require,
AccountId,
Balance,
Gas,
IntoStorageKey,
PromiseOrValue,
PromiseResult,
StorageUsage,
};
use crate::ft::utils::assert_at_least_one_yocto;
const GAS_FOR_FT_RESOLVE_TRANSFER: Gas = Gas(5_000_000_000_000);
pub const GAS_FOR_FT_TRANSFER_CALL: Gas = Gas(25_000_000_000_000 + GAS_FOR_FT_RESOLVE_TRANSFER.0);
pub const GAS_FOR_FT_TRANSFER: Gas = Gas(5_000_000_000_000);
#[ext_contract(ext_self)]
trait ExtFungibleTokenResolver {
fn ft_resolve_transfer(
&mut self,
sender_id: AccountId,
receiver_id: AccountId,
amount: U128
) -> U128;
}
#[ext_contract(ext_fungible_token_receiver)]
pub trait FungibleTokenReceiver {
fn ft_on_transfer(
&mut self,
sender_id: AccountId,
amount: U128,
msg: String
) -> PromiseOrValue<U128>;
}
#[derive(BorshDeserialize, BorshSerialize)]
pub struct FungibleToken {
pub accounts: LookupMap<AccountId, Balance>,
pub total_supply: Balance,
pub account_storage_usage: StorageUsage,
}
impl FungibleToken {
pub fn new<S>(prefix: S) -> Self where S: IntoStorageKey {
let mut this = Self {
accounts: LookupMap::new(prefix),
total_supply: 0,
account_storage_usage: 0,
};
this.measure_account_storage_usage();
this
}
fn measure_account_storage_usage(&mut self) {
let initial_storage_usage = env::storage_usage();
let tmp_account_id = AccountId::new_unchecked("a".repeat(64));
self.accounts.insert(&tmp_account_id, &0u128);
self.account_storage_usage = env::storage_usage() - initial_storage_usage;
self.accounts.remove(&tmp_account_id);
}
pub fn internal_unwrap_balance_of(&self, account_id: &AccountId) -> Balance {
match self.accounts.get(account_id) {
Some(balance) => balance,
None => {
env::panic_str(format!("The account {} is not registered", &account_id).as_str())
}
}
}
pub fn internal_deposit(&mut self, account_id: &AccountId, amount: Balance) {
let balance = self.internal_unwrap_balance_of(account_id);
if let Some(new_balance) = balance.checked_add(amount) {
self.accounts.insert(account_id, &new_balance);
self.total_supply = self.total_supply
.checked_add(amount)
.unwrap_or_else(|| env::panic_str("Total supply overflow"));
} else {
env::panic_str("Balance overflow");
}
}
pub fn internal_withdraw(&mut self, account_id: &AccountId, amount: Balance) {
let balance = self.internal_unwrap_balance_of(account_id);
if let Some(new_balance) = balance.checked_sub(amount) {
self.accounts.insert(account_id, &new_balance);
self.total_supply = self.total_supply
.checked_sub(amount)
.unwrap_or_else(|| env::panic_str("Total supply overflow"));
} else {
env::panic_str("The account doesn't have enough balance");
}
}
pub fn internal_transfer(
&mut self,
sender_id: &AccountId,
receiver_id: &AccountId,
amount: Balance,
memo: Option<String>
) {
require!(sender_id != receiver_id, "Sender and receiver should be different");
require!(amount > 0, "The amount should be a positive number");
self.internal_withdraw(sender_id, amount);
self.internal_deposit(receiver_id, amount);
(FtTransfer {
old_owner_id: sender_id,
new_owner_id: receiver_id,
amount: &U128(amount),
memo: memo.as_deref(),
}).emit();
}
pub fn internal_register_account(&mut self, account_id: &AccountId) {
if self.accounts.insert(account_id, &0).is_some() {
env::panic_str("The account is already registered");
}
}
}
impl FungibleTokenCore for FungibleToken {
fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option<String>) {
assert_at_least_one_yocto();
let sender_id = env::predecessor_account_id();
let amount: Balance = amount.into();
self.internal_transfer(&sender_id, &receiver_id, amount, memo);
}
fn ft_transfer_call(
&mut self,
receiver_id: AccountId,
amount: U128,
memo: Option<String>,
msg: String
) -> PromiseOrValue<U128> {
assert_at_least_one_yocto();
require!(
env::prepaid_gas() > GAS_FOR_FT_TRANSFER_CALL + GAS_FOR_FT_RESOLVE_TRANSFER,
"More gas is required"
);
let sender_id = env::predecessor_account_id();
let amount: Balance = amount.into();
self.internal_transfer(&sender_id, &receiver_id, amount, memo);
ext_fungible_token_receiver
::ext(receiver_id.clone())
.with_static_gas(env::prepaid_gas() - GAS_FOR_FT_TRANSFER_CALL)
.ft_on_transfer(sender_id.clone(), amount.into(), msg)
.then(
ext_self
::ext(env::current_account_id())
.with_static_gas(GAS_FOR_FT_RESOLVE_TRANSFER)
.ft_resolve_transfer(sender_id, receiver_id, amount.into())
)
.into()
}
fn ft_total_supply(&self) -> U128 {
self.total_supply.into()
}
fn ft_balance_of(&self, account_id: AccountId) -> U128 {
self.accounts.get(&account_id).unwrap_or(0).into()
}
}
impl FungibleToken {
pub fn internal_ft_resolve_transfer(
&mut self,
sender_id: &AccountId,
receiver_id: AccountId,
amount: U128
) -> (u128, u128) {
let amount: Balance = amount.into();
let unused_amount = match env::promise_result(0) {
PromiseResult::NotReady => env::abort(),
PromiseResult::Successful(value) => {
if let Ok(unused_amount) = near_sdk::serde_json::from_slice::<U128>(&value) {
std::cmp::min(amount, unused_amount.0)
} else {
amount
}
}
PromiseResult::Failed => amount,
};
if unused_amount > 0 {
let receiver_balance = self.accounts.get(&receiver_id).unwrap_or(0);
if receiver_balance > 0 {
let refund_amount = std::cmp::min(receiver_balance, unused_amount);
self.accounts.insert(&receiver_id, &(receiver_balance - refund_amount));
if let Some(sender_balance) = self.accounts.get(sender_id) {
self.accounts.insert(sender_id, &(sender_balance + refund_amount));
log!("Refund {} from {} to {}", refund_amount, receiver_id, sender_id);
return (amount - refund_amount, 0);
} else {
self.total_supply -= refund_amount;
log!("The account of the sender was deleted");
return (amount, refund_amount);
}
}
}
(amount, 0)
}
}
impl FungibleTokenResolver for FungibleToken {
fn ft_resolve_transfer(
&mut self,
sender_id: AccountId,
receiver_id: AccountId,
amount: U128
) -> U128 {
self.internal_ft_resolve_transfer(&sender_id, receiver_id, amount).0.into()
}
}