near_sdk_contract_tools/standard/nep171/
mod.rs1#![doc = include_str!("../../../tests/macros/standard/nep171/no_hooks.rs")]
15#![doc = include_str!("../../../tests/macros/standard/nep171/hooks.rs")]
21#![doc = include_str!("../../../tests/macros/standard/nep171/non_fungible_token.rs")]
29#![doc = include_str!("../../../tests/macros/standard/nep171/manual_integration.rs")]
38use 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::*;
58mod ext;
60pub use ext::*;
61pub mod hooks;
62
63pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas::from_gas(5_000_000_000_000);
65pub const GAS_FOR_NFT_TRANSFER_CALL: Gas =
67 Gas::from_gas(25_000_000_000_000 + GAS_FOR_RESOLVE_TRANSFER.as_gas());
68pub const INSUFFICIENT_GAS_MESSAGE: &str = "More gas is required";
70
71pub type TokenId = String;
73
74#[derive(BorshSerialize, BorshStorageKey)]
75#[borsh(crate = "near_sdk::borsh")]
76enum StorageKey<'a> {
77 TokenOwner(&'a str),
78}
79
80pub trait Nep171ControllerInternal {
82 type MintHook: for<'a> Hook<Self, Nep171Mint<'a>>
84 where
85 Self: Sized;
86 type TransferHook: for<'a> Hook<Self, Nep171Transfer<'a>>
88 where
89 Self: Sized;
90 type BurnHook: for<'a> Hook<Self, Nep171Burn<'a>>
92 where
93 Self: Sized;
94
95 type CheckExternalTransfer: CheckExternalTransfer<Self>
97 where
98 Self: Sized;
99
100 type LoadTokenMetadata: LoadTokenMetadata<Self>
102 where
103 Self: Sized;
104
105 #[must_use]
107 fn root() -> Slot<()> {
108 Slot::root(DefaultStorageKey::Nep171)
109 }
110
111 #[must_use]
113 fn slot_token_owner(token_id: &TokenId) -> Slot<AccountId> {
114 Self::root().field(StorageKey::TokenOwner(token_id))
115 }
116}
117
118pub trait Nep171Controller {
120 type MintHook: for<'a> Hook<Self, Nep171Mint<'a>>
122 where
123 Self: Sized;
124 type TransferHook: for<'a> Hook<Self, Nep171Transfer<'a>>
126 where
127 Self: Sized;
128 type BurnHook: for<'a> Hook<Self, Nep171Burn<'a>>
130 where
131 Self: Sized;
132
133 type CheckExternalTransfer: CheckExternalTransfer<Self>
135 where
136 Self: Sized;
137
138 type LoadTokenMetadata: LoadTokenMetadata<Self>
140 where
141 Self: Sized;
142
143 fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError>
155 where
156 Self: Sized;
157
158 fn transfer_unchecked(&mut self, token_ids: &[TokenId], receiver_id: &AccountIdRef);
171
172 fn mint(&mut self, action: &Nep171Mint<'_>) -> Result<(), Nep171MintError>;
179
180 fn mint_unchecked(&mut self, token_ids: &[TokenId], owner_id: &AccountIdRef);
183
184 fn burn(&mut self, action: &Nep171Burn<'_>) -> Result<(), Nep171BurnError>;
192
193 fn burn_unchecked(&mut self, token_ids: &[TokenId]) -> bool;
196
197 fn token_owner(&self, token_id: &TokenId) -> Option<AccountId>;
199
200 fn load_token(&self, token_id: &TokenId) -> Option<Token>;
202}
203
204#[derive(PartialEq, Eq, Clone, Debug, Hash)]
206#[near(serializers = [borsh, json])]
207pub enum Nep171TransferAuthorization {
208 Owner,
210 ApprovalId(u32),
212}
213
214pub trait CheckExternalTransfer<C> {
216 fn check_external_transfer(
223 contract: &C,
224 transfer: &Nep171Transfer,
225 ) -> Result<AccountId, Nep171TransferError>;
226}
227
228pub 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, NearSchema)]
419#[serde(crate = "near_sdk::serde")]
420pub struct Token {
421 pub token_id: TokenId,
423 pub owner_id: AccountId,
425 #[serde(flatten)]
427 pub extensions_metadata: std::collections::HashMap<String, near_sdk::serde_json::Value>,
428}
429
430pub trait LoadTokenMetadata<C> {
432 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