Skip to content

Commit

Permalink
BREAKING: Fungible Token Improvements (better hooks + dynamic metadat…
Browse files Browse the repository at this point in the history
…a) (#127)

* feat: better hooks for nep141

* chore: move resolve code to macro

* chore: use Result instead of panicking in nep141 functions

* chore: update nep148 to use dynamic metadata

* feat: extension hooks

* chore: upgrade to [email protected]

* chore: use pretty_assertions and fix gas issue

* chore: clean tests

* fix: update ghactions rust version to 1.72

---------

Co-authored-by: Jacob Lindahl <[email protected]>
  • Loading branch information
encody and Jacob Lindahl authored Oct 12, 2023
1 parent f7f394f commit 03d98d8
Show file tree
Hide file tree
Showing 33 changed files with 1,292 additions and 872 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.69
toolchain: 1.72
components: rustfmt
- name: Check formatting
run: >
Expand All @@ -30,7 +30,7 @@ jobs:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.69
toolchain: 1.72
components: clippy
- name: Run linter
run: cargo clippy -- -D warnings
Expand All @@ -41,7 +41,7 @@ jobs:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.69
toolchain: 1.72
- name: Run unit and integration tests
run: cargo test --workspace --exclude workspaces-tests
workspaces-test:
Expand All @@ -51,7 +51,7 @@ jobs:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.69
toolchain: 1.72
targets: wasm32-unknown-unknown
- name: Run workspaces tests
run: >
Expand Down
38 changes: 21 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
```

Expand Down Expand Up @@ -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();
}
}
Expand Down
33 changes: 7 additions & 26 deletions macros/src/standard/fungible_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Expr>,
pub core_storage_key: Option<Expr>,
pub no_hooks: Flag,

// NEP-148 fields
pub spec: Option<String>,
pub name: String,
pub symbol: String,
pub icon: Option<String>,
pub reference: Option<String>,
pub reference_hash: Option<String>,
pub decimals: u8,
pub metadata_storage_key: Option<Expr>,

// darling
pub generics: syn::Generics,
Expand All @@ -34,17 +28,10 @@ pub struct FungibleTokenMeta {

pub fn expand(meta: FungibleTokenMeta) -> Result<TokenStream, darling::Error> {
let FungibleTokenMeta {
storage_key,
core_storage_key,
metadata_storage_key,
no_hooks,

spec,
name,
symbol,
icon,
reference,
reference_hash,
decimals,

generics,
ident,

Expand All @@ -53,8 +40,9 @@ pub fn expand(meta: FungibleTokenMeta) -> Result<TokenStream, darling::Error> {
} = meta;

let expand_nep141 = nep141::expand(nep141::Nep141Meta {
storage_key,
storage_key: core_storage_key,
no_hooks,
extension_hooks: None,

generics: generics.clone(),
ident: ident.clone(),
Expand All @@ -64,14 +52,7 @@ pub fn expand(meta: FungibleTokenMeta) -> Result<TokenStream, darling::Error> {
});

let expand_nep148 = nep148::expand(nep148::Nep148Meta {
spec,
name,
symbol,
icon,
reference,
reference_hash,
decimals,

storage_key: metadata_storage_key,
generics,
ident,

Expand Down
161 changes: 102 additions & 59 deletions macros/src/standard/nep141.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::ops::Not;

use darling::{util::Flag, FromDeriveInput};
use proc_macro2::TokenStream;
use quote::quote;
Expand All @@ -10,6 +8,7 @@ use syn::Expr;
pub struct Nep141Meta {
pub storage_key: Option<Expr>,
pub no_hooks: Flag,
pub extension_hooks: Option<syn::Type>,
pub generics: syn::Generics,
pub ident: syn::Ident,

Expand All @@ -24,6 +23,7 @@ pub fn expand(meta: Nep141Meta) -> Result<TokenStream, darling::Error> {
let Nep141Meta {
storage_key,
no_hooks,
extension_hooks,
generics,
ident,

Expand All @@ -41,20 +41,21 @@ pub fn expand(meta: Nep141Meta) -> Result<TokenStream, darling::Error> {
}
});

let before_transfer = no_hooks.is_present().not().then(|| {
quote! {
let hook_state = <Self as #me::standard::nep141::Nep141Hook::<_>>::before_transfer(self, &transfer);
}
});
let self_hook = if no_hooks.is_present() {
quote! { () }
} else {
quote! { Self }
};

let after_transfer = no_hooks.is_present().not().then(|| {
quote! {
<Self as #me::standard::nep141::Nep141Hook::<_>>::after_transfer(self, &transfer, hook_state);
}
});
let hook = if let Some(extension_hooks) = extension_hooks {
quote! { (#self_hook, #extension_hooks) }
} else {
self_hook
};

Ok(quote! {
impl #imp #me::standard::nep141::Nep141ControllerInternal for #ident #ty #wher {
type Hook = #hook;
#root
}

Expand All @@ -67,36 +68,23 @@ pub fn expand(meta: Nep141Meta) -> Result<TokenStream, darling::Error> {
amount: #near_sdk::json_types::U128,
memo: Option<String>,
) {
use #me::{
standard::{
nep141::{Nep141Controller, event},
nep297::Event,
},
};
use #me::standard::nep141::*;

#near_sdk::assert_one_yocto();
let sender_id = #near_sdk::env::predecessor_account_id();
let amount: u128 = amount.into();

let transfer = #me::standard::nep141::Nep141Transfer {
let transfer = Nep141Transfer {
sender_id: sender_id.clone(),
receiver_id: receiver_id.clone(),
amount,
memo: memo.clone(),
memo,
msg: None,
revert: false,
};

#before_transfer

Nep141Controller::transfer(
self,
sender_id.clone(),
receiver_id.clone(),
amount,
memo,
);

#after_transfer
Nep141Controller::transfer(self, &transfer)
.unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string()));
}

#[payable]
Expand All @@ -107,41 +95,57 @@ pub fn expand(meta: Nep141Meta) -> Result<TokenStream, darling::Error> {
memo: Option<String>,
msg: String,
) -> #near_sdk::Promise {
use #me::standard::nep141::*;

let prepaid_gas = #near_sdk::env::prepaid_gas();

#near_sdk::require!(
prepaid_gas >= GAS_FOR_FT_TRANSFER_CALL,
MORE_GAS_FAIL_MESSAGE,
);

#near_sdk::assert_one_yocto();
let sender_id = #near_sdk::env::predecessor_account_id();
let amount: u128 = amount.into();

let transfer = #me::standard::nep141::Nep141Transfer {
sender_id: sender_id.clone(),
receiver_id: receiver_id.clone(),
amount,
memo: memo.clone(),
msg: None,
};

#before_transfer

let r = #me::standard::nep141::Nep141Controller::transfer_call(
self,
sender_id.clone(),
receiver_id.clone(),
let transfer = Nep141Transfer {
sender_id,
receiver_id,
amount,
memo,
msg.clone(),
#near_sdk::env::prepaid_gas(),
);

#after_transfer
msg: Some(msg.clone()),
revert: false,
};

r
Nep141Controller::transfer(self, &transfer)
.unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string()));

let receiver_gas = prepaid_gas
.0
.checked_sub(GAS_FOR_FT_TRANSFER_CALL.0) // TODO: Double-check this math. Should this be GAS_FOR_RESOLVE_TRANSFER? If not, this checked_sub call is superfluous given the require!() at the top of this function.
.unwrap_or_else(|| #near_sdk::env::panic_str("Prepaid gas overflow"));

// Initiating receiver's call and the callback
ext_nep141_receiver::ext(transfer.receiver_id.clone())
.with_static_gas(receiver_gas.into())
.ft_on_transfer(transfer.sender_id.clone(), transfer.amount.into(), msg)
.then(
ext_nep141_resolver::ext(#near_sdk::env::current_account_id())
.with_static_gas(GAS_FOR_RESOLVE_TRANSFER)
.ft_resolve_transfer(
transfer.sender_id.clone(),
transfer.receiver_id.clone(),
transfer.amount.into(),
),
)
}

fn ft_total_supply(&self) -> #near_sdk::json_types::U128 {
<Self as #me::standard::nep141::Nep141Controller>::total_supply().into()
#me::standard::nep141::Nep141Controller::total_supply(self).into()
}

fn ft_balance_of(&self, account_id: #near_sdk::AccountId) -> #near_sdk::json_types::U128 {
<Self as #me::standard::nep141::Nep141Controller>::balance_of(&account_id).into()
#me::standard::nep141::Nep141Controller::balance_of(self, &account_id).into()
}
}

Expand All @@ -154,12 +158,51 @@ pub fn expand(meta: Nep141Meta) -> Result<TokenStream, darling::Error> {
receiver_id: #near_sdk::AccountId,
amount: #near_sdk::json_types::U128,
) -> #near_sdk::json_types::U128 {
#me::standard::nep141::Nep141Controller::resolve_transfer(
self,
sender_id,
receiver_id,
amount.into(),
).into()
use #near_sdk::{env, PromiseResult, serde_json, json_types::U128};
use #me::standard::nep141::*;

let amount = amount.0;

let ft_on_transfer_promise_result = env::promise_result(0);

let unused_amount = match ft_on_transfer_promise_result {
PromiseResult::NotReady => env::abort(),
PromiseResult::Successful(value) => {
if let Ok(U128(unused_amount)) = serde_json::from_slice::<U128>(&value) {
std::cmp::min(amount, unused_amount)
} else {
amount
}
}
PromiseResult::Failed => amount,
};

let refunded_amount = if unused_amount > 0 {
let receiver_balance = Nep141Controller::balance_of(self, &receiver_id);
if receiver_balance > 0 {
let refund_amount = std::cmp::min(receiver_balance, unused_amount);
let transfer = Nep141Transfer {
sender_id: receiver_id,
receiver_id: sender_id,
amount: refund_amount,
memo: None,
msg: None,
revert: true,
};

Nep141Controller::transfer(self, &transfer)
.unwrap_or_else(|e| env::panic_str(&e.to_string()));

refund_amount
} else {
0
}
} else {
0
};

// Used amount
U128(amount - refunded_amount)
}
}
})
Expand Down
Loading

0 comments on commit 03d98d8

Please sign in to comment.