use std::sync::Arc;
use serde::Serialize;
use tokio::sync::OnceCell;
use tracing::Instrument;
use crate::client::{CallBuilder, RpcClient, Signer, TransactionBuilder};
use crate::error::Error;
use crate::types::{
AccountId, BlockReference, Finality, Gas, IntoNearToken, NearToken, TryIntoAccountId,
};
use super::types::{FtAmount, FtMetadata, StorageBalance, StorageBalanceBounds};
pub struct FungibleToken {
rpc: Arc<RpcClient>,
signer: Option<Arc<dyn Signer>>,
contract_id: AccountId,
metadata: OnceCell<FtMetadata>,
storage_bounds: OnceCell<StorageBalanceBounds>,
max_nonce_retries: u32,
}
impl FungibleToken {
pub(crate) fn new(
rpc: Arc<RpcClient>,
signer: Option<Arc<dyn Signer>>,
contract_id: AccountId,
max_nonce_retries: u32,
) -> Self {
Self {
rpc,
signer,
contract_id,
metadata: OnceCell::new(),
storage_bounds: OnceCell::new(),
max_nonce_retries,
}
}
pub fn contract_id(&self) -> &AccountId {
&self.contract_id
}
pub fn with_signer(&self, signer: impl Signer + 'static) -> Self {
Self {
rpc: self.rpc.clone(),
signer: Some(Arc::new(signer)),
contract_id: self.contract_id.clone(),
metadata: OnceCell::new(),
storage_bounds: OnceCell::new(),
max_nonce_retries: self.max_nonce_retries,
}
}
fn transaction(&self) -> TransactionBuilder {
TransactionBuilder::new(
self.rpc.clone(),
self.signer.clone(),
self.contract_id.clone(),
self.max_nonce_retries,
)
}
pub async fn metadata(&self) -> Result<&FtMetadata, Error> {
self.metadata
.get_or_try_init(|| async {
let result = self
.rpc
.view_function(
&self.contract_id,
"ft_metadata",
&[],
BlockReference::Finality(Finality::Optimistic),
)
.await
.map_err(Error::from)?;
result.json().map_err(Error::from)
})
.await
}
pub async fn balance_of(&self, account_id: impl TryIntoAccountId) -> Result<FtAmount, Error> {
let account_id: AccountId = account_id.try_into_account_id()?;
let span = tracing::debug_span!("ft_balance_of", contract = %self.contract_id, %account_id);
async {
let metadata = self.metadata().await?;
#[derive(Serialize)]
struct Args<'a> {
account_id: &'a str,
}
let args = serde_json::to_vec(&Args {
account_id: account_id.as_str(),
})?;
let result = self
.rpc
.view_function(
&self.contract_id,
"ft_balance_of",
&args,
BlockReference::Finality(Finality::Optimistic),
)
.await?;
let balance_str: String = result.json().map_err(Error::from)?;
let raw: u128 = balance_str.parse().map_err(|_| {
Error::Rpc(Box::new(crate::error::RpcError::InvalidResponse(format!(
"Invalid balance format: {}",
balance_str
))))
})?;
Ok(FtAmount::from_metadata(raw, metadata))
}
.instrument(span)
.await
}
pub async fn total_supply(&self) -> Result<FtAmount, Error> {
let metadata = self.metadata().await?;
let result = self
.rpc
.view_function(
&self.contract_id,
"ft_total_supply",
&[],
BlockReference::Finality(Finality::Optimistic),
)
.await?;
let supply_str: String = result.json().map_err(Error::from)?;
let raw: u128 = supply_str.parse().map_err(|_| {
Error::Rpc(Box::new(crate::error::RpcError::InvalidResponse(format!(
"Invalid supply format: {}",
supply_str
))))
})?;
Ok(FtAmount::from_metadata(raw, metadata))
}
pub async fn is_registered(&self, account_id: impl TryIntoAccountId) -> Result<bool, Error> {
let balance = self.storage_balance_of(account_id).await?;
Ok(balance.is_some())
}
pub async fn storage_balance_of(
&self,
account_id: impl TryIntoAccountId,
) -> Result<Option<StorageBalance>, Error> {
let account_id: AccountId = account_id.try_into_account_id()?;
#[derive(Serialize)]
struct Args<'a> {
account_id: &'a str,
}
let args = serde_json::to_vec(&Args {
account_id: account_id.as_str(),
})?;
let result = self
.rpc
.view_function(
&self.contract_id,
"storage_balance_of",
&args,
BlockReference::Finality(Finality::Optimistic),
)
.await?;
result.json().map_err(Error::from)
}
pub async fn storage_balance_bounds(&self) -> Result<&StorageBalanceBounds, Error> {
self.storage_bounds
.get_or_try_init(|| async {
let result = self
.rpc
.view_function(
&self.contract_id,
"storage_balance_bounds",
&[],
BlockReference::Finality(Finality::Optimistic),
)
.await
.map_err(Error::from)?;
result.json::<StorageBalanceBounds>().map_err(Error::from)
})
.await
}
pub fn storage_deposit(
&self,
account_id: impl TryIntoAccountId,
deposit: impl IntoNearToken,
) -> CallBuilder {
let account_id: AccountId = account_id
.try_into_account_id()
.expect("invalid account ID");
#[derive(Serialize)]
struct DepositArgs {
account_id: String,
registration_only: bool,
}
self.transaction()
.call("storage_deposit")
.args(DepositArgs {
account_id: account_id.to_string(),
registration_only: true,
})
.deposit(deposit)
.gas(Gas::from_tgas(30))
}
pub fn transfer(
&self,
receiver_id: impl TryIntoAccountId,
amount: impl Into<u128>,
) -> CallBuilder {
let receiver_id: AccountId = receiver_id
.try_into_account_id()
.expect("invalid account ID");
tracing::debug!(contract = %self.contract_id, receiver = %receiver_id, "ft_transfer");
#[derive(Serialize)]
struct TransferArgs {
receiver_id: String,
amount: String,
}
self.transaction()
.call("ft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.to_string(),
amount: amount.into().to_string(),
})
.deposit(NearToken::from_yoctonear(1))
.gas(Gas::from_tgas(30))
}
pub fn transfer_with_memo(
&self,
receiver_id: impl TryIntoAccountId,
amount: impl Into<u128>,
memo: impl Into<String>,
) -> CallBuilder {
let receiver_id: AccountId = receiver_id
.try_into_account_id()
.expect("invalid account ID");
#[derive(Serialize)]
struct TransferArgs {
receiver_id: String,
amount: String,
memo: String,
}
self.transaction()
.call("ft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.to_string(),
amount: amount.into().to_string(),
memo: memo.into(),
})
.deposit(NearToken::from_yoctonear(1))
.gas(Gas::from_tgas(30))
}
pub fn transfer_call(
&self,
receiver_id: impl TryIntoAccountId,
amount: impl Into<u128>,
msg: impl Into<String>,
) -> CallBuilder {
let receiver_id: AccountId = receiver_id
.try_into_account_id()
.expect("invalid account ID");
tracing::debug!(contract = %self.contract_id, receiver = %receiver_id, "ft_transfer_call");
#[derive(Serialize)]
struct TransferCallArgs {
receiver_id: String,
amount: String,
msg: String,
}
self.transaction()
.call("ft_transfer_call")
.args(TransferCallArgs {
receiver_id: receiver_id.to_string(),
amount: amount.into().to_string(),
msg: msg.into(),
})
.deposit(NearToken::from_yoctonear(1))
.gas(Gas::from_tgas(100))
}
}
impl Clone for FungibleToken {
fn clone(&self) -> Self {
Self {
rpc: self.rpc.clone(),
signer: self.signer.clone(),
contract_id: self.contract_id.clone(),
metadata: OnceCell::new(),
storage_bounds: OnceCell::new(),
max_nonce_retries: self.max_nonce_retries,
}
}
}
impl std::fmt::Debug for FungibleToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FungibleToken")
.field("contract_id", &self.contract_id)
.field("metadata_cached", &self.metadata.initialized())
.finish()
}
}