use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::sync::Arc;
use serde::Serialize;
use tokio::sync::OnceCell;
use crate::client::{CallBuilder, RpcClient, Signer, TransactionBuilder};
use crate::error::Error;
use crate::types::{
AccountId, Action, BlockReference, Finality, Gas, IntoNearToken, NearToken, Transaction,
TxExecutionStatus,
};
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
}
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 AsRef<str>) -> Result<FtAmount, Error> {
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_ref(),
})?;
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(crate::error::RpcError::InvalidResponse(format!(
"Invalid balance format: {}",
balance_str
)))
})?;
Ok(FtAmount::from_metadata(raw, metadata))
}
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(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 AsRef<str>) -> 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 AsRef<str>,
) -> Result<Option<StorageBalance>, Error> {
#[derive(Serialize)]
struct Args<'a> {
account_id: &'a str,
}
let args = serde_json::to_vec(&Args {
account_id: account_id.as_ref(),
})?;
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 fn storage_deposit(&self, account_id: impl AsRef<str>) -> StorageDepositCall {
StorageDepositCall::new(
self.rpc.clone(),
self.signer.clone(),
self.contract_id.clone(),
Some(account_id.as_ref().to_string()),
self.storage_bounds.clone(),
)
}
pub fn transfer(&self, receiver_id: impl AsRef<str>, amount: impl Into<u128>) -> CallBuilder {
#[derive(Serialize)]
struct TransferArgs {
receiver_id: String,
amount: String,
}
self.transaction()
.call("ft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.as_ref().to_string(),
amount: amount.into().to_string(),
})
.deposit(NearToken::yocto(1))
.gas(Gas::tgas(30))
}
pub fn transfer_with_memo(
&self,
receiver_id: impl AsRef<str>,
amount: impl Into<u128>,
memo: impl Into<String>,
) -> CallBuilder {
#[derive(Serialize)]
struct TransferArgs {
receiver_id: String,
amount: String,
memo: String,
}
self.transaction()
.call("ft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.as_ref().to_string(),
amount: amount.into().to_string(),
memo: memo.into(),
})
.deposit(NearToken::yocto(1))
.gas(Gas::tgas(30))
}
pub fn transfer_call(
&self,
receiver_id: impl AsRef<str>,
amount: impl Into<u128>,
msg: impl Into<String>,
) -> CallBuilder {
#[derive(Serialize)]
struct TransferCallArgs {
receiver_id: String,
amount: String,
msg: String,
}
self.transaction()
.call("ft_transfer_call")
.args(TransferCallArgs {
receiver_id: receiver_id.as_ref().to_string(),
amount: amount.into().to_string(),
msg: msg.into(),
})
.deposit(NearToken::yocto(1))
.gas(Gas::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()
}
}
pub struct StorageDepositCall {
rpc: Arc<RpcClient>,
signer: Option<Arc<dyn Signer>>,
contract_id: AccountId,
account_id: Option<String>,
deposit: Option<NearToken>,
registration_only: bool,
storage_bounds: OnceCell<StorageBalanceBounds>,
signer_override: Option<Arc<dyn Signer>>,
wait_until: TxExecutionStatus,
}
impl StorageDepositCall {
fn new(
rpc: Arc<RpcClient>,
signer: Option<Arc<dyn Signer>>,
contract_id: AccountId,
account_id: Option<String>,
storage_bounds: OnceCell<StorageBalanceBounds>,
) -> Self {
Self {
rpc,
signer,
contract_id,
account_id,
deposit: None,
registration_only: true,
storage_bounds,
signer_override: None,
wait_until: TxExecutionStatus::ExecutedOptimistic,
}
}
pub fn deposit(mut self, amount: impl IntoNearToken) -> Self {
self.deposit = Some(
amount
.into_near_token()
.expect("invalid deposit amount - use NearToken::from_str() for user input"),
);
self
}
pub fn registration_only(mut self, value: bool) -> Self {
self.registration_only = value;
self
}
pub fn sign_with(mut self, signer: impl Signer + 'static) -> Self {
self.signer_override = Some(Arc::new(signer));
self
}
pub fn wait_until(mut self, status: TxExecutionStatus) -> Self {
self.wait_until = status;
self
}
}
impl IntoFuture for StorageDepositCall {
type Output = Result<StorageBalance, Error>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let signer = self
.signer_override
.as_ref()
.or(self.signer.as_ref())
.ok_or(Error::NoSigner)?;
let signer_id = signer.account_id().clone();
let deposit = if let Some(d) = self.deposit {
d
} else {
let bounds = 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?;
bounds.min
};
#[derive(Serialize)]
struct DepositArgs {
#[serde(skip_serializing_if = "Option::is_none")]
account_id: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
registration_only: bool,
}
let args = serde_json::to_vec(&DepositArgs {
account_id: self.account_id,
registration_only: self.registration_only,
})?;
let key = signer.key();
let public_key = key.public_key().clone();
let access_key = self
.rpc
.view_access_key(
&signer_id,
&public_key,
BlockReference::Finality(Finality::Optimistic),
)
.await?;
let block = self
.rpc
.block(BlockReference::Finality(Finality::Final))
.await?;
let tx = Transaction::new(
signer_id,
public_key,
access_key.nonce + 1,
self.contract_id,
block.header.hash,
vec![Action::function_call(
"storage_deposit".to_string(),
args,
Gas::tgas(30),
deposit,
)],
);
let signature = key.sign(tx.get_hash().as_bytes()).await?;
let signed_tx = crate::types::SignedTransaction {
transaction: tx,
signature,
};
let response = self.rpc.send_tx(&signed_tx, self.wait_until).await?;
let outcome = response.outcome.ok_or_else(|| {
Error::InvalidTransaction(format!(
"Transaction {} submitted with wait_until={:?} but no execution \
outcome was returned. Use rpc().send_tx() for fire-and-forget \
submission.",
response.transaction_hash, self.wait_until,
))
})?;
if outcome.is_failure() {
return Err(Error::TransactionFailed(
outcome.failure_message().unwrap_or_default(),
));
}
let return_value = outcome
.success_value()
.ok_or_else(|| Error::TransactionFailed("No return value".to_string()))?;
let storage_balance: StorageBalance = serde_json::from_slice(&return_value)?;
Ok(storage_balance)
})
}
}