Skip to content

Commit

Permalink
NFT metadata events (draft)
Browse files Browse the repository at this point in the history
  • Loading branch information
tifrel committed Apr 18, 2022
1 parent 0017abf commit 836b714
Showing 1 changed file with 297 additions and 0 deletions.
297 changes: 297 additions & 0 deletions neps/nep-0352.md
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/).

0 comments on commit 836b714

Please sign in to comment.