Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(near-contract-standards): NEP-199 - Non-Fungible Token Royalties and Payouts #1077

Draft
wants to merge 50 commits into
base: master
Choose a base branch
from

Conversation

ruseinov
Copy link
Collaborator

@ruseinov ruseinov commented Aug 20, 2023

A continuation of #778 - NEP-199 standard implementation

This needs tests for the examples.
It also has one failing test that seemingly has nothing to do with the changes.

@ruseinov
Copy link
Collaborator Author

@frol I'm not sure if the sim change should belong to this PR at all to be honest. Thoughts?

@ruseinov ruseinov marked this pull request as draft August 20, 2023 19:16
Cargo.toml Outdated Show resolved Hide resolved
@frol frol changed the title Feat/payout feat(near-contract-standards): NEP-199 - Non-Fungible Token Royalties and Payouts Aug 31, 2023
@frol
Copy link
Collaborator

frol commented Sep 10, 2023

@ruseinov May I ask you to submit a new PR to fix the CI failure? (I believe it is not related to this change)

@ruseinov
Copy link
Collaborator Author

@ruseinov May I ask you to submit a new PR to fix the CI failure? (I believe it is not related to this change)

definitely!

@ruseinov ruseinov marked this pull request as ready for review September 19, 2023 15:29
@ruseinov
Copy link
Collaborator Author

@ruseinov May I ask you to submit a new PR to fix the CI failure? (I believe it is not related to this change)

This seems to only be reproducible on this branch.
I have tried reducing the gas amount to 10_000_000_000_000. Then assert!(res.is_failure()); works, but assert_eq!(res.logs().len(), 0); still does not.

@ruseinov
Copy link
Collaborator Author

Thanks for the changes @ruseinov ! LGTM with two small nits.

Cheers! Fixed!

Comment on lines 96 to 97
// TODO: Figure out where the percent has to be coming from.
Royalties::new(prefix, 0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, currently the royalties.payout.percent is a public field, and it seems that the expectation is that developers using this extension will adjust it. I find this contract-level percentage quite inflexible - 0% shared between all payouts is useless, 1-99% could be limiting or confusing, and 100% seems to be the only reasonable option, but then payout to the token owner needs to be explicitly listed (storage wasted).

In either cases, please, provide usage-level documentation on how to use this stuff.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that this could be done in a different way. Why don't we sum all the payouts and send the rest to the owner:

    pub fn create_payout(&self, balance: Balance, owner_id: &AccountId) -> Payout {
        let mut payout = Payout {
            payout: self
                .accounts
                .iter()
                .map(|(account, percent)| (account.clone(), apply_percent(percent, balance).into()))
                .collect(),
        };
        let rest = balance - payout.payout.values().fold(0, |acc, &sum| acc + sum);
        let owner_payout: u128 = payout.payout.get(owner_id).map_or(0, |x| x.0) + rest;
        payout.payout.insert(owner_id.clone(), owner_payout.into());
        payout
    }

@ruseinov ruseinov requested a review from frol November 3, 2023 17:11
@ruseinov
Copy link
Collaborator Author

ruseinov commented Nov 8, 2023

@frol @agostbiro This needs to be re-approved and merged if all good.

@ruseinov
Copy link
Collaborator Author

ruseinov commented Nov 9, 2023

@frol @agostbiro This needs to be re-approved and merged if all good.

Right, after updating to latest it needs some small fixes, I'll take care of those.

@ruseinov
Copy link
Collaborator Author

Done.

@frol frol marked this pull request as draft November 18, 2023 19:27
@ruseinov ruseinov requested a review from frol November 24, 2023 15:16
@ruseinov ruseinov marked this pull request as ready for review November 24, 2023 19:00
@ruseinov
Copy link
Collaborator Author

@frol seems to be fixed, some flaky example tests due to AddrInUse, but not related to this PR.

Comment on lines +59 to +77
let mut payout = Payout {
payout: self
.accounts
.iter()
.map(|(account, percent_per_token)| {
(
account.clone(),
U128::from(apply_percent(
*percent_per_token.get(&token_id).unwrap_or(&0),
balance,
)),
)
})
.filter(|(_, payout)| payout.0 > 0)
.collect(),
};
let rest = balance - payout.payout.values().fold(0, |acc, &sum| acc + sum.0);
let owner_payout: u128 = payout.payout.get(owner_id).map_or(0, |x| x.0) + rest;
payout.payout.insert(owner_id.clone(), owner_payout.into());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something crazy is going on here. Royalties should be just an UnorderedMap<TokenId, HashMap<AccountId, Percentage>>, and then create_payout will be just:

royalties.get(token_id)
    .items()
    .map(|(account_id, percentage)| (account_id, balance * percentage / 100))
    .collect()

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ruseinov Also, as a rule of thumb, if you write a code that iterates over near_sdk collections without bounds and potentially may exceed more than 100 lookups, you need to redesign the approach as that won't work with the growing number records. In this case, as I mentioned above, it needs a completely different layout

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My mistake, I made a fast fix of a problem that completely redefined the solution.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the truth is that the original map is bound by validate method, it only allows for 10 accounts. So technically it's not an issue.
The data structure and it's access are a little bit convoluted, but if we were to redesign this collection and the approach - we'd have to discuss how we are going to validate bounds going forward.

Comment on lines +66 to +77
/// impl NonFungibleTokenPayout for Contract {
/// #[allow(unused_variables)]
/// fn nft_payout(
/// &self,
/// token_id: String,
/// balance: U128,
/// max_len_payout: Option<u32>,
/// ) -> Payout {
/// self.tokens.nft_payout(token_id, balance, max_len_payout)
/// }
/// #[payable]
/// fn nft_transfer_payout(
Copy link
Collaborator

@frol frol Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized that most of the examples lack at least demo methods on how to mint tokens and, in this case, how to assign royalties to the tokens. This would have highlighted the problem that to assign royalties one would need to access internal fields deep down and manipulate them manually:

self.tokens.royalties.unwrap().royalties.entry(token_id).insert(royalties_to_account_id, 5)

Without this example, this implementation most probably won't be used and we may even argue that we may not need to merge it in the first place.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I have actually been thinking about that too when I first glanced at the original PR, but then it got forgotten as I moved forward.
We do indeed have to expose an interface to assign royalties.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just going to leave this here: what we need to do is introduce a create_royalties method or similar that will only be accessible to nft owner.

@frol frol marked this pull request as draft December 20, 2023 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: NEW❗
Development

Successfully merging this pull request may close these issues.

4 participants