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, NearToken, TryIntoAccountId};
use super::types::{NftContractMetadata, NftToken};
pub struct NonFungibleToken {
rpc: Arc<RpcClient>,
signer: Option<Arc<dyn Signer>>,
contract_id: AccountId,
metadata: OnceCell<NftContractMetadata>,
max_nonce_retries: u32,
}
impl NonFungibleToken {
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(),
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(),
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<&NftContractMetadata, Error> {
self.metadata
.get_or_try_init(|| async {
let result = self
.rpc
.view_function(
&self.contract_id,
"nft_metadata",
&[],
BlockReference::Finality(Finality::Optimistic),
)
.await
.map_err(Error::from)?;
result.json().map_err(Error::from)
})
.await
}
pub async fn token(&self, token_id: impl AsRef<str>) -> Result<Option<NftToken>, Error> {
let token_id = token_id.as_ref();
let span = tracing::debug_span!("nft_token", contract = %self.contract_id, token_id);
async {
#[derive(Serialize)]
struct Args<'a> {
token_id: &'a str,
}
let args = serde_json::to_vec(&Args { token_id })?;
let result = self
.rpc
.view_function(
&self.contract_id,
"nft_token",
&args,
BlockReference::Finality(Finality::Optimistic),
)
.await?;
result.json().map_err(Error::from)
}
.instrument(span)
.await
}
pub async fn tokens_for_owner(
&self,
account_id: impl TryIntoAccountId,
from_index: Option<u64>,
limit: Option<u64>,
) -> Result<Vec<NftToken>, Error> {
let account_id: AccountId = account_id.try_into_account_id()?;
let span =
tracing::debug_span!("nft_tokens_for_owner", contract = %self.contract_id, %account_id);
async {
#[derive(Serialize)]
struct Args<'a> {
account_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
from_index: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u64>,
}
let args = serde_json::to_vec(&Args {
account_id: account_id.as_str(),
from_index: from_index.map(|i| i.to_string()),
limit,
})?;
let result = self
.rpc
.view_function(
&self.contract_id,
"nft_tokens_for_owner",
&args,
BlockReference::Finality(Finality::Optimistic),
)
.await?;
result.json().map_err(Error::from)
}
.instrument(span)
.await
}
pub async fn total_supply(&self) -> Result<u64, Error> {
let result = self
.rpc
.view_function(
&self.contract_id,
"nft_total_supply",
&[],
BlockReference::Finality(Finality::Optimistic),
)
.await
.map_err(Error::from)?;
let supply_str: String = result.json()?;
supply_str.parse().map_err(|_| {
Error::Rpc(Box::new(crate::error::RpcError::InvalidResponse(format!(
"Invalid supply format: {}",
supply_str
))))
})
}
pub async fn supply_for_owner(&self, account_id: impl TryIntoAccountId) -> Result<u64, 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,
"nft_supply_for_owner",
&args,
BlockReference::Finality(Finality::Optimistic),
)
.await
.map_err(Error::from)?;
let supply_str: String = result.json()?;
supply_str.parse().map_err(|_| {
Error::Rpc(Box::new(crate::error::RpcError::InvalidResponse(format!(
"Invalid supply format: {}",
supply_str
))))
})
}
pub fn transfer(
&self,
receiver_id: impl TryIntoAccountId,
token_id: impl AsRef<str>,
) -> CallBuilder {
let receiver_id: AccountId = receiver_id
.try_into_account_id()
.expect("invalid account ID");
tracing::debug!(contract = %self.contract_id, token_id = token_id.as_ref(), receiver = %receiver_id, "nft_transfer");
#[derive(Serialize)]
struct TransferArgs {
receiver_id: String,
token_id: String,
}
self.transaction()
.call("nft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.to_string(),
token_id: token_id.as_ref().to_string(),
})
.deposit(NearToken::from_yoctonear(1))
.gas(Gas::from_tgas(30))
}
pub fn transfer_with_memo(
&self,
receiver_id: impl TryIntoAccountId,
token_id: impl AsRef<str>,
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,
token_id: String,
memo: String,
}
self.transaction()
.call("nft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.to_string(),
token_id: token_id.as_ref().to_string(),
memo: memo.into(),
})
.deposit(NearToken::from_yoctonear(1))
.gas(Gas::from_tgas(30))
}
pub fn transfer_with_approval(
&self,
receiver_id: impl TryIntoAccountId,
token_id: impl AsRef<str>,
approval_id: u64,
) -> CallBuilder {
let receiver_id: AccountId = receiver_id
.try_into_account_id()
.expect("invalid account ID");
#[derive(Serialize)]
struct TransferArgs {
receiver_id: String,
token_id: String,
approval_id: u64,
}
self.transaction()
.call("nft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.to_string(),
token_id: token_id.as_ref().to_string(),
approval_id,
})
.deposit(NearToken::from_yoctonear(1))
.gas(Gas::from_tgas(30))
}
pub fn transfer_call(
&self,
receiver_id: impl TryIntoAccountId,
token_id: impl AsRef<str>,
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, token_id = token_id.as_ref(), receiver = %receiver_id, "nft_transfer_call");
#[derive(Serialize)]
struct TransferCallArgs {
receiver_id: String,
token_id: String,
msg: String,
}
self.transaction()
.call("nft_transfer_call")
.args(TransferCallArgs {
receiver_id: receiver_id.to_string(),
token_id: token_id.as_ref().to_string(),
msg: msg.into(),
})
.deposit(NearToken::from_yoctonear(1))
.gas(Gas::from_tgas(100))
}
}
impl Clone for NonFungibleToken {
fn clone(&self) -> Self {
Self {
rpc: self.rpc.clone(),
signer: self.signer.clone(),
contract_id: self.contract_id.clone(),
metadata: OnceCell::new(),
max_nonce_retries: self.max_nonce_retries,
}
}
}
impl std::fmt::Debug for NonFungibleToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NonFungibleToken")
.field("contract_id", &self.contract_id)
.field("metadata_cached", &self.metadata.initialized())
.finish()
}
}