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, BlockReference, Finality, Gas, NearToken};
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
}
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> {
#[derive(Serialize)]
struct Args<'a> {
token_id: &'a str,
}
let args = serde_json::to_vec(&Args {
token_id: token_id.as_ref(),
})?;
let result = self
.rpc
.view_function(
&self.contract_id,
"nft_token",
&args,
BlockReference::Finality(Finality::Optimistic),
)
.await?;
result.json().map_err(Error::from)
}
pub async fn tokens_for_owner(
&self,
account_id: impl AsRef<str>,
from_index: Option<u64>,
limit: Option<u64>,
) -> Result<Vec<NftToken>, Error> {
#[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_ref(),
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)
}
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(crate::error::RpcError::InvalidResponse(format!(
"Invalid supply format: {}",
supply_str
)))
})
}
pub async fn supply_for_owner(&self, account_id: impl AsRef<str>) -> Result<u64, 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,
"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(crate::error::RpcError::InvalidResponse(format!(
"Invalid supply format: {}",
supply_str
)))
})
}
pub fn transfer(&self, receiver_id: impl AsRef<str>, token_id: impl AsRef<str>) -> CallBuilder {
#[derive(Serialize)]
struct TransferArgs {
receiver_id: String,
token_id: String,
}
self.transaction()
.call("nft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.as_ref().to_string(),
token_id: token_id.as_ref().to_string(),
})
.deposit(NearToken::yocto(1))
.gas(Gas::tgas(30))
}
pub fn transfer_with_memo(
&self,
receiver_id: impl AsRef<str>,
token_id: impl AsRef<str>,
memo: impl Into<String>,
) -> CallBuilder {
#[derive(Serialize)]
struct TransferArgs {
receiver_id: String,
token_id: String,
memo: String,
}
self.transaction()
.call("nft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.as_ref().to_string(),
token_id: token_id.as_ref().to_string(),
memo: memo.into(),
})
.deposit(NearToken::yocto(1))
.gas(Gas::tgas(30))
}
pub fn transfer_with_approval(
&self,
receiver_id: impl AsRef<str>,
token_id: impl AsRef<str>,
approval_id: u64,
) -> CallBuilder {
#[derive(Serialize)]
struct TransferArgs {
receiver_id: String,
token_id: String,
approval_id: u64,
}
self.transaction()
.call("nft_transfer")
.args(TransferArgs {
receiver_id: receiver_id.as_ref().to_string(),
token_id: token_id.as_ref().to_string(),
approval_id,
})
.deposit(NearToken::yocto(1))
.gas(Gas::tgas(30))
}
pub fn transfer_call(
&self,
receiver_id: impl AsRef<str>,
token_id: impl AsRef<str>,
msg: impl Into<String>,
) -> CallBuilder {
#[derive(Serialize)]
struct TransferCallArgs {
receiver_id: String,
token_id: String,
msg: String,
}
self.transaction()
.call("nft_transfer_call")
.args(TransferCallArgs {
receiver_id: receiver_id.as_ref().to_string(),
token_id: token_id.as_ref().to_string(),
msg: msg.into(),
})
.deposit(NearToken::yocto(1))
.gas(Gas::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()
}
}