-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
297 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,297 @@ | ||
--- | ||
NEP: 352 | ||
Title: NFT metadata events | ||
Author: Till Friesewinkel <[email protected]> | ||
DiscussionsTo: https://github.com/nearprotocol/neps/pull/352 | ||
Status: Draft | ||
Type: Standards Track | ||
Category: Contract | ||
Created: 21-Apr-2022 | ||
--- | ||
|
||
## Summary | ||
|
||
This NEP seeks to extend the NFT metadata standard by events, such that indexers | ||
are able to keep track of them. | ||
|
||
## Motivation | ||
|
||
This specifically supports the use-case of indexing metadata belonging to tokens | ||
and to NFT contracts themselves. | ||
|
||
## Rationale and alternatives | ||
|
||
- One alternative for token metadata would be to emit this data as `memo` | ||
parameter on token minting. This doesn't allow changes to metadata after | ||
minting, which may or may not be desirable. This bonus event keeps concerns | ||
separated, as are the core and metadata standards. It also plays better in | ||
conjunction with the 16 kB limit for logs. | ||
- Another alternative is not to emit any events for metadata. This means that | ||
either indexers cannot pick up on token metadata, or have to query contracts | ||
directly for it. This has the advantage that there are less moving and thus | ||
less potentially breaking parts in a contract. It has the disadvantage that | ||
these queries can bring significant overhead with them and bring with them a | ||
N+1 querying problem (either on the client side or via a cloud function). | ||
- The same reasoning applies to contract metadata, it would in many cases be | ||
preferable to index the metadata of a contract, keeping up-to-date via | ||
events | ||
|
||
## Specification | ||
|
||
The following interfaces have been standardized by | ||
[NEP-177](https://nomicon.io/Standards/Tokens/NonFungibleToken/Metadata): | ||
|
||
```ts | ||
interface NftContractMetadata { | ||
spec: string; | ||
name: string; | ||
symbol: string; | ||
icon: string | null; | ||
base_uri: string | null; | ||
reference: string | null; | ||
reference_hash: string | null; | ||
} | ||
|
||
interface NftTokenMetadata { | ||
title: string | null; | ||
description: string | null; | ||
media: string | null; | ||
media_hash: string | null; | ||
copies: number | null; | ||
issued_at: number | null; | ||
expires_at: number | null; | ||
starts_at: number | null; | ||
updated_at: number | null; | ||
extra: string | null; | ||
reference: string | null; | ||
reference_hash: string | null; | ||
} | ||
``` | ||
|
||
Based on those, two new smart contract events are defined: | ||
|
||
```ts | ||
interface NftUpdateContractMetadataEvent { | ||
standard: "nep177"; | ||
version: "1.0.0"; | ||
event: "nft_update_contract_metadata"; | ||
data: NftContractMetadata; | ||
} | ||
|
||
interface NftUpdateTokenMetadataEvent { | ||
standard: "nep177"; | ||
version: "1.0.0"; | ||
event: "nft_update_token_metadata"; | ||
data: NftTokenMetadata; | ||
} | ||
``` | ||
|
||
Whenever metadata is updated or created, these can be run to notify event | ||
listeners of either the incremental updates that happen or the current state of | ||
the metadata. | ||
|
||
## Reference Implementation (Required for Contract category, optional for other categories) | ||
|
||
```rs | ||
use near_sdk::{ | ||
borsh::{self, BorshDeserialize, BorshSerialize}, | ||
near_bindgen, | ||
serde::Serialize, | ||
serde_json, | ||
}; | ||
use std::collections::HashMap; | ||
|
||
type TokenId = String; | ||
|
||
#[derive(Serialize, BorshSerialize, BorshDeserialize, Clone)] | ||
#[serde(crate = "near_sdk::serde")] | ||
pub struct NftContractMetadata { | ||
spec: String, | ||
name: String, | ||
symbol: String, | ||
icon: Option<String>, | ||
base_uri: Option<String>, | ||
reference: Option<String>, | ||
reference_hash: Option<String>, | ||
} | ||
|
||
#[derive(Serialize, BorshSerialize, BorshDeserialize, Clone)] | ||
#[serde(crate = "near_sdk::serde")] | ||
pub struct NftTokenMetadata { | ||
title: Option<String>, | ||
description: Option<String>, | ||
media: Option<String>, | ||
media_hash: Option<String>, | ||
copies: Option<u64>, | ||
issued_at: Option<u64>, | ||
expires_at: Option<u64>, | ||
starts_at: Option<u64>, | ||
updated_at: Option<u64>, | ||
extra: Option<String>, | ||
reference: Option<String>, | ||
reference_hash: Option<String>, | ||
} | ||
|
||
#[derive(Serialize, BorshSerialize, BorshDeserialize)] | ||
#[serde(crate = "near_sdk::serde")] | ||
struct NftUpdateContractMetadataEvent { | ||
standard: String, | ||
version: String, | ||
event: String, | ||
data: NftContractMetadata, | ||
} | ||
|
||
impl NftUpdateContractMetadataEvent { | ||
pub fn new(metadata: NftContractMetadata) -> Self { | ||
Self { | ||
standard: "nep177".to_string(), | ||
version: "1.0.0".to_string(), | ||
event: "nft_update_contract_metadata".to_string(), | ||
data: metadata, | ||
} | ||
} | ||
} | ||
|
||
#[derive(Serialize, BorshSerialize, BorshDeserialize)] | ||
#[serde(crate = "near_sdk::serde")] | ||
struct NftUpdateTokenMetadataEvent { | ||
standard: String, | ||
version: String, | ||
event: String, | ||
data: NftTokenMetadata, | ||
} | ||
|
||
impl NftUpdateTokenMetadataEvent { | ||
pub fn new(metadata: NftTokenMetadata) -> Self { | ||
Self { | ||
standard: "nep177".to_string(), | ||
version: "1.0.0".to_string(), | ||
event: "nft_update_token_metadata".to_string(), | ||
data: metadata, | ||
} | ||
} | ||
} | ||
|
||
#[derive(BorshSerialize, BorshDeserialize)] | ||
pub struct Token { | ||
token_id: TokenId, | ||
metadata: NftTokenMetadata, | ||
} | ||
|
||
#[near_bindgen] | ||
#[derive(BorshSerialize, BorshDeserialize)] | ||
pub struct NftContract { | ||
tokens: HashMap<TokenId, Token>, | ||
metadata: NftContractMetadata, | ||
} | ||
|
||
#[near_bindgen] | ||
impl NftContract { | ||
pub fn set_token_metadata( | ||
&mut self, | ||
token_id: TokenId, | ||
metadata: NftTokenMetadata, | ||
) { | ||
if let Some(mut token) = self.tokens.get_mut(&token_id) { | ||
token.metadata = metadata.clone(); | ||
let event = NftUpdateTokenMetadataEvent::new(metadata); | ||
let event_json = serde_json::to_string(&event).unwrap(); | ||
near_sdk::env::log_str(&format!("EVENT_JSON:{}", event_json)); | ||
} else { | ||
near_sdk::env::panic_str(&format!( | ||
"Cannot find token with ID: {}", | ||
token_id | ||
)) | ||
} | ||
} | ||
|
||
pub fn set_contract_metadata(&mut self, metadata: NftContractMetadata) { | ||
self.metadata = metadata.clone(); | ||
let event = NftUpdateContractMetadataEvent::new(metadata); | ||
let event_json = serde_json::to_string(&event).unwrap(); | ||
near_sdk::env::log_str(&format!("EVENT_JSON:{}", event_json)); | ||
} | ||
} | ||
``` | ||
|
||
### Notes | ||
|
||
- The functions are not required and act simply showcase how a full update of | ||
the metadata would look like. | ||
- Contracts may decide to allow or disallow updating certain fields, e.g. tying | ||
the name of the contract to the NEAR address it's deployed to. | ||
- Contracts may decide to do partial updates, e.g. `set_contract_base_uri` (see | ||
below), in which case the event string can be significantly condensed. | ||
- If an event listener tracked all these events starting from the deployment of | ||
a contract, it should be able to reproduce the output of the `nft_metadata` | ||
method as defined in NEP-177. | ||
|
||
#### An example for `set_contract_base_uri` | ||
|
||
```rs | ||
#[derive(Serialize, BorshSerialize, BorshDeserialize)] | ||
#[serde(crate = "near_sdk::serde")] | ||
pub struct NftSetBaseUriEventData { | ||
base_uri: String, | ||
} | ||
|
||
#[derive(Serialize, BorshSerialize, BorshDeserialize)] | ||
#[serde(crate = "near_sdk::serde")] | ||
struct NftSetBaseUriEvent { | ||
standard: String, | ||
version: String, | ||
event: String, | ||
data: NftSetBaseUriEventData, | ||
} | ||
|
||
impl NftSetBaseUriEvent { | ||
pub fn new(data: NftSetBaseUriEventData) -> Self { | ||
Self { | ||
standard: "nep177".to_string(), | ||
version: "1.0.0".to_string(), | ||
event: "nft_update_contract_metadata".to_string(), | ||
data, | ||
} | ||
} | ||
} | ||
|
||
#[near_bindgen] | ||
impl NftContract { | ||
pub fn set_contract_base_uri(&mut self, base_uri: String) { | ||
self.metadata.base_uri = Some(base_uri.clone()); | ||
let event = | ||
NftSetBaseUriEvent::new(NftSetBaseUriEventData { base_uri }); | ||
let event_json = serde_json::to_string(&event).unwrap(); | ||
near_sdk::env::log_str(&format!("EVENT_JSON:{}", event_json)); | ||
} | ||
} | ||
``` | ||
|
||
## Security Implications | ||
|
||
None, as it only places information that is already publicly available in a | ||
location that is more convenient for indexers. | ||
|
||
## Drawbacks (Optional) | ||
|
||
- More code usually translates to higher storage costs. | ||
- Whenever the NFT metadata standard changes, the event format will need to | ||
change accordingly. The | ||
[event standard from NEP-297](https://nomicon.io/Standards/EventsFormat) | ||
includes versioning and thus allows listeners to gracefully handle such | ||
upgrades. | ||
|
||
## Unresolved Issues (Optional) | ||
|
||
None. | ||
|
||
## Future possibilities | ||
|
||
Keep this up-to-date with (potentially) evolving NFT metadata standard. | ||
|
||
## Copyright | ||
|
||
[copyright]: #copyright | ||
|
||
Copyright and related rights waived via | ||
[CC0](https://creativecommons.org/publicdomain/zero/1.0/). |