polkadot-sdk icon indicating copy to clipboard operation
polkadot-sdk copied to clipboard

Granular NFT traits and new XCM NFT types

Open mrshiposha opened this issue 1 year ago • 7 comments

Overview

This PR is meant to become a follow-up to #5620 that defined new NFT traits. Currently, this PR includes some of the changes from #5620.

This PR provides new XCM types and tools for building NFT Asset Transactors. The new types use general and granular NFT traits from #5620.

The new XCM adapters and utility types to work with NFTs can be considered the main deliverable of the XCM NFT proposal. The new types use a more general approach, making integrating into any chain with various NFT implementations easier.

For instance, different implementations could use:

  • different ID assignment approaches
    • predefined NFT IDs - pallet-uniques, pallet-nfts
    • derived NFT IDs (NFT IDs are automatically derived from collection IDs) - Unique Network, ORML/Acala, Aventus
  • classless (collection-less) tokens - CoreTime NFTs, Aventus NFT Manager NFTs
  • in-class (in-collection) tokens - Unique Network, pallet-uniques, pallet-nfts, ORML/Acala
  • different approaches to storing associated data on-chain:
    • data is stored entirely separately from tokens - pallet-uniques
    • data is stored partially or entirely within tokens (i.e., burning a token means destroying it with its data) - pallet-nfts (partially), Unique Network, ORML/Acala

With new types, these differences can be either abstracted away or worked around (see the example with pallet-nfts below).

Moreover, the new types provide greater flexibility for supporting derivative NFTs, allowing several possible approaches depending on the given chain's team's goals. See the section about derivatives below.

Also, this is the PR I mentioned in the https://github.com/paritytech/polkadot-sdk/issues/4073 issue, as it can be viewed as the solution. In particular, the new adapter (UniqueInstancesAdapter) requires the Transfer operation with the FromTo strategy. This brings the attention of both a developer and a reviewer to the FromTo strategy (meaning that the transfer checks if the asset can be transferred from a given account to another account) both at trait bound and at the call site without sacrificing the flexibility of the NFT traits.

New types for xcm-builder and xcm-executor

This PR introduces several new XCM types.

UniqueInstancesAdapter is a new TransactAsset adapter that supersedes both NonFungibleAdapter and NonFungiblesAdapter (for reserve-based transfers only, as teleports can't be implemented appropriately without transferring the NFT metadata alongside it; no standard solution exists for that yet. Hopefully, the Felloweship RFC 125 (PR, text) will help with that).

Thanks to the new Matcher types, the new adapter can be used instead of both NonFungibleAdapter and NonFungiblesAdapter:

  • MatchesInstance (a trait)
  • MatchInClassInstances
  • MatchClasslessInstances

Superseding the old adapters for pallet-uniques

Here is how the new UniqueInstancesAdapter in Westend Asset Hub replaces the NonFungiblesAdapter:

/// Means for transacting unique assets.
pub type UniquesTransactor = UniqueInstancesAdapter<
	AccountId,
	LocationToAccountId,
	MatchInClassInstances<UniquesConvertedConcreteId>,
	pallet_uniques::asset_ops::Item<Uniques>,
>;

MatchInClassInstances allows us to reuse the already existing UniquesConvertedConcreteId matcher. The pallet_uniques::asset_ops::Item<Uniques> already implements the needed traits and can destroy and re-create NFT items without losing their data.

So, migrating from the old adapter to the new one regarding runtime config changes is easy.

NOTE: pallet_uniques::asset_ops::Item grants access to the asset operations of NFT items of a given pallet-uniques instance, whereas pallet_uniques::asset_ops::Collection grants access to the collection operations.

Declarative modification of an NFT engine

This PR doesn't include the implementation of the new traits for pallet-nfts. However, you can find the implementation for it on this branch: https://github.com/UniqueNetwork/polkadot-sdk/tree/xcm-nft-dev-env.

The pallet-nfts is a good example of an NFT engine that can't destroy and then re-create an NFT without losing its data in general (as is ORML/Acala, Unique Network, Aventus, and possibly more).

Yet, the UniqueInstancesAdapter requires an NFT engine to be able to create and destroy NFTs. We will avoid implementing the new functionality for the NFT engine directly. Instead, we will illustrate how to model the "create" and "destroy" operations using other operations. Essentially, we will declaratively create a new NFT engine right in the runtime config. Here is how this could look.

pub type NftsConvertedConcreteId = assets_common::NftsConvertedConcreteId<NftsPalletLocation>;

type NftsOps = pallet_nfts::asset_ops::Item<Nfts>;

type NftsStashOps = SimpleStash<TreasuryAccount, NftsOps>;

/// Means for transacting nft original assets.
type NftsTransactor = UniqueInstancesAdapter<
	AccountId,
	LocationToAccountId,
	MatchInClassInstances<NftsConvertedConcreteId>,
	UniqueInstancesOps<
	    RestoreOnCreate<NftsStashOps>,
	    NftsOps,
	    StashOnDestroy<NftsStashOps>,
	>,
>;

The main actor here is UniqueInstancesOps. It accepts implementations of the "create", "transfer", and "destroy" operations and merges them into one "NFT engine" that implements all three.

The RestoreOnCreate predictably uses the Restore operation when asked to create an NFT. StashOnDestroy works similarly by using the Stash operation.

The pallet_nfts::asset_ops::Item<Nfts> doesn't implement the Restore and Stash operations, however. So, we utilize SimpleStash, which takes a stash account and an NFT engine capable of transferring assets using the FromTo strategy. It transfers an NFT to the stash account when stashing and from the stash account when restoring.

To sum things up, this example illustrates how the existing NFT engine can be declaratively modified to fit into the XCM adapter if needed.

Supporting derivative NFTs (reserve-based model)

Derivative NFTs can be implemented differently depending on a given chain's team's goals.

For instance, if a team wants to separate the interaction with their chain's original NFTs and derivative NFTs, they could create a separate instance of an NFT engine that works solely with derivatives (if the NFT engine allows that).

For example, pallet-uniques is such an engine. A team could create another instance of this pallet, configuring it to use AssetId as a collection ID and AssetInstance as an item ID. In that case, the configuration of an NFT transactor is pretty much the same as in the example we've seen for pallet-uniques above, except for the matcher, which should reject NFT originals (because the originals are handled separately).


Alternatively, a team might want to interact uniformly with originals and derivatives, ensuring that any clients working with the given chain's NFTs can work with the derivatives without any additional effort (or with a minimal one). Also, there could be complex NFT-related on-chain logic. By choosing the uniform interaction approach, the team can ensure that derivatives can participate in the same logic as the originals.

The uniform approach implies that derivatives will share the chain-local NFT IDs. So, to support this use scenario, this PR provides a minimal separate adapter called UniqueInstancesDepositAdapter to "register" derivatives (i.e., establish the mapping between the (AssetId, AssetInstance) and chain-local NFT ID). This adapter requires the usual AccountId and LocationToAccountId generic parameters and only one additional parameter that implements the Create operation, which takes the beneficiary account and the NonFungibleAsset = (AssetId, AssetInstance) as parameters to create a new derivative NFT.

NOTE: This is only needed if chain-local NFT IDs differ from XCM NFT IDs.

This is what its usage looks like:

pub struct CreateDerivativeNft;
impl AssetDefinition for CreateDerivativeNft {
    // The derivative ID is the same as the chain-local NFT ID
    type Id = AssetIdOf<NftEngine>;
}
impl Create<
    Owned<
        AccountId,
	DeriveAndReportId<NonFungibleAsset, AssetIdOf<NftEngine>>
    >
> for CreateDerivativeNft {
    fn create(
        strategy: Owned<
	    AccountId,
	    DeriveAndReportId<NonFungibleAsset, AssetIdOf<NftEngine>>
	>
    ) -> Result<AssetIdOf<NftEngine>, DispatchError> {
        let Owned {
	    owner,
	    id_assignment,
	    ..
	} = strategy;

	let (
	    asset_id,
	    asset_instance
        ) = id_assignment.params;
   
        let derivative_id = todo!("mint a derivative NFT");
	
	todo!("remember the correspondence between the XCM NFT ID and the derivative_id");

	Ok(derivative_id)
    }
}

type NftDerivativesRegistrar = UniqueInstancesDepositAdapter<
	AccountId,
	LocationToAccountId,
	CreateDerivativeNft,
>;

When a derivative is registered, and the mapping between chain-local NFT ID and XCM NFT ID is saved, we need a matcher to transact known derivatives. Assuming the given chain provides an implementation for the DerivativesRegistry trait (which provides the info about the chain-local NFT ID <-> XCM NFT ID mapping), the team can use the MatchDerivativeInstances, which will match only the derivatives found in the registry.

Finally, this PR provides the pallet-derivatives that can facilitate the creation of such registries. This pallet implements the needed traits and can optionally provide extrinsics for registering derivatives (for instance, for registering derivative collections). Yet, this pallet is optional. The chain's team can use its own tools to store information about derivatives.

mrshiposha avatar Apr 25 '24 17:04 mrshiposha

CC @franciscoaguirre

mrshiposha avatar Apr 25 '24 17:04 mrshiposha

@franciscoaguirre @xlc This PR adds new granular NFT traits to frame-support and provides new XCM types, giving us the instruments to implement XCM NFT in any chain regardless of differences in NFT solutions used in the ecosystem (different pallets, which represent NFTs differently or incompatible with each other, or smart contract-based solutions).

This PR could undoubtedly be divided into at least two. However, I decided to provide all the pieces at once so we can discuss the complete picture. I can divide the PR if needed.

mrshiposha avatar Jul 02 '24 15:07 mrshiposha

The CI pipeline was cancelled due to failure one of the required jobs. Job name: test-linux-stable 2/3 Logs: https://gitlab.parity.io/parity/mirrors/polkadot-sdk/-/jobs/6615539

paritytech-cicd-pr avatar Jul 03 '24 14:07 paritytech-cicd-pr

The CI pipeline was cancelled due to failure one of the required jobs. Job name: cargo-clippy Logs: https://gitlab.parity.io/parity/mirrors/polkadot-sdk/-/jobs/6615523

paritytech-cicd-pr avatar Jul 03 '24 14:07 paritytech-cicd-pr

This pull request has been mentioned on Polkadot Forum. There might be relevant details there:

https://forum.polkadot.network/t/persistent-parachains-for-a-long-term-testnet-on-v1-14/9886/1

Polkadot-Forum avatar Sep 05 '24 09:09 Polkadot-Forum

@franciscoaguirre, I opened a separate PR #5620 containing only the new NFT traits.

Later, I will refactor the current PR #4300 description and code to make it solely XCM NFT-focused.

mrshiposha avatar Sep 06 '24 12:09 mrshiposha

@mrshiposha can you reach out to other NFT players in the ecosystem for reviews? Parity engineers have limited NFT user journeys experience, I would like to get "experts" input.

acatangiu avatar Feb 11 '25 15:02 acatangiu

@acatangiu @franciscoaguirre Hello! I updated the PR using the types from #5620, refactored it, added doc comments, and updated the PR's description.

Could you guys please take a look? :pray:

mrshiposha avatar May 28 '25 13:05 mrshiposha

I see quite a few very useful changes here:

  • Traits for flexible NFT ID handling
  • Support for creation of derivative NFTs -> a fun example use case is enabled when NFT created on one network nests inside an NFT created in another network
  • UniqueInstancesAdapter for reserve-based NFT transfers nicely abstracts NFT types

All good changes overall!

gztensor avatar Jun 11 '25 17:06 gztensor

LGTM

xlc avatar Jun 17 '25 03:06 xlc

This pull request has been mentioned on Polkadot Forum. There might be relevant details there:

https://forum.polkadot.network/t/2025-06-20-technical-fellowship-opendev-call/13443/1

Polkadot-Forum avatar Jun 20 '25 14:06 Polkadot-Forum

@bkontur @franciscoaguirre please take a look at this commit: https://github.com/paritytech/polkadot-sdk/pull/4300/commits/cd4df1235649fdb70a4f7a8f05f9100d03ac1a5b.

It should address the current review comments.

NOTE: <XCM_BUILDER>/unique_instances/ops.rs was not moved to the pallet-derivatives. Only the OwnerConvertedLocation was moved there. Other types can be used for AssetTransactors that deal with both originals and derivatives. So, it seems they are more XCM-specific, so I left them there.

mrshiposha avatar Jul 03 '25 14:07 mrshiposha

@bkontur @franciscoaguirre please take a look at this commit: cd4df12.

It should address the current review comments.

NOTE: <XCM_BUILDER>/unique_instances/ops.rs was not moved to the pallet-derivatives. Only the OwnerConvertedLocation was moved there. Other types can be used for AssetTransactors that deal with both originals and derivatives. So, it seems they are more XCM-specific, so I left them there.

@mrshiposha as we discussed on the call this week, here are my suggestions: https://github.com/UniqueNetwork/polkadot-sdk/pull/14

bkontur avatar Jul 31 '25 12:07 bkontur

/cmd prdoc --audience runtime_dev --bump patch

bkontur avatar Jul 31 '25 21:07 bkontur

Command "prdoc --audience runtime_dev --bump patch" has failed ❌! See logs here

github-actions[bot] avatar Jul 31 '25 22:07 github-actions[bot]