use near_api_types::{
AccountId, Action, Data, NearToken, Reference,
ft::FungibleTokenMetadata,
json::U128,
nft::{NFTContractMetadata, Token},
tokens::{FTBalance, STORAGE_COST_PER_BYTE, UserBalance},
transaction::PrepopulateTransaction,
transaction::actions::TransferAction,
};
use serde_json::json;
use crate::{
NetworkConfig, StorageDeposit,
advanced::{query_request::QueryRequest, query_rpc::SimpleQueryRpc},
common::{
query::{
AccountViewHandler, CallResultHandler, MultiQueryHandler, MultiRequestBuilder,
PostprocessHandler, RequestBuilder,
},
send::Transactionable,
},
contract::Contract,
errors::{ArgumentValidationError, FTValidatorError, ValidationError},
transactions::{ConstructTransaction, TransactionWithSign},
};
#[allow(clippy::too_long_first_doc_paragraph)]
#[derive(Debug, Clone)]
pub struct Tokens {
account_id: AccountId,
}
impl Tokens {
pub const fn account(account_id: AccountId) -> Self {
Self { account_id }
}
pub const fn account_id(&self) -> &AccountId {
&self.account_id
}
pub fn as_account(&self) -> crate::account::Account {
crate::account::Account(self.account_id.clone())
}
pub fn near_balance(
&self,
) -> RequestBuilder<PostprocessHandler<UserBalance, AccountViewHandler>> {
let request = QueryRequest::ViewAccount {
account_id: self.account_id.clone(),
};
RequestBuilder::new(
SimpleQueryRpc { request },
Reference::Optimistic,
AccountViewHandler,
)
.map(|account| {
let account = account.data;
let storage_locked = NearToken::from_yoctonear(
account.storage_usage as u128 * STORAGE_COST_PER_BYTE.as_yoctonear(),
);
UserBalance {
total: account.amount,
storage_locked,
storage_usage: account.storage_usage,
locked: account.locked,
}
})
}
pub fn nft_metadata(
contract_id: AccountId,
) -> RequestBuilder<CallResultHandler<NFTContractMetadata>> {
Contract(contract_id)
.call_function("nft_metadata", ())
.read_only()
}
pub fn nft_assets(
&self,
nft_contract: AccountId,
) -> RequestBuilder<CallResultHandler<Vec<Token>>> {
Contract(nft_contract)
.call_function(
"nft_tokens_for_owner",
json!({
"account_id": self.account_id.to_string(),
}),
)
.read_only()
}
pub fn ft_metadata(
contract_id: AccountId,
) -> RequestBuilder<CallResultHandler<FungibleTokenMetadata>> {
Contract(contract_id)
.call_function("ft_metadata", ())
.read_only()
}
#[allow(clippy::type_complexity)]
pub fn ft_balance(
&self,
ft_contract: AccountId,
) -> MultiRequestBuilder<
PostprocessHandler<
FTBalance,
MultiQueryHandler<(
CallResultHandler<FungibleTokenMetadata>,
CallResultHandler<U128>,
)>,
>,
> {
let handler = MultiQueryHandler::new((
CallResultHandler::<FungibleTokenMetadata>::new(),
CallResultHandler::<U128>::new(),
));
MultiRequestBuilder::new(handler, Reference::Optimistic)
.add_query_builder(Self::ft_metadata(ft_contract.clone()))
.add_query_builder(
Contract(ft_contract)
.call_function(
"ft_balance_of",
json!({
"account_id": self.account_id.clone()
}),
)
.read_only::<()>(),
)
.map(
|(metadata, amount): (Data<FungibleTokenMetadata>, Data<U128>)| {
FTBalance::with_decimals(metadata.data.decimals).with_amount(amount.data.0)
},
)
}
pub fn send_to(&self, receiver_id: AccountId) -> SendToBuilder {
SendToBuilder {
from: self.account_id.clone(),
receiver_id,
}
}
}
#[derive(Debug, Clone)]
pub struct SendToBuilder {
from: AccountId,
receiver_id: AccountId,
}
impl SendToBuilder {
pub fn near(self, amount: NearToken) -> ConstructTransaction {
ConstructTransaction::new(self.from, self.receiver_id)
.add_action(Action::Transfer(TransferAction { deposit: amount }))
}
pub fn ft(
self,
ft_contract: AccountId,
amount: FTBalance,
) -> TransactionWithSign<FTTransactionable> {
let transaction = Contract(ft_contract)
.call_function(
"ft_transfer",
json!({
"receiver_id": self.receiver_id,
"amount": amount.amount().to_string(),
}),
)
.transaction()
.deposit(NearToken::from_yoctonear(1))
.with_signer_account(self.from);
TransactionWithSign {
tx: FTTransactionable {
receiver: self.receiver_id,
transaction: transaction.transaction,
decimals: amount.decimals(),
},
}
}
pub fn nft(self, nft_contract: AccountId, token_id: String) -> ConstructTransaction {
Contract(nft_contract)
.call_function(
"nft_transfer",
json!({
"receiver_id": self.receiver_id,
"token_id": token_id
}),
)
.transaction()
.deposit(NearToken::from_yoctonear(1))
.with_signer_account(self.from)
}
pub fn ft_call(
self,
ft_contract: AccountId,
amount: FTBalance,
msg: String,
) -> TransactionWithSign<FTTransactionable> {
let transaction = Contract(ft_contract)
.call_function(
"ft_transfer_call",
json!({
"receiver_id": self.receiver_id,
"amount": amount.amount().to_string(),
"msg": msg,
}),
)
.transaction()
.deposit(NearToken::from_yoctonear(1))
.with_signer_account(self.from);
TransactionWithSign {
tx: FTTransactionable {
receiver: self.receiver_id,
transaction: transaction.transaction,
decimals: amount.decimals(),
},
}
}
pub fn nft_call(
self,
nft_contract: AccountId,
token_id: String,
msg: String,
) -> ConstructTransaction {
Contract(nft_contract)
.call_function(
"nft_transfer_call",
json!({
"receiver_id": self.receiver_id,
"token_id": token_id,
"msg": msg,
}),
)
.transaction()
.deposit(NearToken::from_yoctonear(1))
.with_signer_account(self.from)
}
}
#[derive(Clone, Debug)]
pub struct FTTransactionable {
transaction: Result<PrepopulateTransaction, ArgumentValidationError>,
receiver: AccountId,
decimals: u8,
}
impl FTTransactionable {
pub async fn check_decimals(&self, network: &NetworkConfig) -> Result<(), ValidationError> {
let transaction = match &self.transaction {
Ok(transaction) => transaction,
Err(e) => return Err(e.to_owned().into()),
};
let metadata = Tokens::ft_metadata(transaction.receiver_id.clone());
let Ok(metadata) = metadata.fetch_from(network).await else {
return Ok(());
};
if metadata.data.decimals != self.decimals {
Err(FTValidatorError::DecimalsMismatch {
expected: metadata.data.decimals,
got: self.decimals,
})?;
}
Ok(())
}
}
#[async_trait::async_trait]
impl Transactionable for FTTransactionable {
fn prepopulated(&self) -> Result<PrepopulateTransaction, ArgumentValidationError> {
self.transaction.clone()
}
async fn validate_with_network(
&self,
network: &NetworkConfig,
) -> core::result::Result<(), ValidationError> {
self.check_decimals(network).await?;
let transaction = match &self.transaction {
Ok(transaction) => transaction,
Err(_) => return Ok(()),
};
let storage_balance = StorageDeposit::on_contract(transaction.receiver_id.clone())
.view_account_storage(self.receiver.clone())
.fetch_from(network)
.await
.map_err(ValidationError::QueryError)?;
if storage_balance.data.is_none() {
Err(FTValidatorError::StorageDepositNeeded)?;
}
Ok(())
}
async fn edit_with_network(
&mut self,
network: &NetworkConfig,
) -> core::result::Result<(), ValidationError> {
self.check_decimals(network).await?;
let transaction = match &mut self.transaction {
Ok(transaction) => transaction,
Err(_) => return Ok(()),
};
let storage_balance = StorageDeposit::on_contract(transaction.receiver_id.clone())
.view_account_storage(self.receiver.clone())
.fetch_from(network)
.await
.map_err(ValidationError::QueryError)?;
if storage_balance.data.is_none() {
let mut action = StorageDeposit::on_contract(transaction.receiver_id.clone())
.deposit(self.receiver.clone(), NearToken::from_millinear(100))
.into_transaction()
.with_signer_account(transaction.signer_id.clone())
.prepopulated()?
.actions;
action.append(&mut transaction.actions);
transaction.actions = action;
}
Ok(())
}
}