diff --git a/README.md b/README.md index 3b3e28f..d7ca4c7 100644 --- a/README.md +++ b/README.md @@ -99,19 +99,28 @@ e.emit(); To create a contract that is compatible with the NEP-141 and NEP-148 standards, that emits standard-compliant (NEP-141, NEP-297) events. ```rust -use near_sdk_contract_tools::FungibleToken; +use near_sdk_contract_tools::ft::*; use near_sdk::near_bindgen; #[derive(FungibleToken)] -#[fungible_token( - name = "My Fungible Token", - symbol = "MYFT", - decimals = 18, - no_hooks -)] +#[fungible_token(no_hooks)] #[near_bindgen] -struct FungibleToken { - // ... +struct FungibleToken {} + +#[near_bindgen] +impl FungibleToken { + #[init] + pub fn new() -> Self { + let mut contract = Self {}; + + contract.set_metadata(&FungibleTokenMetadata::new( + "My Fungible Token".to_string(), + "MYFT".to_string(), + 24, + )); + + contract + } } ``` @@ -140,20 +149,15 @@ pub struct MyNft {} One may wish to combine the features of multiple macros in one contract. All of the macros are written such that they will work in a standalone manner, so this should largely work without issue. However, sometimes it may be desirable for the macros to work in _combination_ with each other. For example, to make a fungible token pausable, use the fungible token hooks to require that a contract be unpaused before making a token transfer: ```rust -use near_sdk_contract_tools::{ - pause::Pause, - standard::nep141::{Nep141Hook, Nep141Transfer}, - FungibleToken, Pause, -}; +use near_sdk_contract_tools::{ft::*, pause::Pause, Pause}; use near_sdk::near_bindgen; #[derive(FungibleToken, Pause)] -#[fungible_token(name = "Pausable Fungible Token", symbol = "PFT", decimals = 18)] #[near_bindgen] struct Contract {} -impl Nep141Hook for Contract { - fn before_transfer(&mut self, _transfer: &Nep141Transfer) { +impl SimpleNep141Hook for Contract { + fn before_transfer(&self, _transfer: &Nep141Transfer) { Contract::require_unpaused(); } } diff --git a/macros/src/standard/fungible_token.rs b/macros/src/standard/fungible_token.rs index 866a0af..b3e695b 100644 --- a/macros/src/standard/fungible_token.rs +++ b/macros/src/standard/fungible_token.rs @@ -9,17 +9,11 @@ use super::{nep141, nep148}; #[darling(attributes(fungible_token), supports(struct_named))] pub struct FungibleTokenMeta { // NEP-141 fields - pub storage_key: Option, + pub core_storage_key: Option, pub no_hooks: Flag, // NEP-148 fields - pub spec: Option, - pub name: String, - pub symbol: String, - pub icon: Option, - pub reference: Option, - pub reference_hash: Option, - pub decimals: u8, + pub metadata_storage_key: Option, // darling pub generics: syn::Generics, @@ -34,17 +28,10 @@ pub struct FungibleTokenMeta { pub fn expand(meta: FungibleTokenMeta) -> Result { let FungibleTokenMeta { - storage_key, + core_storage_key, + metadata_storage_key, no_hooks, - spec, - name, - symbol, - icon, - reference, - reference_hash, - decimals, - generics, ident, @@ -53,7 +40,7 @@ pub fn expand(meta: FungibleTokenMeta) -> Result { } = meta; let expand_nep141 = nep141::expand(nep141::Nep141Meta { - storage_key, + storage_key: core_storage_key, no_hooks, generics: generics.clone(), @@ -64,14 +51,7 @@ pub fn expand(meta: FungibleTokenMeta) -> Result { }); let expand_nep148 = nep148::expand(nep148::Nep148Meta { - spec, - name, - symbol, - icon, - reference, - reference_hash, - decimals, - + storage_key: metadata_storage_key, generics, ident, diff --git a/macros/src/standard/nep148.rs b/macros/src/standard/nep148.rs index 5580317..227f3bd 100644 --- a/macros/src/standard/nep148.rs +++ b/macros/src/standard/nep148.rs @@ -1,18 +1,12 @@ -use darling::{FromDeriveInput, ToTokens}; +use darling::FromDeriveInput; use proc_macro2::TokenStream; use quote::quote; +use syn::Expr; #[derive(Debug, FromDeriveInput)] #[darling(attributes(nep148), supports(struct_named))] pub struct Nep148Meta { - pub spec: Option, - pub name: String, - pub symbol: String, - pub icon: Option, - pub reference: Option, - pub reference_hash: Option, - pub decimals: u8, - + pub storage_key: Option, pub generics: syn::Generics, pub ident: syn::Ident, @@ -23,64 +17,35 @@ pub struct Nep148Meta { pub near_sdk: syn::Path, } -fn optionize(t: Option) -> TokenStream -where - T: ToTokens, -{ - t.map_or_else(|| quote! { None }, |v| quote! { Some(#v) }) -} - pub fn expand(meta: Nep148Meta) -> Result { let Nep148Meta { + storage_key, generics, ident, - // fields - spec, - name, - symbol, - icon, - reference, - reference_hash, - decimals, me, near_sdk, } = meta; - let spec = spec.map(|s| s.to_token_stream()).unwrap_or_else(|| { + let root = storage_key.map(|storage_key| { quote! { - #me::standard::nep148::FT_METADATA_SPEC + fn root() -> #me::slot::Slot<()> { + #me::slot::Slot::root(#storage_key) + } } }); - let icon = optionize(icon); - let reference = optionize(reference); - - // TODO: Download reference field at compile time and calculate reference_hash automatically - let reference_hash = optionize(reference_hash.map(|s| { - let v = format!("{:?}", base64::decode(s).unwrap()) - .parse::() - .unwrap(); - - quote! { #near_sdk::json_types::Base64VecU8::from(#v.to_vec()) } - })); - let (imp, ty, wher) = generics.split_for_impl(); Ok(quote! { - use #me::standard::nep148::Nep148; + impl #imp #me::standard::nep148::Nep148ControllerInternal for #ident #ty #wher { + #root + } + #[#near_sdk::near_bindgen] impl #imp #me::standard::nep148::Nep148 for #ident #ty #wher { fn ft_metadata(&self) -> #me::standard::nep148::FungibleTokenMetadata { - #me::standard::nep148::FungibleTokenMetadata { - spec: #spec.into(), - name: #name.into(), - symbol: #symbol.into(), - icon: #icon.map(|s: &str| s.into()), - reference: #reference.map(|s: &str| s.into()), - reference_hash: #reference_hash, - decimals: #decimals, - } + #me::standard::nep148::Nep148Controller::get_metadata(self) } } }) diff --git a/src/lib.rs b/src/lib.rs index f1f34d7..74f0f41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,8 @@ pub enum DefaultStorageKey { ApprovalManager, /// Default storage key for [`standard::nep141::Nep141ControllerInternal::root`]. Nep141, + /// Default storage key for [`standard::nep148::Nep148ControllerInternal::root`]. + Nep148, /// Default storage key for [`standard::nep171::Nep171ControllerInternal::root`]. Nep171, /// Default storage key for [`standard::nep177::Nep177ControllerInternal::root`]. @@ -34,6 +36,7 @@ impl IntoStorageKey for DefaultStorageKey { match self { DefaultStorageKey::ApprovalManager => b"~am".to_vec(), DefaultStorageKey::Nep141 => b"~$141".to_vec(), + DefaultStorageKey::Nep148 => b"~$148".to_vec(), DefaultStorageKey::Nep171 => b"~$171".to_vec(), DefaultStorageKey::Nep177 => b"~$177".to_vec(), DefaultStorageKey::Nep178 => b"~$178".to_vec(), @@ -66,3 +69,11 @@ pub mod nft { Nep171, Nep177, Nep178, Nep181, NonFungibleToken, }; } + +/// Re-exports of the FT standard traits. +pub mod ft { + pub use crate::{ + standard::{nep141::*, nep148::*}, + FungibleToken, Nep141, Nep148, + }; +} diff --git a/src/standard/nep141/mod.rs b/src/standard/nep141/mod.rs index f3d53b4..15797d0 100644 --- a/src/standard/nep141/mod.rs +++ b/src/standard/nep141/mod.rs @@ -9,13 +9,13 @@ use serde::{Deserialize, Serialize}; use crate::{slot::Slot, standard::nep297::*, DefaultStorageKey}; -pub mod error; +mod error; pub use error::*; -pub mod event; +mod event; pub use event::*; -pub mod ext; +mod ext; pub use ext::*; -pub mod hook; +mod hook; pub use hook::*; /// Gas value required for ft_resolve_transfer calls diff --git a/src/standard/nep148.rs b/src/standard/nep148.rs index 5f66cea..ebcebc8 100644 --- a/src/standard/nep148.rs +++ b/src/standard/nep148.rs @@ -1,16 +1,22 @@ //! NEP-148 fungible token metadata implementation //! -#![allow(missing_docs)] // ext_contract doesn't play nice with #![warn(missing_docs)] use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, - ext_contract, + env, json_types::Base64VecU8, + BorshStorageKey, }; use serde::{Deserialize, Serialize}; -/// Version of the NEP-148 metadata spec +use crate::{slot::Slot, DefaultStorageKey}; + +pub use ext::*; + +/// Version of the NEP-148 metadata spec. pub const FT_METADATA_SPEC: &str = "ft-1.0.0"; +/// Error message for unset metadata. +pub const ERR_METADATA_UNSET: &str = "NEP-148 metadata is not set"; /// NEP-148-compatible metadata struct #[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Eq, PartialEq, Clone, Debug)] @@ -30,49 +36,121 @@ pub struct FungibleTokenMetadata { /// For tamper protection. pub reference_hash: Option, /// Cosmetic. Number of base-10 decimal places to shift the floating point. - /// 18 is a common value. + /// 24 is a common value. pub decimals: u8, } -/// Contract that supports the NEP-148 metadata standard -#[ext_contract(ext_nep148)] -pub trait Nep148 { +impl FungibleTokenMetadata { + /// Creates a new metadata struct. + pub fn new(name: String, symbol: String, decimals: u8) -> Self { + Self { + spec: FT_METADATA_SPEC.into(), + name, + symbol, + icon: None, + reference: None, + reference_hash: None, + decimals, + } + } + + /// Sets the spec field. + pub fn spec(mut self, spec: String) -> Self { + self.spec = spec; + self + } + + /// Sets the name field. + pub fn name(mut self, name: String) -> Self { + self.name = name; + self + } + + /// Sets the symbol field. + pub fn symbol(mut self, symbol: String) -> Self { + self.symbol = symbol; + self + } + + /// Sets the icon field. + pub fn icon(mut self, icon: String) -> Self { + self.icon = Some(icon); + self + } + + /// Sets the reference field. + pub fn reference(mut self, reference: String) -> Self { + self.reference = Some(reference); + self + } + + /// Sets the reference_hash field. + pub fn reference_hash(mut self, reference_hash: Base64VecU8) -> Self { + self.reference_hash = Some(reference_hash); + self + } + + /// Sets the decimals field. + pub fn decimals(mut self, decimals: u8) -> Self { + self.decimals = decimals; + self + } +} + +#[derive(BorshSerialize, BorshStorageKey)] +enum StorageKey { + Metadata, +} + +/// Internal functions for [`Nep148Controller`]. +pub trait Nep148ControllerInternal { + /// Returns the root storage slot for NEP-148. + fn root() -> Slot<()> { + Slot::new(DefaultStorageKey::Nep148) + } + + /// Returns the storage slot for NEP-148 metadata. + fn metadata() -> Slot { + Self::root().field(StorageKey::Metadata) + } +} + +/// Management functions for NEP-148. +pub trait Nep148Controller { /// Returns the metadata struct for this contract. - fn ft_metadata(&self) -> FungibleTokenMetadata; + /// + /// # Panics + /// + /// Panics if the metadata has not been set. + fn get_metadata(&self) -> FungibleTokenMetadata; + + /// Sets the metadata struct for this contract. + fn set_metadata(&mut self, metadata: &FungibleTokenMetadata); } -#[cfg(test)] -mod tests { - use crate::standard::nep148::FungibleTokenMetadata; - use near_sdk::borsh::BorshSerialize; - - #[test] - fn borsh_serialization_ignores_cow() { - let m1 = FungibleTokenMetadata { - spec: "spec".into(), - name: "name".into(), - symbol: "symbol".into(), - icon: Some("icon".into()), - reference: Some("reference".into()), - reference_hash: Some(b"reference_hash".to_vec().into()), - decimals: 18, - }; - - let m2 = FungibleTokenMetadata { - spec: "spec".to_owned(), - name: "name".to_owned(), - symbol: "symbol".to_owned(), - icon: Some("icon".to_owned()), - reference: Some("reference".to_owned()), - reference_hash: Some(b"reference_hash".to_vec().into()), - decimals: 18, - }; - - assert_eq!(m1, m2); - - let m1_serialized = m1.try_to_vec().unwrap(); - let m2_serialized = m2.try_to_vec().unwrap(); - - assert_eq!(m1_serialized, m2_serialized); +impl Nep148Controller for T { + fn get_metadata(&self) -> FungibleTokenMetadata { + Self::metadata() + .read() + .unwrap_or_else(|| env::panic_str(ERR_METADATA_UNSET)) + } + + fn set_metadata(&mut self, metadata: &FungibleTokenMetadata) { + Self::metadata().set(Some(metadata)); + } +} + +mod ext { + #![allow(missing_docs)] // ext_contract doesn't play well + + use near_sdk::ext_contract; + + use super::FungibleTokenMetadata; + + /// Contract that supports the NEP-148 metadata standard + #[ext_contract(ext_nep148)] + pub trait Nep148 { + /// Returns the metadata struct for this contract. + fn ft_metadata(&self) -> FungibleTokenMetadata; } } diff --git a/tests/macros/mod.rs b/tests/macros/mod.rs index 218e717..a55cde8 100644 --- a/tests/macros/mod.rs +++ b/tests/macros/mod.rs @@ -4,8 +4,8 @@ use near_sdk::{ test_utils::VMContextBuilder, testing_env, AccountId, BorshStorageKey, }; -use near_sdk_contract_tools::escrow::Escrow; use near_sdk_contract_tools::{ + escrow::Escrow, migrate::{MigrateExternal, MigrateHook}, owner::Owner, pause::Pause, @@ -396,17 +396,32 @@ mod pausable_fungible_token { }; use near_sdk_contract_tools::{ pause::Pause, - standard::nep141::{Nep141, Nep141Controller, Nep141Hook, Nep141Transfer}, + standard::{nep141::*, nep148::*}, FungibleToken, Pause, }; #[derive(FungibleToken, Pause, BorshDeserialize, BorshSerialize)] - #[fungible_token(name = "Pausable Fungible Token", symbol = "PFT", decimals = 18)] #[near_bindgen] struct Contract { pub storage_usage: u64, } + #[near_bindgen] + impl Contract { + #[init] + pub fn new() -> Self { + let mut contract = Self { storage_usage: 0 }; + + contract.set_metadata(&FungibleTokenMetadata::new( + "Pausable Fungible Token".into(), + "PFT".into(), + 18, + )); + + contract + } + } + #[derive(Default)] struct HookState { pub storage_usage_start: u64, @@ -445,7 +460,7 @@ mod pausable_fungible_token { let alice: AccountId = "alice".parse().unwrap(); let bob: AccountId = "bob_account".parse().unwrap(); - let mut c = Contract { storage_usage: 0 }; + let mut c = Contract::new(); c.deposit_unchecked(&alice, 100).unwrap(); @@ -467,7 +482,7 @@ mod pausable_fungible_token { let alice: AccountId = "alice".parse().unwrap(); let bob: AccountId = "bob_account".parse().unwrap(); - let mut c = Contract { storage_usage: 0 }; + let mut c = Contract::new(); c.deposit_unchecked(&alice, 100).unwrap(); diff --git a/tests/macros/standard/fungible_token.rs b/tests/macros/standard/fungible_token.rs index 10e2b87..5bc6769 100644 --- a/tests/macros/standard/fungible_token.rs +++ b/tests/macros/standard/fungible_token.rs @@ -2,26 +2,35 @@ use near_sdk::{ json_types::Base64VecU8, near_bindgen, test_utils::VMContextBuilder, testing_env, AccountId, }; use near_sdk_contract_tools::{ - standard::nep141::{Nep141, Nep141Controller}, + standard::{nep141::*, nep148::*}, FungibleToken, }; #[derive(FungibleToken)] -#[fungible_token( - name = "My Fungible Token", - symbol = "MYFT", - decimals = 18, - icon = "https://example.com/icon.png", - reference = "https://example.com/metadata.json", - reference_hash = "YXNkZg==", - no_hooks -)] +#[fungible_token(no_hooks)] #[near_bindgen] struct MyFungibleTokenContract {} +#[near_bindgen] +impl MyFungibleTokenContract { + #[init] + pub fn new() -> Self { + let mut contract = Self {}; + + contract.set_metadata( + &FungibleTokenMetadata::new("My Fungible Token".into(), "MYFT".into(), 18) + .icon("https://example.com/icon.png".into()) + .reference("https://example.com/metadata.json".into()) + .reference_hash(Base64VecU8::from([97, 115, 100, 102].to_vec())), + ); + + contract + } +} + #[test] fn fungible_token_transfer() { - let mut ft = MyFungibleTokenContract {}; + let mut ft = MyFungibleTokenContract::new(); let alice: AccountId = "alice".parse().unwrap(); let bob: AccountId = "bob".parse().unwrap(); @@ -53,7 +62,7 @@ fn fungible_token_transfer() { #[test] fn metadata() { - let ft = MyFungibleTokenContract {}; + let ft = MyFungibleTokenContract::new(); let meta = ft.ft_metadata(); assert_eq!(meta.decimals, 18); diff --git a/tests/macros/standard/nep148.rs b/tests/macros/standard/nep148.rs index f5b79c0..b1adc1b 100644 --- a/tests/macros/standard/nep148.rs +++ b/tests/macros/standard/nep148.rs @@ -1,21 +1,30 @@ use near_sdk::{json_types::Base64VecU8, near_bindgen}; -use near_sdk_contract_tools::Nep148; +use near_sdk_contract_tools::{standard::nep148::*, Nep148}; #[derive(Nep148)] -#[nep148( - name = "Test Fungible Token", - symbol = "TFT", - decimals = 18, - icon = "https://example.com/icon.png", - reference = "https://example.com/metadata.json", - reference_hash = "YXNkZg==" -)] #[near_bindgen] struct DerivesFTMetadata {} +#[near_bindgen] +impl DerivesFTMetadata { + #[init] + pub fn new() -> Self { + let mut contract = Self {}; + + contract.set_metadata( + &FungibleTokenMetadata::new("Test Fungible Token".into(), "TFT".into(), 18) + .icon("https://example.com/icon.png".into()) + .reference("https://example.com/metadata.json".into()) + .reference_hash(Base64VecU8::from([97, 115, 100, 102].to_vec())), + ); + + contract + } +} + #[test] fn test() { - let ft = DerivesFTMetadata {}; + let ft = DerivesFTMetadata::new(); let meta = ft.ft_metadata(); println!("{:?}", &meta); assert_eq!(meta.decimals, 18); diff --git a/workspaces-tests/src/bin/fungible_token.rs b/workspaces-tests/src/bin/fungible_token.rs index c5dcab3..6306e2e 100644 --- a/workspaces-tests/src/bin/fungible_token.rs +++ b/workspaces-tests/src/bin/fungible_token.rs @@ -9,10 +9,13 @@ use near_sdk::{ json_types::U128, near_bindgen, PanicOnDefault, }; -use near_sdk_contract_tools::{standard::nep141::*, FungibleToken}; +use near_sdk_contract_tools::{ + standard::{nep141::*, nep148::*}, + FungibleToken, +}; #[derive(PanicOnDefault, BorshSerialize, BorshDeserialize, FungibleToken)] -#[fungible_token(name = "My Fungible Token", symbol = "MYFT", decimals = 24, no_hooks)] +#[fungible_token(no_hooks)] #[near_bindgen] pub struct Contract {} @@ -20,7 +23,15 @@ pub struct Contract {} impl Contract { #[init] pub fn new() -> Self { - Self {} + let mut contract = Self {}; + + contract.set_metadata(&FungibleTokenMetadata::new( + "My Fungible Token".into(), + "MYFT".into(), + 24, + )); + + contract } pub fn mint(&mut self, amount: U128) {