near_sdk_contract_tools/standard/nep171/
mod.rs

1//! NEP-171 non-fungible token core implementation.
2//!
3//! Reference: <https://github.com/near/NEPs/blob/master/neps/nep-0171.md>
4//!
5//! # Usage
6//!
7//! It is recommended to use the [`near_sdk_contract_tools_macros::Nep171`]
8//! derive macro or the [`near_sdk_contract_tools_macros::NonFungibleToken`]
9//! macro to implement NEP-171 with this crate.
10//!
11//! ## Basic implementation with no transfer hooks
12//!
13//! ```rust
14#![doc = include_str!("../../../tests/macros/standard/nep171/no_hooks.rs")]
15//! ```
16//!
17//! ## Basic implementation with transfer hooks
18//!
19//! ```rust
20#![doc = include_str!("../../../tests/macros/standard/nep171/hooks.rs")]
21//! ```
22//!
23//! ## Using the `NonFungibleToken` derive macro for partially-automatic integration with other utilities
24//!
25//! The `NonFungibleToken` derive macro automatically wires up all of the NFT-related standards' implementations (NEP-171, NEP-177, NEP-178) for you.
26//!
27//! ```rust
28#![doc = include_str!("../../../tests/macros/standard/nep171/non_fungible_token.rs")]
29//! ```
30//!
31//! ## Manual integration with other utilities
32//!
33//! Note: NFT-related utilities are automatically integrated with each other
34//! when using the [`near_sdk_contract_tools_macros::NonFungibleToken`] derive
35//! macro.
36//! ```rust
37#![doc = include_str!("../../../tests/macros/standard/nep171/manual_integration.rs")]
38//! ```
39
40use std::error::Error;
41
42use near_sdk::{
43    borsh::BorshSerialize,
44    near,
45    serde::{Deserialize, Serialize},
46    AccountId, AccountIdRef, BorshStorageKey, Gas, NearSchema,
47};
48
49use crate::{hook::Hook, slot::Slot, standard::nep297::Event, DefaultStorageKey};
50
51pub mod action;
52use action::*;
53
54pub mod error;
55use error::*;
56pub mod event;
57use event::*;
58// separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)]
59mod ext;
60pub use ext::*;
61pub mod hooks;
62
63/// Minimum required gas for [`Nep171Resolver::nft_resolve_transfer`] call in promise chain during [`Nep171::nft_transfer_call`].
64pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas::from_gas(5_000_000_000_000);
65/// Minimum gas required to execute the main body of [`Nep171::nft_transfer_call`] + gas for [`Nep171Resolver::nft_resolve_transfer`].
66pub const GAS_FOR_NFT_TRANSFER_CALL: Gas =
67    Gas::from_gas(25_000_000_000_000 + GAS_FOR_RESOLVE_TRANSFER.as_gas());
68/// Error message when insufficient gas is attached to function calls with a minimum attached gas requirement (i.e. those that produce a promise chain, perform cross-contract calls).
69pub const INSUFFICIENT_GAS_MESSAGE: &str = "More gas is required";
70
71/// NFT token IDs.
72pub type TokenId = String;
73
74#[derive(BorshSerialize, BorshStorageKey)]
75#[borsh(crate = "near_sdk::borsh")]
76enum StorageKey<'a> {
77    TokenOwner(&'a str),
78}
79
80/// Internal (storage location) methods for implementors of [`Nep171Controller`].
81pub trait Nep171ControllerInternal {
82    /// Hook for mint operations.
83    type MintHook: for<'a> Hook<Self, Nep171Mint<'a>>
84    where
85        Self: Sized;
86    /// Hook for transfer operations.
87    type TransferHook: for<'a> Hook<Self, Nep171Transfer<'a>>
88    where
89        Self: Sized;
90    /// Hook for burn operations.
91    type BurnHook: for<'a> Hook<Self, Nep171Burn<'a>>
92    where
93        Self: Sized;
94
95    /// Invoked during an external transfer.
96    type CheckExternalTransfer: CheckExternalTransfer<Self>
97    where
98        Self: Sized;
99
100    /// Load additional token data into [`Token::extensions_metadata`].
101    type LoadTokenMetadata: LoadTokenMetadata<Self>
102    where
103        Self: Sized;
104
105    /// Root storage slot.
106    #[must_use]
107    fn root() -> Slot<()> {
108        Slot::root(DefaultStorageKey::Nep171)
109    }
110
111    /// Storage slot for the owner of a token.
112    #[must_use]
113    fn slot_token_owner(token_id: &TokenId) -> Slot<AccountId> {
114        Self::root().field(StorageKey::TokenOwner(token_id))
115    }
116}
117
118/// Non-public controller interface for NEP-171 implementations.
119pub trait Nep171Controller {
120    /// Hook for mint operations.
121    type MintHook: for<'a> Hook<Self, Nep171Mint<'a>>
122    where
123        Self: Sized;
124    /// Hook for transfer operations.
125    type TransferHook: for<'a> Hook<Self, Nep171Transfer<'a>>
126    where
127        Self: Sized;
128    /// Hook for burn operations.
129    type BurnHook: for<'a> Hook<Self, Nep171Burn<'a>>
130    where
131        Self: Sized;
132
133    /// Invoked during an external transfer.
134    type CheckExternalTransfer: CheckExternalTransfer<Self>
135    where
136        Self: Sized;
137
138    /// Load additional token data into [`Token::extensions_metadata`].
139    type LoadTokenMetadata: LoadTokenMetadata<Self>
140    where
141        Self: Sized;
142
143    /// Transfer a token from `sender_id` to `receiver_id`, as for an external
144    /// call to `nft_transfer`. Checks that the transfer is valid using
145    /// [`CheckExternalTransfer::check_external_transfer`] before performing
146    /// the transfer. Emits events and runs relevant hooks.
147    ///
148    /// # Errors
149    ///
150    /// - If the token does not exist.
151    /// - If the sender is not approved.
152    /// - If the sender is the receiver.
153    /// - If the correct account does not own the token.
154    fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError>
155    where
156        Self: Sized;
157
158    /// Performs a token transfer without running [`CheckExternalTransfer::check_external_transfer`].
159    /// Does not emit events or run hooks.
160    ///
161    /// # Warning
162    ///
163    /// This function performs _no checks_. It is up to the caller to ensure
164    /// that the transfer is valid. Possible unintended effects of invalid
165    /// transfers include:
166    ///
167    /// - Transferring a token "from" an account that does not own it.
168    /// - Creating token IDs that did not previously exist.
169    /// - Transferring a token to the account that already owns it.
170    fn transfer_unchecked(&mut self, token_ids: &[TokenId], receiver_id: &AccountIdRef);
171
172    /// Mints a new token `token_id` to `owner_id`. Emits events and runs
173    /// relevant hooks.
174    ///
175    /// # Errors
176    ///
177    /// - If the token ID already exists.
178    fn mint(&mut self, action: &Nep171Mint<'_>) -> Result<(), Nep171MintError>;
179
180    /// Mints a new token `token_id` to `owner_id` without checking if the
181    /// token already exists. Does not emit events or run hooks.
182    fn mint_unchecked(&mut self, token_ids: &[TokenId], owner_id: &AccountIdRef);
183
184    /// Burns tokens `token_ids` owned by `current_owner_id`. Emits events and
185    /// runs relevant hooks.
186    ///
187    /// # Errors
188    ///
189    /// - If the token does not exist.
190    /// - If the token is not owned by the expected owner.
191    fn burn(&mut self, action: &Nep171Burn<'_>) -> Result<(), Nep171BurnError>;
192
193    /// Burns tokens `token_ids` without checking the owners. Does not emit
194    /// events or run hooks.
195    fn burn_unchecked(&mut self, token_ids: &[TokenId]) -> bool;
196
197    /// Returns the owner of a token, if it exists.
198    fn token_owner(&self, token_id: &TokenId) -> Option<AccountId>;
199
200    /// Loads the metadata associated with a token.
201    fn load_token(&self, token_id: &TokenId) -> Option<Token>;
202}
203
204/// Authorization for a transfer.
205#[derive(PartialEq, Eq, Clone, Debug, Hash)]
206#[near(serializers = [borsh, json])]
207pub enum Nep171TransferAuthorization {
208    /// The sender is the owner of the token.
209    Owner,
210    /// The sender holds a valid approval ID for the token.
211    ApprovalId(u32),
212}
213
214/// Different ways of checking if a transfer is valid.
215pub trait CheckExternalTransfer<C> {
216    /// Checks if a transfer is valid. Returns the account ID of the current
217    /// owner of the token.
218    ///
219    /// # Errors
220    ///
221    /// Returns an error if the external transfer should not be performed.
222    fn check_external_transfer(
223        contract: &C,
224        transfer: &Nep171Transfer,
225    ) -> Result<AccountId, Nep171TransferError>;
226}
227
228/// Default external transfer checker. Only allows transfers by the owner of a
229/// token. Does not support approval IDs.
230pub struct DefaultCheckExternalTransfer;
231
232impl<T: Nep171Controller> CheckExternalTransfer<T> for DefaultCheckExternalTransfer {
233    fn check_external_transfer(
234        contract: &T,
235        transfer: &Nep171Transfer,
236    ) -> Result<AccountId, Nep171TransferError> {
237        let owner_id =
238            contract
239                .token_owner(&transfer.token_id)
240                .ok_or_else(|| TokenDoesNotExistError {
241                    token_id: transfer.token_id.clone(),
242                })?;
243
244        match transfer.authorization {
245            Nep171TransferAuthorization::Owner => {
246                if transfer.sender_id.as_ref() != owner_id {
247                    return Err(TokenNotOwnedByExpectedOwnerError {
248                        expected_owner_id: transfer.sender_id.clone().into(),
249                        owner_id,
250                        token_id: transfer.token_id.clone(),
251                    }
252                    .into());
253                }
254            }
255            Nep171TransferAuthorization::ApprovalId(approval_id) => {
256                return Err(SenderNotApprovedError {
257                    owner_id,
258                    sender_id: transfer.sender_id.clone().into(),
259                    token_id: transfer.token_id.clone(),
260                    approval_id,
261                }
262                .into())
263            }
264        }
265
266        if transfer.receiver_id.as_ref() == owner_id {
267            return Err(TokenReceiverIsCurrentOwnerError {
268                owner_id,
269                token_id: transfer.token_id.clone(),
270            }
271            .into());
272        }
273
274        Ok(owner_id)
275    }
276}
277
278impl<T: Nep171ControllerInternal> Nep171Controller for T {
279    type MintHook = <Self as Nep171ControllerInternal>::MintHook;
280    type TransferHook = <Self as Nep171ControllerInternal>::TransferHook;
281    type BurnHook = <Self as Nep171ControllerInternal>::BurnHook;
282
283    type CheckExternalTransfer = <Self as Nep171ControllerInternal>::CheckExternalTransfer;
284    type LoadTokenMetadata = <Self as Nep171ControllerInternal>::LoadTokenMetadata;
285
286    fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError> {
287        match Self::CheckExternalTransfer::check_external_transfer(self, transfer) {
288            Ok(current_owner_id) => {
289                Self::TransferHook::hook(self, transfer, |contract| {
290                    contract.transfer_unchecked(
291                        std::array::from_ref(&transfer.token_id),
292                        &transfer.receiver_id,
293                    );
294
295                    Nep171Event::NftTransfer(vec![NftTransferLog {
296                        authorized_id: None,
297                        old_owner_id: current_owner_id.into(),
298                        new_owner_id: transfer.receiver_id.clone(),
299                        token_ids: vec![transfer.token_id.clone().into()],
300                        memo: transfer.memo.clone(),
301                    }])
302                    .emit();
303                });
304
305                Ok(())
306            }
307            Err(e) => Err(e),
308        }
309    }
310
311    fn transfer_unchecked(&mut self, token_ids: &[TokenId], receiver_id: &AccountIdRef) {
312        for token_id in token_ids {
313            let mut slot = Self::slot_token_owner(token_id);
314            slot.write_deref(receiver_id);
315        }
316    }
317
318    fn mint_unchecked(&mut self, token_ids: &[TokenId], owner_id: &AccountIdRef) {
319        for token_id in token_ids {
320            let mut slot = Self::slot_token_owner(token_id);
321            slot.write_deref(owner_id);
322        }
323    }
324
325    fn mint(&mut self, action: &Nep171Mint<'_>) -> Result<(), Nep171MintError> {
326        if action.token_ids.is_empty() {
327            return Ok(());
328        }
329
330        for token_id in &action.token_ids {
331            let slot = Self::slot_token_owner(token_id);
332            if slot.exists() {
333                return Err(TokenAlreadyExistsError {
334                    token_id: token_id.to_string(),
335                }
336                .into());
337            }
338        }
339
340        Self::MintHook::hook(self, action, |contract| {
341            contract.mint_unchecked(&action.token_ids, &action.receiver_id);
342
343            Nep171Event::NftMint(vec![NftMintLog {
344                token_ids: action.token_ids.iter().map(Into::into).collect(),
345                owner_id: action.receiver_id.clone(),
346                memo: action.memo.clone(),
347            }])
348            .emit();
349
350            Ok(())
351        })
352    }
353
354    fn burn(&mut self, action: &Nep171Burn<'_>) -> Result<(), Nep171BurnError> {
355        if action.token_ids.is_empty() {
356            return Ok(());
357        }
358
359        for token_id in &action.token_ids {
360            if let Some(actual_owner_id) = self.token_owner(token_id) {
361                if actual_owner_id != action.owner_id.as_ref() {
362                    return Err(TokenNotOwnedByExpectedOwnerError {
363                        expected_owner_id: action.owner_id.clone().into(),
364                        owner_id: actual_owner_id,
365                        token_id: token_id.clone(),
366                    }
367                    .into());
368                }
369            } else {
370                return Err(TokenDoesNotExistError {
371                    token_id: token_id.clone(),
372                }
373                .into());
374            }
375        }
376
377        Self::BurnHook::hook(self, action, |contract| {
378            contract.burn_unchecked(&action.token_ids);
379
380            Nep171Event::NftBurn(vec![NftBurnLog {
381                token_ids: action.token_ids.iter().map(Into::into).collect(),
382                owner_id: action.owner_id.clone(),
383                authorized_id: None,
384                memo: action.memo.clone(),
385            }])
386            .emit();
387
388            Ok(())
389        })
390    }
391
392    fn burn_unchecked(&mut self, token_ids: &[TokenId]) -> bool {
393        let mut removed_successfully = true;
394
395        for token_id in token_ids {
396            removed_successfully &= Self::slot_token_owner(token_id).remove();
397        }
398
399        removed_successfully
400    }
401
402    fn token_owner(&self, token_id: &TokenId) -> Option<AccountId> {
403        Self::slot_token_owner(token_id).read()
404    }
405
406    fn load_token(&self, token_id: &TokenId) -> Option<Token> {
407        let mut metadata = std::collections::HashMap::new();
408        Self::LoadTokenMetadata::load(self, token_id, &mut metadata).ok()?;
409        Some(Token {
410            token_id: token_id.clone(),
411            owner_id: self.token_owner(token_id)?,
412            extensions_metadata: metadata,
413        })
414    }
415}
416
417/// Token information structure.
418#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, NearSchema)]
419#[serde(crate = "near_sdk::serde")]
420pub struct Token {
421    /// Token ID.
422    pub token_id: TokenId,
423    /// Current owner of the token.
424    pub owner_id: AccountId,
425    /// Metadata provided by extensions.
426    #[serde(flatten)]
427    pub extensions_metadata: std::collections::HashMap<String, near_sdk::serde_json::Value>,
428}
429
430/// Trait for NFT extensions to load token metadata.
431pub trait LoadTokenMetadata<C> {
432    /// Load token metadata into `metadata`.
433    ///
434    /// # Errors
435    ///
436    /// If the token metadata could not be loaded.
437    fn load(
438        contract: &C,
439        token_id: &TokenId,
440        metadata: &mut std::collections::HashMap<String, near_sdk::serde_json::Value>,
441    ) -> Result<(), Box<dyn Error>>;
442}
443
444impl<C> LoadTokenMetadata<C> for () {
445    fn load(
446        _contract: &C,
447        _token_id: &TokenId,
448        _metadata: &mut std::collections::HashMap<String, near_sdk::serde_json::Value>,
449    ) -> Result<(), Box<dyn Error>> {
450        Ok(())
451    }
452}
453
454impl<C, T: LoadTokenMetadata<C>, U: LoadTokenMetadata<C>> LoadTokenMetadata<C> for (T, U) {
455    fn load(
456        contract: &C,
457        token_id: &TokenId,
458        metadata: &mut std::collections::HashMap<String, near_sdk::serde_json::Value>,
459    ) -> Result<(), Box<dyn Error>> {
460        T::load(contract, token_id, metadata)?;
461        U::load(contract, token_id, metadata)?;
462        Ok(())
463    }
464}
465
466// further variations are technically unnecessary: just use (T, (U, V)) or ((T, U), V)