difftreelog
Merge commit '1f47f964a0fc5eaa160d9f6fc463551ad4dbd74e' into feature/NFTPAR-196_sponsor_setVariableMetadata
in: master
19 files changed
.gitignorediffbeforeafterboth10einstein_key_file10einstein_key_file11*store_key*.json11*store_key*.json121213/.idea/13/.idea/1415tests/.vscodeCargo.lockdiffbeforeafterboth345634563457[[package]]3457[[package]]3458name = "nft-runtime"3458name = "nft-runtime"3459version = "3.0.0"3459version = "2.1.0"3460dependencies = [3460dependencies = [3461 "frame-benchmarking",3461 "frame-benchmarking",3462 "frame-executive",3462 "frame-executive",378537853786[[package]]3786[[package]]3787name = "pallet-nft"3787name = "pallet-nft"3788version = "3.0.0"3788version = "2.1.0"3789dependencies = [3789dependencies = [3790 "frame-benchmarking",3790 "frame-benchmarking",3791 "frame-support",3791 "frame-support",README.mddiffbeforeafterboth445## Project Description5## Project Description667The NFT Pallet is the core of NFT functionality. Like ERC-721 standard in Ethereum ecosystem, this pallet provides the basement for creating collections of unique non-divisible things, also called Non Fungible Tokens (NFTs), minting NFT of a given Collection, and managing their ownership.7The NFT Pallet is the core of NFT functionality. Like ERC-721 standard in Ethereum ecosystem, this pallet provides the8basement for creating collections of unique non-divisible things, also called Non Fungible Tokens (NFTs), minting NFT of9a given Collection, and managing their ownership.8109The pallet also enables storing NFT properties. Though (according to ERC-721) NFT properties belong to logic of a concrete application that operates a Collection, so purposefully the NFT Tracking Module does not have any knowledge about properties except their byte size leaving application logic out to be controlled by Smart Contracts.11The pallet also enables storing NFT properties. Though (according to ERC-721) NFT properties belong to logic of a12concrete application that operates a Collection, so purposefully the NFT Tracking Module does not have any knowledge13about properties except their byte size leaving application logic out to be controlled by Smart Contracts.101411The NFT Chain also provides:15The NFT Chain also provides:121613* Smart Contracts Pallet and example smart contract that interacts with NFT Runtime17- Smart Contracts Pallet and example smart contract that interacts with NFT Runtime14* ERC-1155 Functionality (currently PoC as Re-Fungible tokens, i.e. items that are still unique, but that can be split between multiple users)18- ERC-1155 Functionality (currently PoC as Re-Fungible tokens, i.e. items that are still unique, but that can be split19 between multiple users)15* Variety of economic options for dapp producers to choose from to create freemium games and other ways to attract users. As a step one, we implemented an economic model when a collection sponsor can be set to pay for collection Transfer transactions.20- Variety of economic options for dapp producers to choose from to create freemium games and other ways to attract21 users. As a step one, we implemented an economic model when a collection sponsor can be set to pay for collection22 Transfer transactions.162317Wider NFT Ecosystem (most of it was developed during Hackusama):24Wider NFT Ecosystem (most of it was developed during Hackusama):18* [SubstraPunks Game hosted on IPFS](https://github.com/usetech-llc/substrapunks)19* [NFT Wallet and UI](https://uniqueapps.usetech.com/#/nft)20* [NFT Asset for Unity Framework](https://github.com/usetech-llc/nft_unity)212526- [SubstraPunks Game hosted on IPFS](https://github.com/usetech-llc/substrapunks)27- [NFT Wallet and UI](https://uniqueapps.usetech.com/#/nft)28- [NFT Asset for Unity Framework](https://github.com/usetech-llc/nft_unity)2922Please see our [walk-thorugh instructions](doc/hackusama_walk_through.md) to try everything out!30Please see our [walk-thorugh instructions](doc/hackusama_walk_through.md) to try everything out!233124## Hackusama Update32## Hackusama Update253326During the Kusama Hackaphon the following changes were made:34During the Kusama Hackaphon the following changes were made:3527* Enabled Smart Contracts Pallet36- Enabled Smart Contracts Pallet28* Enabled integration between Smart Contracts and NFT Pallet (required special edition of RC4 Substrate version)37- Enabled integration between Smart Contracts and NFT Pallet (required special edition of RC4 Substrate version)29* Fixed misc. bugs in NFT Pallet38- Fixed misc. bugs in NFT Pallet30* Deployed NFT TestNet. Public node available at wss://unique.usetech.com, custom UI types - see below in this README.39- Deployed NFT TestNet. Public node available at wss://unique.usetech.com, custom UI types - see below in this README.31* New Features:40- New Features:32 * Re-Fungible Token Mode41 - Re-Fungible Token Mode33 * Off-Chain Schema to store token image URLs42 - Off-Chain Schema to store token image URLs34 * Alternative economic model43 - Alternative economic model35 * White Lists and Public Mint Permission44 - White Lists and Public Mint Permission36* Use example: [SubstraPunks Game](https://github.com/usetech-llc/substrapunks), fully hosted on IPFS and NFT Testnet Blockchain.45- Use example: [SubstraPunks Game](https://github.com/usetech-llc/substrapunks), fully hosted on IPFS and NFT Testnet46 Blockchain.374738## Application Development48## Application Development394940If you are building an application that operates NFT tokens, use [this document](doc/application_development.md).50If you are building an application that operates NFT tokens, use [this document](doc/application_development.md).415142## Building52## Building435344Building NFT chain requires special versions of Rust and toolchain. We don't use the most recent versions of everything so that we can keep the builds stable.54Building NFT chain requires special versions of Rust and toolchain. We don't use the most recent versions of everything55so that we can keep the builds stable.4556461. Install Rust:571. Install Rust:475860rustup default nightly-2021-01-2771rustup default nightly-2021-01-2761```72```6273634. Add wasm target for default toolchain:744. Add wasm target for nightly toolchain:647565```bash76```bash66rustup target add wasm32-unknown-unknown --toolchain nightly-2021-01-2777rustup target add wasm32-unknown-unknown --toolchain nightly-2021-01-2784cargo run -- --dev95cargo run -- --dev85```96```869787Detailed logs may be shown by running the node with the following environment variables set: `RUST_LOG=debug RUST_BACKTRACE=1 cargo run -- --dev`.98Detailed logs may be shown by running the node with the following environment variables set:99`RUST_LOG=debug RUST_BACKTRACE=1 cargo run -- --dev`.8810089If you want to see the multi-node consensus algorithm in action locally, then you can create a local testnet with two validator nodes for Alice and Bob, who are the initial authorities of the genesis chain that have been endowed with testnet units. Give each node a name and expose them so they are listed on the Polkadot [telemetry site](https://telemetry.polkadot.io/#/Local%20Testnet). You'll need two terminal windows open.101If you want to see the multi-node consensus algorithm in action locally, then you can create a local testnet with two102validator nodes for Alice and Bob, who are the initial authorities of the genesis chain that have been endowed with103testnet units. Give each node a name and expose them so they are listed on the Polkadot104[telemetry site](https://telemetry.polkadot.io/#/Local%20Testnet). You'll need two terminal windows open.9010591We'll start Alice's substrate node first on default TCP port 30333 with her chain database stored locally at `/tmp/alice`. The bootnode ID of her node is `QmQZ8TjTqeDj3ciwr93EJ95hxfDsb9pEYDizUAbWpigtQN`, which is generated from the `--node-key` value that we specify below:106We'll start Alice's substrate node first on default TCP port 30333 with her chain database stored locally at107`/tmp/alice`. The bootnode ID of her node is `QmQZ8TjTqeDj3ciwr93EJ95hxfDsb9pEYDizUAbWpigtQN`, which is generated from108the `--node-key` value that we specify below:9210993```bash110```bash94cargo run -- \111cargo run -- \100 --validator117 --validator101```118```102119103In the second terminal, we'll start Bob's substrate node on a different TCP port of 30334, and with his chain database stored locally at `/tmp/bob`. We'll specify a value for the `--bootnodes` option that will connect his node to Alice's bootnode ID on TCP port 30333:120In the second terminal, we'll start Bob's substrate node on a different TCP port of 30334, and with his chain database121stored locally at `/tmp/bob`. We'll specify a value for the `--bootnodes` option that will connect his node to Alice's122bootnode ID on TCP port 30333:104123105```bash124```bash106cargo run -- \125cargo run -- \152171153## Running Integration Tests172## Running Integration Tests154173155See [tests/README.md](./tests/README.md).174See [tests/README.md](./tests/README.md).175doc/application_development.mddiffbeforeafterboth223## Architecture3## Architecture445Both centralized and serverless architectures are supported and application creator can decide which one to use depending on how much logic and data they intend to manage off-chain and off-line.5Both centralized and serverless architectures are supported and application creator can decide which one to use6depending on how much logic and data they intend to manage off-chain and off-line.677### Server-based architecture8### Server-based architecture899In server-based architecture the application creator manually creates NFT collections and authorizes server to perform operations on each collection. See NFT palette methods that have admin permission level to understand better what server (or administrators) can do.10In server-based architecture the application creator manually creates NFT collections and authorizes server to perform11operations on each collection. See NFT palette methods that have admin permission level to understand better what server12(or administrators) can do.10131114121513### Serverless architecture16### Serverless architecture141715In serverless architecture the application creator does all initialization and token distribution manually. Alternatively, smart contracts may be used in order to implement some distribution mechanics such as token sales or claiming. The dApp has access to all properties of an NFT token, but it does not have any elevated permissions, since it resides on user site. All operations happen on user's behalf signed by user's private key.18In serverless architecture the application creator does all initialization and token distribution manually.19Alternatively, smart contracts may be used in order to implement some distribution mechanics such as token sales or20claiming. The dApp has access to all properties of an NFT token, but it does not have any elevated permissions, since it21resides on user site. All operations happen on user's behalf signed by user's private key.16221723182427#### CreateCollection33#### CreateCollection283429##### Description35##### Description30This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner and admin of the collection are set to the address that signed the transaction. Both addresses can be changed later.313637This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of38certain length. The initial owner and admin of the collection are set to the address that signed the transaction. Both39addresses can be changed later.4032##### Permissions41##### Permissions33Anyone344243- Anyone4435##### Parameters45##### Parameters36customDataSz: Size of NFT properties data.374647- customDataSz: Size of NFT properties data.4838##### Events49##### Events39CollectionCreated40CollectionID: Globally unique identifier of newly created collection.41Owner: Collection owner425051- CollectionCreated52 - CollectionID: Globally unique identifier of newly created collection.53 - Mode: Collection mode54 - Owner: Collection owner5543#### ChangeCollectionOwner56#### ChangeCollectionOwner445745##### Description58##### Description5946Change the owner of the collection60Change the owner of the collection476148##### Permissions62##### Permissions49Collection Owner506364- Collection Owner6551##### Parameters66##### Parameters52CollectionId536768- CollectionId6954#### DestroyCollection70#### DestroyCollection557156##### Description72##### Description57DANGEROUS: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.587374DANGEROUS: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose75real money.7659##### Permissions77##### Permissions60Collection Owner617879- Collection Owner8062##### Parameters81##### Parameters63CollectionId648283- CollectionId8465#### CreateItem85#### CreateItem668667##### Description87##### Description8868This method creates a concrete instance of NFT Collection created with CreateCollection method.89This method creates a concrete instance of NFT Collection created with CreateCollection method.699070##### Permissions91##### Permissions71Collection Owner72Collection Admin739293- Collection Owner94- Collection Admin9574##### Parameters96##### Parameters75CollectionID: ID of the collection76Properties: Array of bytes that contains NFT properties. Since NFT Module is agnostic of properties’ meaning, it is treated purely as an array of bytes77Owner: Address, initial owner of the NFT789798- CollectionID: ID of the collection99- Properties: Array of bytes that contains NFT properties. Since NFT Module is agnostic of properties’ meaning, it is100 treated purely as an array of bytes101- Owner: Address, initial owner of the NFT10279##### Events103##### Events80ItemCreated104ItemCreated81ItemId: Identifier of newly created NFT, which is unique within the Collection, so the NFT is uniquely identified with a pair of values: CollectionId and ItemId.105ItemId: Identifier of newly created NFT, which is unique within the Collection, so the NFT is uniquely identified with a pair of values: CollectionId and ItemId.106Recipient: Address, owner of newly created item8210783#### BurnItem108#### BurnItem8410985##### Description110##### Description11186This method destroys a concrete instance of NFT.112This method destroys a concrete instance of NFT.8711388##### Permissions114##### Permissions89Collection Owner90Collection Admin91Current NFT Owner92115116- Collection Owner117- Collection Admin118- Current NFT Owner11993##### Parameters120##### Parameters94CollectionID: ID of the collection95ItemID: ID of NFT to burn96121122- CollectionID: ID of the collection123- ItemID: ID of NFT to burn12497##### Events125##### Events98ItemDestroyed99CollectionID100ItemId: Identifier of burned NFT101126127- ItemDestroyed128 - CollectionID129 - ItemId: Identifier of burned NFT102130103#### AddCollectionAdmin131#### AddCollectionAdmin104132105##### Description133##### Description106NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership.107134135NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can136issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership.137108This method adds an admin of the Collection.138This method adds an admin of the Collection.109139110##### Permissions140##### Permissions111Collection Owner112Collection Admin113141142- Collection Owner143- Collection Admin144114##### Parameters145##### Parameters115CollectionID: ID of the Collection to add admin for116Admin: Address of new admin to add117146147- CollectionID: ID of the Collection to add admin for148- Admin: Address of new admin to add149118#### RemoveCollectionAdmin150#### RemoveCollectionAdmin119151120##### Description152##### Description121Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.122153154Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which155case only Collection Owner will be able to add an Admin.156123##### Permissions157##### Permissions124Collection Owner125Collection Admin126158159- Collection Owner160- Collection Admin161127##### Parameters162##### Parameters128CollectionID: ID of the Collection to remove admin for129Admin: Address of admin to remove130163164- CollectionID: ID of the Collection to remove admin for165- Admin: Address of admin to remove166131### Item Ownership and Transfers167### Item Ownership and Transfers168132This group of methods allows managing NFT ownership.169This group of methods allows managing NFT ownership.133170134#### GetOwner171#### GetOwner135172136##### Description173##### Description137Return the address of the NFT owner. 138174175Return the address of the NFT owner.176139##### Permissions177##### Permissions140Anyone141178179- Anyone180142##### Parameters181##### Parameters143CollectionId144ItemId: ID of the NFT145182183- CollectionId184- ItemId: ID of the NFT185146##### Returns186##### Returns187147Owner address188Owner address148189149#### BalanceOf190#### BalanceOf150191151##### Description192##### Description152This method is included for compatibility with ERC-721. Return the total count of NFTs of a given Collection that belong to a given address. 153193194This method is included for compatibility with ERC-721. Return the total count of NFTs of a given Collection that belong195to a given address.196154##### Permissions197##### Permissions155Anyone156198199- Anyone200157##### Parameters201##### Parameters158CollectionId159Address to count NFTs for160202203- CollectionId204- Address to count NFTs for205161##### Returns206##### Returns207162Total count of NFTs for Address208Total count of NFTs for Address163209164165#### Transfer210#### Transfer166211167##### Description212##### Description213168Change ownership of the NFT.214Change ownership of the NFT.169215170##### Permissions216##### Permissions171Collection Owner172Collection Admin173Current NFT owner174217218- Collection Owner219- Collection Admin220- Current NFT owner221175##### Parameters222##### Parameters176Recipient: Address of token recipient177ClassId: ID of item class178ItemId: ID of the item179223224- Recipient: Address of token recipient225- ClassId: ID of item class226- ItemId: ID of the item227180#### TransferFrom228#### TransferFrom181229182##### Description230##### Description183Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.184231232Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method233executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this234owner.235185##### Permissions236##### Permissions186Collection Owner187Collection Admin188Current NFT owner189Address approved by current NFT owner190237238- Collection Owner239- Collection Admin240- Current NFT owner241- Address approved by current NFT owner242191##### Parameters243##### Parameters192Recipient: Address of token recipient193ClassId: ID of item class194ItemId: ID of the item195244245- Recipient: Address of token recipient246- ClassId: ID of item class247- ItemId: ID of the item196248197#### Approve249#### Approve198250199##### Description251##### Description252200Set, change, or remove approved address to transfer the ownership of the NFT.253Set, change, or remove approved address to transfer the ownership of the NFT.201254202##### Permissions255##### Permissions203Collection Owner204Collection Admin205Current NFT owner206256257- Collection Owner258- Collection Admin259- Current NFT owner260207##### Parameters261##### Parameters208Approved: Address that is approved to transfer this NFT or zero (if needed to remove approval)209ClassId: ID of item class210ItemId: ID of the item211262263- Approved: Address that is approved to transfer this NFT or zero (if needed to remove approval)264- ClassId: ID of item class265- ItemId: ID of the item266212#### GetApproved267#### GetApproved213268214##### Description269##### Description270215Get the approved address for a single NFT.271Get the approved address for a single NFT.216272217##### Permissions273##### Permissions218Anyone219274275- Anyone276220##### Parameters277##### Parameters221ClassId: ID of item class222ItemId: ID of the item223278279- ClassId: ID of item class280- ItemId: ID of the item281224##### Returns282##### Returns283225Approved address284Approved address226285doc/demo_milestone1-2.mddiffbeforeafterboth667### A running chain7### A running chain889**Substrate based blockchain node to host NFT Tracking Module created with substrate-up scripts (currently substrate-up only support Substrate 1.0)**9**Substrate based blockchain node to host NFT Tracking Module created with substrate-up scripts (currently substrate-up10only support Substrate 1.0)**101111We have created chain based on both versions of substrate: 1 and 2. Nonetheless, the substrate 1 is not being widely used and we decided to obsolete this branch of code. The code still exists in [substrate1](https://github.com/usetech-llc/nft_parachain/tree/substrate1) branch, but this delivery document is based on substrate 2 version.12We have created chain based on both versions of substrate: 1 and 2. Nonetheless, the substrate 1 is not being widely13used and we decided to obsolete this branch of code. The code still exists in14[substrate1](https://github.com/usetech-llc/nft_parachain/tree/substrate1) branch, but this delivery document is based15on substrate 2 version.121613The node can be run using docker container:17The node can be run using docker container:1814```19```15docker-compose up -d20docker-compose up -d16```21```172218#### NFT Tracking Module23#### NFT Tracking Module19Open extrinsics tab of the [standard UI](https://polkadot.js.org/apps/#/extrinsics) and find "nft" in the module list. This proves that NFT module is deployed.202421#### Balances Module, Other modules included by default 25Open extrinsics tab of the [standard UI](https://polkadot.js.org/apps/#/extrinsics) and find "nft" in the module list.22The [same UI](https://polkadot.js.org/apps/#/extrinsics) allows verification that all other modules are also installed as needed.26This proves that NFT module is deployed.232728#### Balances Module, Other modules included by default2930The [same UI](https://polkadot.js.org/apps/#/extrinsics) allows verification that all other modules are also installed31as needed.3224#### Configuration of runtime as needed (e.g. for hot module updates)33#### Configuration of runtime as needed (e.g. for hot module updates)253426The `set_code` method worked out of the box and we did not need to perform any additional customizations to upload an updated WASM version of NFT palette.35The `set_code` method worked out of the box and we did not need to perform any additional customizations to upload an36updated WASM version of NFT palette.273728### NFT Tracking Module38### NFT Tracking Module293930Before we continue, we need to do some preparations in the UI: Add NFT Data types so that the UI knows how to decode them. Go to [Settings-Developer Tab](https://polkadot.js.org/apps/#/settings/developer) and add following types to the JSON object in there:40Before we continue, we need to do some preparations in the UI: Add NFT Data types so that the UI knows how to decode41them. Go to [Settings-Developer Tab](https://polkadot.js.org/apps/#/settings/developer) and add following types to the42JSON object in there:4331```44```32{45{506351#### CreateCollection64#### CreateCollection526553Before running test, open [chain state tab](https://polkadot.js.org/apps/#/chainstate) and read `nft`.`nextCollectionId` state variable, which shows how many collections were created so far. If you just started the chain, this should equal 0. 66Before running test, open [chain state tab](https://polkadot.js.org/apps/#/chainstate) and read `nft`.`nextCollectionId`67state variable, which shows how many collections were created so far. If you just started the chain, this should68equal 0.546955Open extrinsics tab of the [standard UI](https://polkadot.js.org/apps/#/extrinsics) and select ALICE account. Select `nft` module and `createCollection` method. Put 1 in `custom_data_size` and run the transaction. After it is finalized, read the `nft`.`nextCollectionId` state variable. It will be equal 1.70Open extrinsics tab of the [standard UI](https://polkadot.js.org/apps/#/extrinsics) and select ALICE account. Select71`nft` module and `createCollection` method. Put 1 in `custom_data_size` and run the transaction. After it is finalized,72read the `nft`.`nextCollectionId` state variable. It will be equal 1.567357Also, read the state variable `nft`.`collection` with ID = 1 (Because everything in NFT Palette is numbered from 1, not from 0). You will see something like this:74Also, read the state variable `nft`.`collection` with ID = 1 (Because everything in NFT Palette is numbered from 1, not75from 0). You will see something like this:587659```77```60nft.collection: CollectionType78nft.collection: CollectionType678568### ChangeCollectionOwner86### ChangeCollectionOwner698770Open extrinsics tab of the [standard UI](https://polkadot.js.org/apps/#/extrinsics). Select `nft` module and `changeCollectionOwner` method. First, try to execute it to change owner to BOB for collection 1 from some different account than ALICE - FERDIE to see that it is not possible because ALICE owns collection 1 and FERDIE is not allowed to give it to BOB. Second, select ALICE as transaction signer and run it again. This time the collection owner changes, which will be reflected if you read the state variable `nft`.`collection` with ID = 1:88Open extrinsics tab of the [standard UI](https://polkadot.js.org/apps/#/extrinsics). Select `nft` module and89`changeCollectionOwner` method. First, try to execute it to change owner to BOB for collection 1 from some different90account than ALICE - FERDIE to see that it is not possible because ALICE owns collection 1 and FERDIE is not allowed to91give it to BOB. Second, select ALICE as transaction signer and run it again. This time the collection owner changes,92which will be reflected if you read the state variable `nft`.`collection` with ID = 1:719372```94```73nft.collection: CollectionType95nft.collection: CollectionType8410685**Note: Before destroying collection, you can see Milestone 2 deliverables to avoid creating collection again**107**Note: Before destroying collection, you can see Milestone 2 deliverables to avoid creating collection again**8610887Run `nft`.`destroyCollection` with collection ID = 1, and then read collection from state. The returned fields will have default values, which indicates that collection does not exsit anymore: 109Run `nft`.`destroyCollection` with collection ID = 1, and then read collection from state. The returned fields will have110default values, which indicates that collection does not exsit anymore:11188```112```89nft.collection: CollectionType113nft.collection: CollectionType108132109**using Polkadot UI from https://substrate-ui.parity.io/**133**using Polkadot UI from https://substrate-ui.parity.io/**110134111This document, except we are using Substrate v2, so we can use the newer version of the UI: https://polkadot.js.org/apps/#135This document, except we are using Substrate v2, so we can use the newer version of the UI:136https://polkadot.js.org/apps/#112137113## Milestone 2138## Milestone 2139114### NFT Tracking Module140### NFT Tracking Module141115**Note:** If you have destroyed collection, create it again using `nft`.`createCollection` method.142**Note:** If you have destroyed collection, create it again using `nft`.`createCollection` method.116143117**Note 2:** The order of items in this section is different from the original spec to make the acceptance workflow more natural.144**Note 2:** The order of items in this section is different from the original spec to make the acceptance workflow more145natural.118146119If you did not destroy collection while looking at Milestone 1 deliverables, use collection ID 1 in the further examples, otherwise use collection ID = 2.147If you did not destroy collection while looking at Milestone 1 deliverables, use collection ID 1 in the further148examples, otherwise use collection ID = 2.120149121#### CreateItem150#### CreateItem122Before creating an NFT item, let's read ALICE balance for your collection, which indicates how many NFT tokens ALICE owns in this collection. Read the chain state `nft`.`balance(<Collection ID>, ALICE)`, and it will be 0.123151124Execute extrinsic `nft`.`createItem` from ALICE account. Set properties to `0x01`. Now if you read the chain state `nft`.`balance(<Collection ID>, ALICE)`, it will be equal to 1. Also, you can read chain state `nft`.`itemList(<Collection ID>, 1)`, and it will return data for the token 1:152Before creating an NFT item, let's read ALICE balance for your collection, which indicates how many NFT tokens ALICE153owns in this collection. Read the chain state `nft`.`balance(<Collection ID>, ALICE)`, and it will be 0.125154155Execute extrinsic `nft`.`createItem` from ALICE account. Set properties to `0x01`. Now if you read the chain state156`nft`.`balance(<Collection ID>, ALICE)`, it will be equal to 1. Also, you can read chain state157`nft`.`itemList(<Collection ID>, 1)`, and it will return data for the token 1:158126```159```127nft.itemList: NftItemType160nft.itemList: NftItemType128{161{133```166```134167135#### GetOwner168#### GetOwner169136Reading the ownership is done by reading chainstate `nft`.`itemList(<Collection ID>, 1)`. One of the returned fields is Owner:170Reading the ownership is done by reading chainstate `nft`.`itemList(<Collection ID>, 1)`. One of the returned fields is171Owner:172137```173```138nft.itemList: NftItemType174nft.itemList: NftItemType144```180```145181146#### Transfer182#### Transfer183147Execute `nft`.`transfer` from ALICE address to transfer token 1 to BOB and check the ownership again:184Execute `nft`.`transfer` from ALICE address to transfer token 1 to BOB and check the ownership again:185148```186```149nft.itemList: NftItemType187nft.itemList: NftItemType150{188{159#### BalanceOf197#### BalanceOf160198161Read the chain state `nft`.`balance` for ALICE address and see that she owns 1 token:199Read the chain state `nft`.`balance` for ALICE address and see that she owns 1 token:200162```201```163nft.balance: u64202nft.balance: u6416412031165```204```166205167#### AddCollectionAdmin206#### AddCollectionAdmin168Execute `nft`.`AddCollectionAdmin` from ALICE account and let CHARLIE be an admin. Now you can see that CHARLIE can transfer ALICE's token from ALICE's account to EVE and back. Also, you can read admin list from chain state and see that it is not empty:169207208Execute `nft`.`AddCollectionAdmin` from ALICE account and let CHARLIE be an admin. Now you can see that CHARLIE can209transfer ALICE's token from ALICE's account to EVE and back. Also, you can read admin list from chain state and see that210it is not empty:211170```212```171nft.adminList: Vec<AccountId>213nft.adminList: Vec<AccountId>172[214[175```217```176218177#### RemoveCollectionAdmin219#### RemoveCollectionAdmin178Execute `nft`.`RemoveCollectionAdmin` from ALICE account to remove CHARLIE from admins. Now you can see that CHARLIE cannot transfer ALICE's tokens anymore. If you read the chan state `nft`.`adminList`, the response will be empty:179220221Execute `nft`.`RemoveCollectionAdmin` from ALICE account to remove CHARLIE from admins. Now you can see that CHARLIE222cannot transfer ALICE's tokens anymore. If you read the chan state `nft`.`adminList`, the response will be empty:223180```224```181nft.adminList: Vec<AccountId>225nft.adminList: Vec<AccountId>182[]226[]183```227```184228185#### BurnItem229#### BurnItem230186Execute `nft`.`burnItem` from ALICE account to burn token 1, and then read the chain state `nft`.`itemList(<Collection ID>, 1)`. This time the chain state returns default values in fields because token does not exist anymore. You can also check ALICE balance in chain state, now it is equal 0 again.231Execute `nft`.`burnItem` from ALICE account to burn token 1, and then read the chain state232`nft`.`itemList(<Collection ID>, 1)`. This time the chain state returns default values in fields because token does not233exist anymore. You can also check ALICE balance in chain state, now it is equal 0 again.234187```235```188nft.itemList: NftItemType236nft.itemList: NftItemType194```242```195243196### Economic Model Specification244### Economic Model Specification245197[Economic Model Specification](economic_model.md)246[Economic Model Specification](economic_model.md)198247199### Contracts Module Specification248### Contracts Module Specification249200https://docs.google.com/document/d/1gDtYjPR9C1VZChxEA-xAdQWQyEvMg245XrZR_MpE3cg/edit?usp=sharing250https://docs.google.com/document/d/1gDtYjPR9C1VZChxEA-xAdQWQyEvMg245XrZR_MpE3cg/edit?usp=sharing201251202### Bonus goal252### Bonus goal203253204**Basic demo - Cryptopunks representation on the Substrate Chain.**254**Basic demo - Cryptopunks representation on the Substrate Chain.**205255206See this repo, it has running and playing instructions:256See this repo, it has running and playing instructions: https://github.com/usetech-llc/substrapunks207https://github.com/usetech-llc/substrapunks208257doc/economic_model.mddiffbeforeafterboth223## Economy Goals3## Economy Goals445One of the goals of this specification is to find means to provide for network infrastructure (incentivisation of validators) and development support for NFT Parachain. Transaction fees will be used as the main source of funds. Fees will be distributed among network participants. The distribution of fees will depend on validator contribution to the network (similarly to Polkadot network) and the chosen fee structure (model).5One of the goals of this specification is to find means to provide for network infrastructure (incentivisation of6validators) and development support for NFT Parachain. Transaction fees will be used as the main source of funds. Fees7will be distributed among network participants. The distribution of fees will depend on validator contribution to the8network (similarly to Polkadot network) and the chosen fee structure (model).697## Summary10## Summary8119We are going to offer several fee models for our users. Some models will be implemented immediately, some will wait until there is a demand. 12We are going to offer several fee models for our users. Some models will be implemented immediately, some will wait13until there is a demand.101411The fee models can be setup by Collection and by Smart Contract address. Every Collection or Smart Contract has a fee model associated with it, which determines how transactions are paid. Network users can choose the fee model that better suits their application. The fee model can be changed later as well.15The fee models can be setup by Collection and by Smart Contract address. Every Collection or Smart Contract has a fee16model associated with it, which determines how transactions are paid. Network users can choose the fee model that better17suits their application. The fee model can be changed later as well.121813Initially we offer 4 models for consideration:19Initially we offer 4 models for consideration:142015* User paid fees (conventional blockchain method)21- User paid fees (conventional blockchain method)16* Sponsored (same as above, but paid by sponsor)22- Sponsored (same as above, but paid by sponsor)17* Prepaid plan23- Prepaid plan18* Resource purchase24- Resource purchase192520## Token and Accounts26## Token and Accounts212722Network Tokens are tracked via standard Balances module, which is included in NFT Parachain.28Network Tokens are tracked via standard Balances module, which is included in NFT Parachain.232924User Account matches to user address in the network. Among other properties user Account has Balance, which will be used later on.30User Account matches to user address in the network. Among other properties user Account has Balance, which will be used31later on.253226## Settings Economic Model33## Settings Economic Model2734313832#### Description39#### Description334034Set the collection sponsor address. The sponsorship needs to be confirmed by sending a ConfirmSponsorship transaction from that address. Also, the protection measures need to be in place so that sponsor account cannot be depleted by malicious users. White listing is one of the measured. It can be enabled with SetPublicAccessMode method.41Set the collection sponsor address. The sponsorship needs to be confirmed by sending a ConfirmSponsorship transaction42from that address. Also, the protection measures need to be in place so that sponsor account cannot be depleted by43malicious users. White listing is one of the measured. It can be enabled with SetPublicAccessMode method.354436##### List of transactions45##### List of transactions4637Transactions that can be sponsored:47Transactions that can be sponsored:38* Transfer39* Approve40* TransferFrom41* TransferWithData42* TransferFromWithData43* BurnItem444849- Transfer50- Approve51- TransferFrom52- TransferWithData53- TransferFromWithData54- BurnItem5545##### Rate Limiting56##### Rate Limiting465747This is a design proposal, actual implementation may change.58This is a design proposal, actual implementation may change.485949Features:60Features:50* Token transactions per day: The timestamp of last transaction performed on a token (NFT only) is associated with token ID in a dictionary. When a next transaction is run, the time is checked against this timestamp and, if less than allowed by rate limit, the fee is paid by caller account instead of the sponsor.51* Address transactions per day: The timestamp of last transaction performed by an address (only if transaction is in the [list of affected transactions](#list-of-transactions)) is associated with caller address in a dictionary. Further, the logic is similar to limiting by token as above. If only one of the limits (by token or by address) indicates that fee should be paid by the user, the fee is paid by the user.52* A pallet method (SetCollectionRateLimits) will be added to set these parameters and enable rate limiting.53* One idea to consider is deposits that must be made by an address in order to become white listed (instead of admin review).546162- Token transactions per day: The timestamp of last transaction performed on a token (NFT only) is associated with63 token ID in a dictionary. When a next transaction is run, the time is checked against this timestamp and, if less64 than allowed by rate limit, the fee is paid by caller account instead of the sponsor.65- Address transactions per day: The timestamp of last transaction performed by an address (only if transaction is in66 the [list of affected transactions](#list-of-transactions)) is associated with caller address in a dictionary.67 Further, the logic is similar to limiting by token as above. If only one of the limits (by token or by address)68 indicates that fee should be paid by the user, the fee is paid by the user.69- A pallet method (SetCollectionRateLimits) will be added to set these parameters and enable rate limiting.70- One idea to consider is deposits that must be made by an address in order to become white listed (instead of admin71 review).7255#### Permissions73#### Permissions567457* Collection Owner75- Collection Owner587659#### Parameters77#### Parameters607861* CollectionId: ID of the collection79- CollectionId: ID of the collection62* Sponsor: Sponsor address80- Sponsor: Sponsor address63* Fee Model ID: ID of selected economic model. Currently, only "Sponsored" mode is supported, which will be ID = 1.81- Fee Model ID: ID of selected economic model. Currently, only "Sponsored" mode is supported, which will be ID = 1.648265### ConfirmSponsorship83### ConfirmSponsorship8466#### Description85#### Description8667Confirm sponsorship of a collection. This call is needed to protect sponsor address from malicious collection creators.87Confirm sponsorship of a collection. This call is needed to protect sponsor address from malicious collection creators.688869#### Permissions89#### Permissions70* Collection Sponsor only719091- Collection Sponsor only9272#### Parameters93#### Parameters73* CollectionId: ID of the collection749495- CollectionId: ID of the collection9675### RemoveCollectionSponsor97### RemoveCollectionSponsor9876#### Description99#### Description10077Switch back to pay-per-own-transaction model.101Switch back to pay-per-own-transaction model.7810279#### Permissions103#### Permissions80* Collection owner81104105- Collection owner10682#### Parameters107#### Parameters83* collectionId: ID of the collection84108109- collectionId: ID of the collection11085### ClaimContract111### ClaimContract11286#### Description113#### Description87Claim the contract ownership for the purpose of sponsoring. Once the contract is claimed, only the contract owner can manage the contract sponsorship. Also, the contract may be claimed only once.88114115Claim the contract ownership for the purpose of sponsoring. Once the contract is claimed, only the contract owner can116manage the contract sponsorship. Also, the contract may be claimed only once.11789#### Permissions118#### Permissions90* Anyone91119120- Anyone12192#### Parameters122#### Parameters93* ContractAddress: Address of the contract94* Sponsor: Sponsor address95123124- ContractAddress: Address of the contract125- Sponsor: Sponsor address12696### SetContractSponsor127### SetContractSponsor12897#### Description129#### Description98Set the contract sponsor address. The sponsorship needs to be confirmed by sending a ConfirmContractSponsorship transaction from that address. The spam protection measures are similar to collection sponsorship.99130131Set the contract sponsor address. The sponsorship needs to be confirmed by sending a ConfirmContractSponsorship132transaction from that address. The spam protection measures are similar to collection sponsorship.133100#### Permissions134#### Permissions101* Contract Owner102135136- Contract Owner137103#### Parameters138#### Parameters104* ContractAddress: Address of the contract105* Sponsor: Sponsor address106* Fee Model ID: ID of selected economic model. Currently, only "Sponsored" mode is supported, which will be ID = 1.107139140- ContractAddress: Address of the contract141- Sponsor: Sponsor address142- Fee Model ID: ID of selected economic model. Currently, only "Sponsored" mode is supported, which will be ID = 1.143108### ConfirmContractSponsorship144### ConfirmContractSponsorship145109#### Description146#### Description147110Confirm sponsorship of a contract. This call is needed to protect sponsor address from malicious sponsorship requests.148Confirm sponsorship of a contract. This call is needed to protect sponsor address from malicious sponsorship requests.111149112#### Permissions150#### Permissions113* Contract Sponsor only114151152- Contract Sponsor only153115#### Parameters154#### Parameters116* ContractAddress: Address of the contract117155156- ContractAddress: Address of the contract157118### RemoveContractSponsor158### RemoveContractSponsor159119#### Description160#### Description161120Switch back to pay-per-own-transaction model.162Switch back to pay-per-own-transaction model.121163122#### Permissions164#### Permissions123* Contract owner124165166- Contract owner167125#### Parameters168#### Parameters126* contractAddress: Address of the contract127169170- contractAddress: Address of the contract171128### SetPublicAccessMode172### SetPublicAccessMode173129#### Description174#### Description130Toggle between normal and white list access for the methods with access for “Anyone”. If White List mode is enabled, AddToWhiteList and RemoveFromWhiteList methods can be called to add to and remove addresses from the white list.131175132White list mode is the property of collection. If it is turned on, all public operations such as token transfers, for example, which normally have “Anyone” permission, become white listed, i.e. are only available to collection owner, admins, and addresses from the white list. White lists can be helpful for rate limiting of transfers when collection sponsoring is enabled.176Toggle between normal and white list access for the methods with access for “Anyone”. If White List mode is enabled,177AddToWhiteList and RemoveFromWhiteList methods can be called to add to and remove addresses from the white list.133178179White list mode is the property of collection. If it is turned on, all public operations such as token transfers, for180example, which normally have “Anyone” permission, become white listed, i.e. are only available to collection owner,181admins, and addresses from the white list. White lists can be helpful for rate limiting of transfers when collection182sponsoring is enabled.183134#### Permissions184#### Permissions135* Collection Owner136185186- Collection Owner187137#### Parameters188#### Parameters138* CollectionID: ID of the Collection to set access mode for139* Mode140 * 0 = Normal141 * 1 = White list142189190- CollectionID: ID of the Collection to set access mode for191- Mode192 - 0 = Normal193 - 1 = White list194143### AddToWhiteList195### AddToWhiteList196144#### Description197#### Description198145Add an address to white list.199Add an address to white list.146200147#### Permissions201#### Permissions148* Collection Owner149* Collection Admin150202203- Collection Owner204- Collection Admin205151#### Parameters206#### Parameters152* CollectionID: ID of the Collection153* Address154207208- CollectionID: ID of the Collection209- Address210155### RemoveFromWhiteList211### RemoveFromWhiteList212156#### Description213#### Description214157Remove an address from white list.215Remove an address from white list.158216159#### Permissions217#### Permissions160* Collection Owner161* Collection Admin162218219- Collection Owner220- Collection Admin221163#### Parameters222#### Parameters164* CollectionID: ID of the Collection165* Address166223224- CollectionID: ID of the Collection225- Address226167### SetCollectionRateLimits227### SetCollectionRateLimits228168#### Description229#### Description230169Enable or disable rate limits for a collection231Enable or disable rate limits for a collection170232171#### Permissions233#### Permissions172* Collection Owner173* Collection Admin174234235- Collection Owner236- Collection Admin237175#### Parameters238#### Parameters176* CollectionID: ID of the Collection177* TokenInterval: Number of seconds allowed between sponsored token trasnsactions. 0 means there is no limit.178* AddressInterval: Number of seconds allowed between sponsored address trasnsactions. 0 means there is no limit.179239240- CollectionID: ID of the Collection241- TokenInterval: Number of seconds allowed between sponsored token trasnsactions. 0 means there is no limit.242- AddressInterval: Number of seconds allowed between sponsored address trasnsactions. 0 means there is no limit.180243181## User Paid Fees244## User Paid Fees182245183This is conventional fee model, when every Account pais for the transactions they sign and send. Transaction fee will gradually increase if the network load is higher to prevent denial of service. The same type of transaction (with the same transaction weight) will result in higher fee if previous block's weight gets close to maximum block weight. The ratio for multiplying fees will be determined empirically. The multiplication will take place until the blocks stop overpopulating, but with certain saturation. If blocks underfill, i.e. block weight is under the certain threshold, then the next block will have lower fees. The lowering will continue until blocks stop underpopulating, with some saturation.246This is conventional fee model, when every Account pais for the transactions they sign and send. Transaction fee will247gradually increase if the network load is higher to prevent denial of service. The same type of transaction (with the248same transaction weight) will result in higher fee if previous block's weight gets close to maximum block weight. The249ratio for multiplying fees will be determined empirically. The multiplication will take place until the blocks stop250overpopulating, but with certain saturation. If blocks underfill, i.e. block weight is under the certain threshold, then251the next block will have lower fees. The lowering will continue until blocks stop underpopulating, with some saturation.184252185Each module call will have weight annotation that will determine the weight calculation for this call:253Each module call will have weight annotation that will determine the weight calculation for this call:186254187* CreateCollection: Fixed255- CreateCollection: Fixed188* ChangeCollectionOwner: Fixed256- ChangeCollectionOwner: Fixed189* DestroyCollection: Linear of number of items (or owner addresses, depending on the collection type) multiplied by Collection Data Size257- DestroyCollection: Linear of number of items (or owner addresses, depending on the collection type) multiplied by258 Collection Data Size190* CreateItem: Linear of Collection Data Size259- CreateItem: Linear of Collection Data Size191* BurnItem: Linear of Collection Data Size260- BurnItem: Linear of Collection Data Size192* AddCollectionAdmin: Fixed261- AddCollectionAdmin: Fixed193* RemoveCollectionAdmin: Fixed262- RemoveCollectionAdmin: Fixed194* Transfers and approvals: Fixed263- Transfers and approvals: Fixed195* Transfers with data: Fixed + linear of data size264- Transfers with data: Fixed + linear of data size196* All other: Fixed265- All other: Fixed197266198This model is default and will be set for each collection until collection owner decides to change it to some other model.267This model is default and will be set for each collection until collection owner decides to change it to some other268model.199269200### Fee Distribution270### Fee Distribution201271202The fees will be burned. Incentivization is done through network token inflation, which will be designed in tokenomics.272The fees will be burned. Incentivization is done through network token inflation, which will be designed in tokenomics.203273204## Sponsored274## Sponsored205275206The only difference from User Paid Fees model is that collection owner will be paying for their users. The collection owner must have enough balance on his account in order to pay for user transactions.276The only difference from User Paid Fees model is that collection owner will be paying for their users. The collection277owner must have enough balance on his account in order to pay for user transactions.207278208### Fee Distribution279### Fee Distribution209280210Same as in User Paid Fees.281Same as in User Paid Fees.211282212213## Prepaid Plan283## Prepaid Plan214284215Collection Owner makes regular payments to prepay for some planned network load, i.e. some fixed number of transactions, created items, etc.285Collection Owner makes regular payments to prepay for some planned network load, i.e. some fixed number of transactions,286created items, etc.216287217218### Fee Distribution288### Fee Distribution219289220Same as in User Paid Fees.290Same as in User Paid Fees.221291222223## Resource Purchase292## Resource Purchase224293225Collection Owner purchases some fixed amount of network resource, which can be measured in number of users and their monthly transactions, for example. This is a one time payment. If Collection Owner decides to switch to a different fee model, the resources may be sold back to the system (with some commission).294Collection Owner purchases some fixed amount of network resource, which can be measured in number of users and their295monthly transactions, for example. This is a one time payment. If Collection Owner decides to switch to a different fee296model, the resources may be sold back to the system (with some commission).226297227Received funds will be converted to DOTs and used in staking (nomination or validation), which will allow to receive the income that will pay for infrastructure and development support.298Received funds will be converted to DOTs and used in staking (nomination or validation), which will allow to receive the299income that will pay for infrastructure and development support.228300229### Fee Distribution301### Fee Distribution230231Fixed percentage of staking income is distributed between validators proportionally to their contribution to the network. The rest is credited to the network owner.232302303Fixed percentage of staking income is distributed between validators proportionally to their contribution to the304network. The rest is credited to the network owner.233305doc/hackusama_walk_through.mddiffbeforeafterboth889### Deployed NFT TestNet9### Deployed NFT TestNet101011The TestNet public node is avaiable at wss://unique.usetech.com. It is easy to verify that it functions with the standard AppsUI:11The TestNet public node is avaiable at wss://unique.usetech.com. It is easy to verify that it functions with the12standard AppsUI:1213131. Open the [Apps UI](https://polkadot.js.org/apps/#)141. Open the [Apps UI](https://polkadot.js.org/apps/#)142. Click on the network icon in the top left corner152. Click on the network icon in the top left corner153. Scroll down the list and input `wss://unique.usetech.com` under "custom endpoint"163. Scroll down the list and input `wss://unique.usetech.com` under "custom endpoint"164. After connection is established, copy these [UI Custom Types](https://github.com/usetech-llc/nft_parachain#ui-custom-types) and paste them in [Developer Settings](https://polkadot.js.org/apps/#/settings/developer). Hit "Save" button.174. After connection is established, copy these18 [UI Custom Types](https://github.com/usetech-llc/nft_parachain#ui-custom-types) and paste them in19 [Developer Settings](https://polkadot.js.org/apps/#/settings/developer). Hit "Save" button.175. Now the NFT node is connected with AppsUI. 205. Now the NFT node is connected with AppsUI.186. Go to [Chain State](https://polkadot.js.org/apps/#/chainstate) and verify that NFT pallet is visible and you can read information from it.216. Go to [Chain State](https://polkadot.js.org/apps/#/chainstate) and verify that NFT pallet is visible and you can read22 information from it.197. For example, select nft -> collection, and enter collection ID 4 (for SubstraPunks) and click "+" button. You will see the collection properties such as UTF-16 encoded collection name and description, UTF-8 encoded token prefix, the size of custom data, etc.237. For example, select nft -> collection, and enter collection ID 4 (for SubstraPunks) and click "+" button. You will24 see the collection properties such as UTF-16 encoded collection name and description, UTF-8 encoded token prefix, the25 size of custom data, etc.202621#### Getting some Unique Tokens27#### Getting some Unique Tokens2228231. Install Polkadot{.js} extension if you don't have it already installed. Here are the links for [Chrome](https://chrome.google.com/webstore/detail/polkadot%7Bjs%7D-extension/mopnmbcafieddcagagdcbnhejhlodfdd) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/polkadot-js-extension/) extensions.291. Install Polkadot{.js} extension if you don't have it already installed. Here are the links for30 [Chrome](https://chrome.google.com/webstore/detail/polkadot%7Bjs%7D-extension/mopnmbcafieddcagagdcbnhejhlodfdd) and31 [Firefox](https://addons.mozilla.org/en-US/firefox/addon/polkadot-js-extension/) extensions.242. Create an address322. Create an address253. Contact us at [Unique Network Telegram channel](https://t.me/joinchat/DPVt1RwN50Uic_Q9lFcg9A) in order to get some Unique token and some Re-Fungible tokens. Faucet is comming soon, we will update this instructions when it goes live.333. Contact us at [Unique Network Telegram channel](https://t.me/joinchat/DPVt1RwN50Uic_Q9lFcg9A) in order to get some34 Unique token and some Re-Fungible tokens. Faucet is comming soon, we will update this instructions when it goes live.263527### Smart Contracts Pallet36### Smart Contracts Pallet2837291. Open [Contracts](https://polkadot.js.org/apps/#/contracts) tab. It only appears in the UI when Smart Contracts pallet is included in the runtime.381. Open [Contracts](https://polkadot.js.org/apps/#/contracts) tab. It only appears in the UI when Smart Contracts pallet39 is included in the runtime.302. Additionally, we can find an existing smart contract. Click on "Add an existing contract" link.402. Additionally, we can find an existing smart contract. Click on "Add an existing contract" link.313. Input this contract address: `5GdNqKMv4Sszq3SRd3TkXNa6a9ct4D3nXvtTWTFR7rTyccVJ`413. Input this contract address: `5GdNqKMv4Sszq3SRd3TkXNa6a9ct4D3nXvtTWTFR7rTyccVJ`324. Input any contract name, for example `Claim Substrapunks`424. Input any contract name, for example `Claim Substrapunks`335. Download and drag-and-drop this [metadata.json](https://github.com/usetech-llc/substrapunks/releases/download/v1.0.2/metadata.json) file into contract ABI field.435. Download and drag-and-drop this44 [metadata.json](https://github.com/usetech-llc/substrapunks/releases/download/v1.0.2/metadata.json) file into45 contract ABI field.346. Click Save. The contract appears in the page. If the contract did not exist at that address, the UI would display an error message: "Unable to find deployed contract code at the specified address".466. Click Save. The contract appears in the page. If the contract did not exist at that address, the UI would display an47 error message: "Unable to find deployed contract code at the specified address".354836### Integration Between Smart Contracts and NFT Pallet49### Integration Between Smart Contracts and NFT Pallet375038This was one of the most challenging parts of the Hackusama for us. The pre-RC4 versions of Substrate did not function properly when we tried to use `ext_dispatch_call` to dispatch a runtime call to NFT pallet from Contracts pallet, and RC4 had the `ext_dispatch_call` already removed to "free space" for some friendlier way of interaction between pallets, that was not yet implemented. Thanks to Alexander Theißen who reverted the removal of `ext_dispatch_call` and created a special branch based on RC4 for us!51This was one of the most challenging parts of the Hackusama for us. The pre-RC4 versions of Substrate did not function52properly when we tried to use `ext_dispatch_call` to dispatch a runtime call to NFT pallet from Contracts pallet, and53RC4 had the `ext_dispatch_call` already removed to "free space" for some friendlier way of interaction between pallets,54that was not yet implemented. Thanks to Alexander Theißen who reverted the removal of `ext_dispatch_call` and created a55special branch based on RC4 for us!395640The smart contract source code exists in this [repository folder](https://github.com/usetech-llc/nft_parachain/tree/master/smart_contract/ink-types-node-runtime), but the best way to test how the interaction between smart contracts and NFT pallet works is to see it in action using the [SubstraPunks Game Example](https://ipfs-gateway.usetech.com/ipns/QmaMtDqE9nhMX9RQLTpaCboqg7bqkb6Gi67iCKMe8NDpCE/) that uses smart contract to claim free characters. The complete demo of this game will come later, so please bare with us and let's put this item demonstration for a bit later.57The smart contract source code exists in this58[repository folder](https://github.com/usetech-llc/nft_parachain/tree/master/smart_contract/ink-types-node-runtime), but59the best way to test how the interaction between smart contracts and NFT pallet works is to see it in action using the60[SubstraPunks Game Example](https://ipfs-gateway.usetech.com/ipns/QmaMtDqE9nhMX9RQLTpaCboqg7bqkb6Gi67iCKMe8NDpCE/) that61uses smart contract to claim free characters. The complete demo of this game will come later, so please bare with us and62let's put this item demonstration for a bit later.416342### Re-Fungibility support64### Re-Fungibility support436544This feature is best demonstrated in action using our NFT wallet. First, you need to have some Unique and Re-Fungible tokens. [This section](#getting-some-unique-tokens) tells how to get them.66This feature is best demonstrated in action using our NFT wallet. First, you need to have some Unique and Re-Fungible67tokens. [This section](#getting-some-unique-tokens) tells how to get them.4568461. Open the [NFT Wallet](https://uniqueapps.usetech.com/#/nft)691. Open the [NFT Wallet](https://uniqueapps.usetech.com/#/nft)472. Search for collection called "Artwork". Search can be done either by collection name or collection ID (which is 2).702. Search for collection called "Artwork". Search can be done either by collection name or collection ID (which is 2).483. Click on "+ Add collection" in search results. The collection will appear under "My collections"713. Click on "+ Add collection" in search results. The collection will appear under "My collections"494. Expand the "Artwork" collection and see that you own a token with a partial balance724. Expand the "Artwork" collection and see that you own a token with a partial balance505. Transfer the token to some other address: Click "Transfer token" and enter address and the amount. Amount should be entered as decimal fraction. For example "0.01" to transfer 1/100th part of the token. 735. Transfer the token to some other address: Click "Transfer token" and enter address and the amount. Amount should be74 entered as decimal fraction. For example "0.01" to transfer 1/100th part of the token.516. Observe that your balance decreased by the amount you entered.756. Observe that your balance decreased by the amount you entered.527653#### The Hard Way77#### The Hard Way84 fraction: 1,000108 fraction: 1,00085 }109 }86 ],110 ],87 Data: 111 Data:88}112}89```113```9011491The Owner field contains a list of owners with the fractions they own. The fractions are represented as fixed point decimals. The number of decimal points in this fraction is determined by collection properties. For example, in the Artwork collection this property is equal to 4. The owned fraction is then determined as `fraction` divided by 10^DecimalPoints, i.e. 10,000 in this case.115The Owner field contains a list of owners with the fractions they own. The fractions are represented as fixed point116decimals. The number of decimal points in this fraction is determined by collection properties. For example, in the117Artwork collection this property is equal to 4. The owned fraction is then determined as `fraction` divided by11810^DecimalPoints, i.e. 10,000 in this case.92119935. Staying in Chain State, select "nft" - "colection"1205. Staying in Chain State, select "nft" - "colection"946. Input "2" and click "+"1216. Input "2" and click "+"957. Observe the following structure returned. It tells that collection is ReFungible and DecimalPoints field is equal to 4.1227. Observe the following structure returned. It tells that collection is ReFungible and DecimalPoints field is equal123 to 4.9612497```125```98{126{111139112### Off-Chain Schema to store token image URLs140### Off-Chain Schema to store token image URLs113141114This feature is best demonstrated in action using our NFT wallet. First, you need to have some Unique and Re-Fungible tokens. [This section](#getting-some-unique-tokens) tells how to get them.142This feature is best demonstrated in action using our NFT wallet. First, you need to have some Unique and Re-Fungible143tokens. [This section](#getting-some-unique-tokens) tells how to get them.1151441161. Open the [NFT Wallet](https://uniqueapps.usetech.com/#/nft)1451. Open the [NFT Wallet](https://uniqueapps.usetech.com/#/nft)1172. Search for collection called "Artwork". Search can be done either by collection name or collection ID (which is 2).1462. Search for collection called "Artwork". Search can be done either by collection name or collection ID (which is 2).1183. Expand "Artwork" collection1473. Expand "Artwork" collection1194. Click on the image to zoom in. 1484. Click on the image to zoom in.1205. Right-click on the image and select "Open Image in New Tab"1495. Right-click on the image and select "Open Image in New Tab"1216. Note the image URL:1506. Note the image URL:151122```152```123https://ipfs-gateway.usetech.com/ipfs/QmUSv64cUmL2m44QYkUFWmH89qykC8VLPFwjhpeAScjejS/image1.jpg153https://ipfs-gateway.usetech.com/ipfs/QmUSv64cUmL2m44QYkUFWmH89qykC8VLPFwjhpeAScjejS/image1.jpg124``` 154```1551257. Open [Chain State](https://uniqueapps.usetech.com/#/chainstate).1567. Open [Chain State](https://uniqueapps.usetech.com/#/chainstate).1268. Select "nft" - "colection"1578. Select "nft" - "colection"1279. Input "2" and click "+"1589. Input "2" and click "+"12810. Scroll down:15910. Scroll down:160129```161```130 ],162 ],131 TokenPrefix: ARTu0000,163 TokenPrefix: ARTu0000,135 UnconfirmedSponsor: 5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM167 UnconfirmedSponsor: 5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM136}168}137```169```13811. Observe the field called `OffchainSchema`. This field defines the URL schema that is used to contruct token URLs based on the token ID. In this particular example, the {id} template variable is replaced with token id 1, which results in the proper image URL.13917017111. Observe the field called `OffchainSchema`. This field defines the URL schema that is used to contruct token URLs172 based on the token ID. In this particular example, the {id} template variable is replaced with token id 1, which173 results in the proper image URL.174140### New economic models175### New economic models141176142This feature is best demonstrated when you own a SubstraPunk character. Please proceed with the demonstration, and then come back after you claim one.177This feature is best demonstrated when you own a SubstraPunk character. Please proceed with the demonstration, and then178come back after you claim one.1431791441. Create a new address in Polkadot{.js} extension. We need an address that has zero balance in Unique tokens. Let's call it "ZERO BALANCE" address.1801. Create a new address in Polkadot{.js} extension. We need an address that has zero balance in Unique tokens. Let's181 call it "ZERO BALANCE" address.1452. Open the [NFT Wallet](https://uniqueapps.usetech.com/#/nft)1822. Open the [NFT Wallet](https://uniqueapps.usetech.com/#/nft)1463. Search for collection called "SubstraPunks" (partial search also works, so you can just type "punk" and hit Enter)1833. Search for collection called "SubstraPunks" (partial search also works, so you can just type "punk" and hit Enter)1474. Add collection1844. Add collection1485. Expand substrapunks collection and transfer your PNK token to your "ZERO BALANCE" address.1855. Expand substrapunks collection and transfer your PNK token to your "ZERO BALANCE" address.1496. Now select your "ZERO BALANCE" address in the drop-down list and repeat searching and adding the collection for this address.1866. Now select your "ZERO BALANCE" address in the drop-down list and repeat searching and adding the collection for this187 address.1507. Transfer token back to your main address. Observe that despite the zero balance, the transfer is successful.1887. Transfer token back to your main address. Observe that despite the zero balance, the transfer is successful.151189152### White Lists and Public Mint Permission190### White Lists and Public Mint Permission153191154We did not complete these features in time by Hackusama deadline, but because they are important for security of the network, we completed them after the deadline anyway. They can be seen in branch [feature/white_list](https://github.com/usetech-llc/nft_parachain/tree/feature/white_list). Here are the permalinks to essential functions:192We did not complete these features in time by Hackusama deadline, but because they are important for security of the193network, we completed them after the deadline anyway. They can be seen in branch194[feature/white_list](https://github.com/usetech-llc/nft_parachain/tree/feature/white_list). Here are the permalinks to195essential functions:155196156[white_lists](https://github.com/usetech-llc/nft_parachain/blob/b7c59f0085ed2bc1922e937adf68ef4174a8ba36/pallets/nft/src/lib.rs#L659)197[white_lists](https://github.com/usetech-llc/nft_parachain/blob/b7c59f0085ed2bc1922e937adf68ef4174a8ba36/pallets/nft/src/lib.rs#L659)157198163204164All features of the NFT Wallet were already demonstrated previously:205All features of the NFT Wallet were already demonstrated previously:165206166* Enables adding favorite collections207- Enables adding favorite collections167* Shows tokens owned208- Shows tokens owned168* Allows NFT and Re-Fungible transfers209- Allows NFT and Re-Fungible transfers169* Shows Re-Fungible Balances210- Shows Re-Fungible Balances170* Shows Token Images211- Shows Token Images171212172## Our version of AppsUI213## Our version of AppsUI173214174The UI was also mainly demonstrated. We only need to show some configuration that happens behind the scenes:215The UI was also mainly demonstrated. We only need to show some configuration that happens behind the scenes:175216176### Loads appropriate custom API types217### Loads appropriate custom API types2181771. Open the [NFT Wallet Developer Settings](https://uniqueapps.usetech.com/#/settings/developer)2191. Open the [NFT Wallet Developer Settings](https://uniqueapps.usetech.com/#/settings/developer)1782. Observe that Custom UI types are already in place2202. Observe that Custom UI types are already in place179221191233192[Demonstration](https://github.com/usetech-llc/nft_unity/blob/master/src/DemoApplication/readme.md)234[Demonstration](https://github.com/usetech-llc/nft_unity/blob/master/src/DemoApplication/readme.md)193235194195## SubstraPunks Game236## SubstraPunks Game196237197[Project Description](https://github.com/usetech-llc/substrapunks/blob/master/README.md)238[Project Description](https://github.com/usetech-llc/substrapunks/blob/master/README.md)198239199First, you will need some Unique balance. [This section](#getting-some-unique-tokens) tells how to get the TestNet currency.240First, you will need some Unique balance. [This section](#getting-some-unique-tokens) tells how to get the TestNet241currency.2002422011. Open this [SubstraPunks Game](https://ipfs-gateway.usetech.com/ipns/QmaMtDqE9nhMX9RQLTpaCboqg7bqkb6Gi67iCKMe8NDpCE/) IPFS link2431. Open this [SubstraPunks Game](https://ipfs-gateway.usetech.com/ipns/QmaMtDqE9nhMX9RQLTpaCboqg7bqkb6Gi67iCKMe8NDpCE/)244 IPFS link2022. Find a character you like that still has red background. Red indicates that the character was not yet claimed by anyone.2452. Find a character you like that still has red background. Red indicates that the character was not yet claimed by246 anyone.2033. Allow access for Polkadot{.js} to this site2473. Allow access for Polkadot{.js} to this site2044. Click "Claim" button2484. Click "Claim" button2055. Select the account that has some Unique balance2495. Select the account that has some Unique balance2066. Click "Claim" again, sign transaction, and wait until it mines. 2506. Click "Claim" again, sign transaction, and wait until it mines.207251208Let's make a pause here: You were just demonstrated how the integration between smart contracts and NFT Pallet works. This is what we've been previously talking about [here](#integration-between-smart-contracts-and-nft-pallet)252Let's make a pause here: You were just demonstrated how the integration between smart contracts and NFT Pallet works.253This is what we've been previously talking about [here](#integration-between-smart-contracts-and-nft-pallet)209254210The source code of the generic smart contract is located in [smart_contract](https://github.com/usetech-llc/nft_parachain/tree/dev/smart_contract/ink-types-node-runtime) folder. But to demonstrate smart contracts in practice we created additional [Claim Contract](https://github.com/usetech-llc/substrapunks/tree/master/smart_contract) to implement claiming functionality in this game. 255The source code of the generic smart contract is located in256[smart_contract](https://github.com/usetech-llc/nft_parachain/tree/dev/smart_contract/ink-types-node-runtime) folder.257But to demonstrate smart contracts in practice we created additional258[Claim Contract](https://github.com/usetech-llc/substrapunks/tree/master/smart_contract) to implement claiming259functionality in this game.211260212Here is how Claiming works: Claim Contract owns all characters in the game initially. Player sends a transaction calling the method claim:261Here is how Claiming works: Claim Contract owns all characters in the game initially. Player sends a transaction calling262the method claim:263213```264```214fn claim(&mut self, collection_id: u64, item_id: u64, new_owner: AccountId)265fn claim(&mut self, collection_id: u64, item_id: u64, new_owner: AccountId)215```266```216267217Inside the method `claim` there is a call made to NFT pallet to transfer the token that is being claimed to the player:268Inside the method `claim` there is a call made to NFT pallet to transfer the token that is being claimed to the player:269218```270```219runtime_calls::transfer(collection_id, item_id, new_owner);271runtime_calls::transfer(collection_id, item_id, new_owner);220```272```2212732227. Follow the link to [NFT Wallet](https://uniqueapps.usetech.com/#/nft)2747. Follow the link to [NFT Wallet](https://uniqueapps.usetech.com/#/nft)2238. Search for "SubstraPunks" collection, add it2758. Search for "SubstraPunks" collection, add it2249. Expand the collection and find your character in there!2769. Expand the collection and find your character in there!22510. Now you can try transfers and test the [economic model](#new-economic-models), the last thing we put off until a SubstraPunks character is claimed.27710. Now you can try transfers and test the [economic model](#new-economic-models), the last thing we put off until a226278 SubstraPunks character is claimed.227279228node/Cargo.tomldiffbeforeafterboth12description = 'Substrate node nft'12description = 'Substrate node nft'13edition = '2018'13edition = '2018'14homepage = 'https://usetech.com/blockchain'14homepage = 'https://usetech.com/blockchain'15license = 'Unlicense'15license = 'All Rights Reserved'16name = 'nft'16name = 'nft'17repository = 'https://github.com/substrate-developer-hub/nft/'17repository = 'https://github.com/usetech-llc/nft_private/'18version = '3.0.0'18version = '3.0.0'191920[package.metadata.docs.rs]20[package.metadata.docs.rs]29jsonrpc-core = '15.0.0'29jsonrpc-core = '15.0.0'303031# Substrate dependencies31# Substrate dependencies32nft-runtime = { path = '../runtime', version = '3.0.0' }32nft-runtime = { path = '../runtime', version = '2.1.0' }33frame-benchmarking = {version = '2.0.1', git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique'}33frame-benchmarking = {version = '2.0.1', git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique'}34frame-benchmarking-cli = {version = '2.0.1', git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique'}34frame-benchmarking-cli = {version = '2.0.1', git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique'}35pallet-transaction-payment-rpc = {version = '2.0.1', git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique'}35pallet-transaction-payment-rpc = {version = '2.0.1', git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique'}node/src/command.rsdiffbeforeafterboth232324impl SubstrateCli for Cli {24impl SubstrateCli for Cli {25 fn impl_name() -> String {25 fn impl_name() -> String {26 "Substrate Node".into()26 "Unique Node".into()27 }27 }282829 fn impl_version() -> String {29 fn impl_version() -> String {pallets/nft/Cargo.tomldiffbeforeafterboth6license = 'Unlicense'6license = 'Unlicense'7name = 'pallet-nft'7name = 'pallet-nft'8repository = 'https://github.com/substrate-developer-hub/nft/'8repository = 'https://github.com/substrate-developer-hub/nft/'9version = '3.0.0'9version = '2.1.0'101011[package.metadata.docs.rs]11[package.metadata.docs.rs]12targets = ['x86_64-unknown-linux-gnu']12targets = ['x86_64-unknown-linux-gnu']pallets/nft/src/lib.rsdiffbeforeafterboth198 Self { 198 Self { 199 account_token_ownership_limit: 10_000_000, 199 account_token_ownership_limit: 10_000_000, 200 token_limit: u32::max_value(),200 token_limit: u32::max_value(),201 sponsored_data_size: u32::max_value(), 201 sponsored_data_size: u32::MAX, 202 sponsored_data_rate_limit: None,202 sponsored_data_rate_limit: None,203 sponsor_transfer_timeout: 14400,203 sponsor_transfer_timeout: 14400,204 owner_can_transfer: true,204 owner_can_transfer: true,388 AccountTokenLimitExceeded,388 AccountTokenLimitExceeded,389 /// Collection limit bounds per collection exceeded389 /// Collection limit bounds per collection exceeded390 CollectionLimitBoundsExceeded,390 CollectionLimitBoundsExceeded,391 /// Tried to enable permissions which are only permitted to be disabled392 OwnerPermissionsCantBeReverted,391 /// Schema data size limit bound exceeded393 /// Schema data size limit bound exceeded392 SchemaDataLimitExceeded,394 SchemaDataLimitExceeded,393 /// Maximum refungibility exceeded395 /// Maximum refungibility exceeded504 /// * collection_id: Id of the collection where item was created.506 /// * collection_id: Id of the collection where item was created.505 /// 507 /// 506 /// * item_id: Id of an item. Unique within the collection.508 /// * item_id: Id of an item. Unique within the collection.509 ///510 /// * recipient: Owner of newly created item 507 ItemCreated(CollectionId, TokenId),511 ItemCreated(CollectionId, TokenId, AccountId),508512509 /// Collection item was burned.513 /// Collection item was burned.510 /// 514 /// 515 /// item_id: Identifier of burned NFT.519 /// item_id: Identifier of burned NFT.516 ItemDestroyed(CollectionId, TokenId),520 ItemDestroyed(CollectionId, TokenId),521522 /// Item was transferred523 ///524 /// * collection_id: Id of collection to which item is belong525 ///526 /// * item_id: Id of an item527 ///528 /// * sender: Original owner of item529 ///530 /// * recipient: New owner of item531 ///532 /// * amount: Always 1 for NFT533 Transfer(CollectionId, TokenId, AccountId, AccountId, u128),517 }534 }518);535);519536568 _ => 0585 _ => 0569 };586 };587588 let chain_limit = ChainLimit::get();570589571 // bound Total number of collections590 // bound Total number of collections572 ensure!(CollectionCount::get() < ChainLimit::get().collection_numbers_limit, Error::<T>::TotalCollectionsLimitExceeded);591 ensure!(CollectionCount::get() < chain_limit.collection_numbers_limit, Error::<T>::TotalCollectionsLimitExceeded);573592574 // check params593 // check params575 ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);594 ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);590 CreatedCollectionCount::put(next_id);609 CreatedCollectionCount::put(next_id);591 CollectionCount::put(total);610 CollectionCount::put(total);611612 let limits = CollectionLimits {613 sponsored_data_size: chain_limit.custom_data_limit,614 ..Default::default()615 };592616593 // Create new collection617 // Create new collection594 let new_collection = CollectionType {618 let new_collection = CollectionType {606 sponsor_confirmed: false,630 sponsor_confirmed: false,607 variable_on_chain_schema: Vec::new(),631 variable_on_chain_schema: Vec::new(),608 const_on_chain_schema: Vec::new(),632 const_on_chain_schema: Vec::new(),609 limits: CollectionLimits::default()633 limits,610 };634 };611635612 // Add new collection to map636 // Add new collection to map633 let sender = ensure_signed(origin)?;657 let sender = ensure_signed(origin)?;634 Self::check_owner_permissions(collection_id, sender)?;658 Self::check_owner_permissions(collection_id, sender)?;659660 let target_collection = <Collection<T>>::get(collection_id);661 if !target_collection.limits.owner_can_destroy {662 fail!(Error::<T>::NoPermission);663 }635664636 <AddressTokens<T>>::remove_prefix(collection_id);665 <AddressTokens<T>>::remove_prefix(collection_id);637 <Allowances<T>>::remove_prefix(collection_id);666 <Allowances<T>>::remove_prefix(collection_id);1017 let target_collection = <Collection<T>>::get(collection_id).unwrap();1046 let target_collection = <Collection<T>>::get(collection_id).unwrap();1018 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||1047 ensure!(1048 Self::is_item_owner(sender.clone(), collection_id, item_id) ||1049 (1050 target_collection.limits.owner_can_transfer &&1019 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1051 Self::is_owner_or_admin_permissions(collection_id, sender.clone())1052 ),1020 Error::<T>::NoPermission);1053 Error::<T>::NoPermission1054 );10211055109111251092 // Transfer permissions check1126 // Transfer permissions check1093 let target_collection = <Collection<T>>::get(collection_id).unwrap();1127 let target_collection = <Collection<T>>::get(collection_id).unwrap();1128 let allowance_limit = if (1129 target_collection.limits.owner_can_transfer &&1094 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||1130 Self::is_owner_or_admin_permissions(1131 collection_id,1132 sender.clone(),1133 )1134 ) {1135 None1095 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1136 } else if let Some(amount) = Self::owned_amount(1137 sender.clone(),1138 collection_id,1139 item_id,1140 ) {1141 Some(amount)1142 } else {1096 Error::<T>::NoPermission);1143 fail!(Error::<T>::NoPermission);1144 };109711451098 if target_collection.access == AccessMode::WhiteList {1146 if target_collection.access == AccessMode::WhiteList {1099 Self::check_white_list(collection_id, &sender)?;1147 Self::check_white_list(collection_id, &sender)?;1105 if allowance_exists {1153 if allowance_exists {1106 allowance += <Allowances<T>>::get(collection_id, (item_id, &sender, &spender));1154 allowance += <Allowances<T>>::get(collection_id, (item_id, &sender, &spender));1107 }1155 }1156 if let Some(limit) = allowance_limit {1157 ensure!(limit >= allowance, Error::<T>::TokenValueTooLow);1158 }1108 <Allowances<T>>::insert(collection_id, (item_id, sender.clone(), spender.clone()), allowance);1159 <Allowances<T>>::insert(collection_id, (item_id, sender.clone(), spender.clone()), allowance);110911601110 Ok(())1161 Ok(())1151 // Transfer permissions check 1202 // Transfer permissions check 1152 ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1203 ensure!(1204 appoved_transfer || 1205 (1206 target_collection.limits.owner_can_transfer &&1207 Self::is_owner_or_admin_permissions(collection_id, sender.clone())1208 ),1153 Error::<T>::NoPermission);1209 Error::<T>::NoPermission1210 );115412111517 pub fn set_collection_limits(1574 pub fn set_collection_limits(1518 origin,1575 origin,1519 collection_id: u32,1576 collection_id: u32,1520 limits: CollectionLimits<T::BlockNumber>,1577 new_limits: CollectionLimits<T::BlockNumber>,1521 ) -> DispatchResult {1578 ) -> DispatchResult {1522 let sender = ensure_signed(origin)?;1579 let sender = ensure_signed(origin)?;1523 Self::check_owner_permissions(collection_id, sender.clone())?;1580 Self::check_owner_permissions(collection_id, sender.clone())?;1524 let mut target_collection = <Collection<T>>::get(collection_id).unwrap();1581 let mut target_collection = <Collection<T>>::get(collection_id).unwrap();1582 let old_limits = target_collection.limits;1525 let chain_limits = ChainLimit::get();1583 let chain_limits = ChainLimit::get();1526 let climits = target_collection.limits;152715841528 // collection bounds1585 // collection bounds1529 ensure!(limits.sponsor_transfer_timeout <= MAX_SPONSOR_TIMEOUT &&1586 ensure!(new_limits.sponsor_transfer_timeout <= MAX_SPONSOR_TIMEOUT &&1530 limits.account_token_ownership_limit <= MAX_TOKEN_OWNERSHIP, 1587 new_limits.account_token_ownership_limit <= MAX_TOKEN_OWNERSHIP && 1588 new_limits.sponsored_data_size <= chain_limits.custom_data_limit,1531 Error::<T>::CollectionLimitBoundsExceeded);1589 Error::<T>::CollectionLimitBoundsExceeded);153215901533 // token_limit check prev1591 // token_limit check prev1534 ensure!(climits.token_limit > limits.token_limit && 1592 ensure!(old_limits.token_limit >= new_limits.token_limit, Error::<T>::CollectionTokenLimitExceeded);1593 ensure!(new_limits.token_limit > 0, Error::<T>::CollectionTokenLimitExceeded);15941595 ensure!(1596 (old_limits.owner_can_transfer || !new_limits.owner_can_transfer) &&1535 limits.token_limit <= chain_limits.account_token_ownership_limit, 1597 (old_limits.owner_can_destroy || !new_limits.owner_can_destroy),1536 Error::<T>::AccountTokenLimitExceeded);1598 Error::<T>::OwnerPermissionsCantBeReverted,1599 );153716001538 target_collection.limits = limits;1601 target_collection.limits = new_limits;1539 <Collection<T>>::insert(collection_id, target_collection);1602 <Collection<T>>::insert(collection_id, target_collection);154016031541 Ok(())1604 Ok(())156416271565 match target_collection.mode1628 match target_collection.mode1566 {1629 {1567 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient)?,1630 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient.clone())?,1568 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &sender, &recipient)?,1631 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &sender, &recipient)?,1569 CollectionMode::ReFungible => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient)?,1632 CollectionMode::ReFungible => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient.clone())?,1570 _ => ()1633 _ => ()1571 };1634 };16351636 Self::deposit_event(RawEvent::Transfer(collection_id, item_id, sender, recipient, value));157216371573 Ok(())1638 Ok(())1574 }1639 }1643 {1708 {1644 CreateItemData::NFT(data) => {1709 CreateItemData::NFT(data) => {1645 let item = NftItemType {1710 let item = NftItemType {1646 owner,1711 owner: owner.clone(),1647 const_data: data.const_data,1712 const_data: data.const_data,1648 variable_data: data.variable_data1713 variable_data: data.variable_data1649 };1714 };1668 };1733 };166917341670 // call event1735 // call event1671 Self::deposit_event(RawEvent::ItemCreated(collection_id, <ItemListIndex>::get(collection_id)));1736 Self::deposit_event(RawEvent::ItemCreated(collection_id, <ItemListIndex>::get(collection_id), owner));167217371673 Ok(())1738 Ok(())1674 }1739 }1876 Ok(())1941 Ok(())1877 }1942 }19431944 fn owned_amount(1945 subject: T::AccountId,1946 collection_id: CollectionId,1947 item_id: TokenId,1948 ) -> Option<u128> {1949 let target_collection = <Collection<T>>::get(collection_id);19501951 match target_collection.mode {1952 CollectionMode::NFT => {1953 if <NftItemList<T>>::get(collection_id, item_id).owner == subject {1954 return Some(1)1955 }1956 None1957 },1958 CollectionMode::Fungible(_) => {1959 if <FungibleItemList<T>>::contains_key(collection_id, &subject) {1960 return Some(<FungibleItemList<T>>::get(collection_id, &subject)1961 .value);1962 }1963 None1964 },1965 CollectionMode::ReFungible => <ReFungibleItemList<T>>::get(collection_id, item_id)1966 .owner1967 .iter()1968 .find(|i| i.owner == subject)1969 .map(|i| i.fraction),1970 CollectionMode::Invalid => None,1971 }1972 }187819731879 fn is_item_owner(subject: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> bool {1974 fn is_item_owner(subject: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> bool {1880 let target_collection = <Collection<T>>::get(collection_id).unwrap();1975 let target_collection = <Collection<T>>::get(collection_id).unwrap();runtime/Cargo.tomldiffbeforeafterboth1[package]1[package]2authors = ['Substrate DevHub <https://github.com/substrate-developer-hub>']2authors = ['UseTech Professional <https://usetech.com/blockchain>']3edition = '2018'3edition = '2018'4homepage = 'https://substrate.io'4homepage = 'https://substrate.io'5license = 'Unlicense'5license = 'All Rights Reserved'6name = 'nft-runtime'6name = 'nft-runtime'7repository = 'https://github.com/usetech-llc/nft_parachain/'7repository = 'https://github.com/usetech-llc/nft_parachain/'8version = '3.0.0'8version = '2.1.0'9910[package.metadata.docs.rs]10[package.metadata.docs.rs]11targets = ['x86_64-unknown-linux-gnu']11targets = ['x86_64-unknown-linux-gnu']25serde = { features = ['derive'], optional = true, version = '1.0.101' }25serde = { features = ['derive'], optional = true, version = '1.0.101' }262627# local dependencies27# local dependencies28pallet-nft = { path = '../pallets/nft', default-features = false, version = '3.0.0' }28pallet-nft = { path = '../pallets/nft', default-features = false, version = '2.1.0' }292930# Substrate dependencies30# Substrate dependencies31frame-benchmarking = { default-features = false, optional = true, version = '2.0.1' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique' }31frame-benchmarking = { default-features = false, optional = true, version = '2.0.1' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique' }tests/package.jsondiffbeforeafterboth29 "testRemoveFromWhiteList": "mocha --timeout 9999999 -r ts-node/register ./**/removeFromWhiteList.test.ts",29 "testRemoveFromWhiteList": "mocha --timeout 9999999 -r ts-node/register ./**/removeFromWhiteList.test.ts",30 "testConnection": "mocha --timeout 9999999 -r ts-node/register ./**/connection.test.ts",30 "testConnection": "mocha --timeout 9999999 -r ts-node/register ./**/connection.test.ts",31 "testCollection": "mocha --timeout 9999999 -r ts-node/register ./**/createCollection.test.ts",31 "testCollection": "mocha --timeout 9999999 -r ts-node/register ./**/createCollection.test.ts",32 "testCreateItem": "mocha --timeout 9999999 -r ts-node/register ./**/createItem.test.ts",32 "testCreateMultipleItems": "mocha --timeout 9999999 -r ts-node/register ./**/createMultipleItems.test.ts",33 "testCreateMultipleItems": "mocha --timeout 9999999 -r ts-node/register ./**/createMultipleItems.test.ts",33 "testApprove": "mocha --timeout 9999999 -r ts-node/register ./**/approve.test.ts",34 "testApprove": "mocha --timeout 9999999 -r ts-node/register ./**/approve.test.ts",34 "testTransferFrom": "mocha --timeout 9999999 -r ts-node/register ./**/transferFrom.test.ts",35 "testTransferFrom": "mocha --timeout 9999999 -r ts-node/register ./**/transferFrom.test.ts",tests/src/approve.test.tsdiffbeforeafterboth2// This file is subject to the terms and conditions defined in2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.3// file 'LICENSE', which is part of this source code package.4//4//5import { ApiPromise } from '@polkadot/api';5import { IKeyringPair } from '@polkadot/types/types';6import BN from 'bn.js';6import { ApiPromise } from '@polkadot/api';7import chai from 'chai';7import chai from 'chai';8import chaiAsPromised from 'chai-as-promised';8import chaiAsPromised from 'chai-as-promised';9import privateKey from './substrate/privateKey';9import privateKey from './substrate/privateKey';12 approveExpectFail,12 approveExpectFail,13 approveExpectSuccess,13 approveExpectSuccess,14 createCollectionExpectSuccess,14 createCollectionExpectSuccess,15 createFungibleItemExpectSuccess,16 createItemExpectSuccess,15 createItemExpectSuccess,17 destroyCollectionExpectSuccess,16 destroyCollectionExpectSuccess,18 transferFromExpectSuccess,17 setCollectionLimitsExpectSuccess,19 U128_MAX,18 transferExpectSuccess,20} from './util/helpers';19} from './util/helpers';212022chai.use(chaiAsPromised);21chai.use(chaiAsPromised);23const expect = chai.expect;242225describe('Integration Test approve(spender, collection_id, item_id, amount):', () => {23describe('Integration Test approve(spender, collection_id, item_id, amount):', () => {24 let Alice: IKeyringPair;25 let Bob: IKeyringPair;26 let Charlie: IKeyringPair;2728 before(async () => {29 await usingApi(async () => {30 Alice = privateKey('//Alice');31 Bob = privateKey('//Bob');32 Charlie = privateKey('//Charlie');33 });34 });3526 it('Execute the extrinsic and check approvedList', async () => {36 it('Execute the extrinsic and check approvedList', async () => {27 await usingApi(async (api: ApiPromise) => {28 const Alice = privateKey('//Alice');29 const Bob = privateKey('//Bob');30 const nftCollectionId = await createCollectionExpectSuccess();37 const nftCollectionId = await createCollectionExpectSuccess();31 // nft38 // nft32 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');39 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');40 await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});47 await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});41 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');48 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');42 await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob);49 await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob);43 });44 });50 });455146 it('Remove approval by using 0 amount', async () => {52 it('Remove approval by using 0 amount', async () => {47 await usingApi(async (api: ApiPromise) => {48 const Alice = privateKey('//Alice');49 const Bob = privateKey('//Bob');50 const nftCollectionId = await createCollectionExpectSuccess();53 const nftCollectionId = await createCollectionExpectSuccess();51 // nft54 // nft52 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');55 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');63 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');66 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');64 await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 1);67 await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 1);65 await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 0);68 await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 0);66 });67 });69 });7071 it('can be called by collection owner on non-owned item when OwnerCanTransfer == true', async () => {72 const collectionId = await createCollectionExpectSuccess();73 const itemId = await createItemExpectSuccess(Alice, collectionId, 'NFT', Bob.address);7475 await approveExpectSuccess(collectionId, itemId, Alice, Charlie);76 });68});77});697870describe('Negative Integration Test approve(spender, collection_id, item_id, amount):', () => {79describe('Negative Integration Test approve(spender, collection_id, item_id, amount):', () => {80 let Alice: IKeyringPair;81 let Bob: IKeyringPair;82 let Charlie: IKeyringPair;8384 before(async () => {85 await usingApi(async (api) => {86 Alice = privateKey('//Alice');87 Bob = privateKey('//Bob');88 Charlie = privateKey('//Charlie');89 });90 });9171 it('Approve for a collection that does not exist', async () => {92 it('Approve for a collection that does not exist', async () => {72 await usingApi(async (api: ApiPromise) => {93 await usingApi(async (api: ApiPromise) => {73 const Alice = privateKey('//Alice');74 const Bob = privateKey('//Bob');75 // nft94 // nft76 const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number;95 const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number;77 await approveExpectFail(nftCollectionCount + 1, 1, Alice, Bob);96 await approveExpectFail(nftCollectionCount + 1, 1, Alice, Bob);85 });104 });8610587 it('Approve for a collection that was destroyed', async () => {106 it('Approve for a collection that was destroyed', async () => {88 await usingApi(async (api: ApiPromise) => {89 const Alice = privateKey('//Alice');90 const Bob = privateKey('//Bob');91 // nft107 // nft92 const nftCollectionId = await createCollectionExpectSuccess();108 const nftCollectionId = await createCollectionExpectSuccess();93 await destroyCollectionExpectSuccess(nftCollectionId);109 await destroyCollectionExpectSuccess(nftCollectionId);101 await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});117 await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});102 await destroyCollectionExpectSuccess(reFungibleCollectionId);118 await destroyCollectionExpectSuccess(reFungibleCollectionId);103 await approveExpectFail(reFungibleCollectionId, 1, Alice, Bob);119 await approveExpectFail(reFungibleCollectionId, 1, Alice, Bob);104 });105 });120 });106121107 it('Approve transfer of a token that does not exist', async () => {122 it('Approve transfer of a token that does not exist', async () => {108 await usingApi(async (api: ApiPromise) => {109 const Alice = privateKey('//Alice');110 const Bob = privateKey('//Bob');111 // nft123 // nft112 const nftCollectionId = await createCollectionExpectSuccess();124 const nftCollectionId = await createCollectionExpectSuccess();113 await approveExpectFail(nftCollectionId, 2, Alice, Bob);125 await approveExpectFail(nftCollectionId, 2, Alice, Bob);118 const reFungibleCollectionId =130 const reFungibleCollectionId =119 await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});131 await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});120 await approveExpectFail(reFungibleCollectionId, 2, Alice, Bob);132 await approveExpectFail(reFungibleCollectionId, 2, Alice, Bob);121 });122 });133 });123134124 it('Approve using the address that does not own the approved token', async () => {135 it('Approve using the address that does not own the approved token', async () => {125 await usingApi(async (api: ApiPromise) => {126 const Alice = privateKey('//Alice');127 const Bob = privateKey('//Bob');128 const nftCollectionId = await createCollectionExpectSuccess();136 const nftCollectionId = await createCollectionExpectSuccess();129 // nft137 // nft130 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');138 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');138 await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});146 await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});139 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');147 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');140 await approveExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice);148 await approveExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice);141 });142 });149 });150151 it('should fail if approved more NFTs than owned', async () => {152 const nftCollectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });153 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');154 await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1, 'NFT');155 await approveExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice);156 await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice);157 });158159 it('should fail if approved more ReFungibles than owned', async () => {160 const nftCollectionId = await createCollectionExpectSuccess({ mode: { type: 'ReFungible' } });161 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'ReFungible');162 await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 100, 'ReFungible');163 await approveExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice, 100);164 await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice, 1);165 });166167 it('should fail if approved more Fungibles than owned', async () => {168 const nftCollectionId = await createCollectionExpectSuccess({ mode: { type: 'Fungible', decimalPoints: 0 } });169 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'Fungible');170 await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 10, 'Fungible');171 await approveExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice, 10);172 await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice, 1);173 });174175 it('fails when called by collection owner on non-owned item when OwnerCanTransfer == false', async () => {176 const collectionId = await createCollectionExpectSuccess();177 const itemId = await createItemExpectSuccess(Alice, collectionId, 'NFT', Bob.address);178 await setCollectionLimitsExpectSuccess(Alice, collectionId, { OwnerCanTransfer: false });179180 await approveExpectFail(collectionId, itemId, Alice, Charlie);181 });143});182});144183tests/src/destroyCollection.test.tsdiffbeforeafterboth3// file 'LICENSE', which is part of this source code package.3// file 'LICENSE', which is part of this source code package.4//4//556import { IKeyringPair } from '@polkadot/types/types';6import chai from 'chai';7import chai from 'chai';7import chaiAsPromised from 'chai-as-promised';8import chaiAsPromised from 'chai-as-promised';9import privateKey from './substrate/privateKey';8import { default as usingApi } from "./substrate/substrate-api";10import { default as usingApi } from "./substrate/substrate-api";9import { createCollectionExpectSuccess, destroyCollectionExpectSuccess, destroyCollectionExpectFailure } from "./util/helpers";11import { createCollectionExpectSuccess, destroyCollectionExpectSuccess, destroyCollectionExpectFailure, setCollectionLimitsExpectSuccess } from "./util/helpers";101211chai.use(chaiAsPromised);13chai.use(chaiAsPromised);121426});28});272928describe('(!negative test!) integration test: ext. destroyCollection():', () => {30describe('(!negative test!) integration test: ext. destroyCollection():', () => {31 let alice: IKeyringPair;3233 before(async () => {34 await usingApi(async (api) => {35 alice = privateKey('//Alice');36 });37 });3829 it('(!negative test!) Destroy a collection that never existed', async () => {39 it('(!negative test!) Destroy a collection that never existed', async () => {30 await usingApi(async (api) => {40 await usingApi(async (api) => {43 await destroyCollectionExpectFailure(collectionId, '//Bob');53 await destroyCollectionExpectFailure(collectionId, '//Bob');44 await destroyCollectionExpectSuccess(collectionId, '//Alice');54 await destroyCollectionExpectSuccess(collectionId, '//Alice');45 });55 });56 it('fails when OwnerCanDestroy == false', async () => {57 const collectionId = await createCollectionExpectSuccess();58 await setCollectionLimitsExpectSuccess(alice, collectionId, { OwnerCanDestroy: false });5960 await destroyCollectionExpectFailure(collectionId, '//Alice');61 });46});62});4763tests/src/setCollectionLimits.test.tsdiffbeforeafterboth14 createCollectionExpectSuccess, getCreatedCollectionCount,14 createCollectionExpectSuccess, getCreatedCollectionCount,15 getCreateItemResult,15 getCreateItemResult,16 getDetailedCollectionInfo,16 getDetailedCollectionInfo,17 setCollectionLimitsExpectFailure,18 setCollectionLimitsExpectSuccess,17} from './util/helpers';19} from './util/helpers';182019chai.use(chaiAsPromised);21chai.use(chaiAsPromised);26const accountTokenOwnershipLimit = 0;28const accountTokenOwnershipLimit = 0;27const sponsoredDataSize = 0;29const sponsoredDataSize = 0;28const sponsoredMintSize = 0;30const sponsoredMintSize = 0;29const tokenLimit = 0;31const sponsorTimeout = 1;3031describe('hooks', () => {32 before(async () => {33 await usingApi(async () => {34 const keyring = new Keyring({ type: 'sr25519' });32const tokenLimit = 1;35 alice = keyring.addFromUri('//Alice');36 });37 });38 it('choose or create collection for testing', async () => {39 await usingApi(async () => {40 collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}});41 });42 });43});443345describe('setCollectionLimits positive', () => {34describe('setCollectionLimits positive', () => {46 let tx;35 let tx;47 before(async () => {36 before(async () => {48 await usingApi(async () => {37 await usingApi(async () => {49 const keyring = new Keyring({ type: 'sr25519' });38 const keyring = new Keyring({ type: 'sr25519' });50 alice = keyring.addFromUri('//Alice');39 alice = keyring.addFromUri('//Alice');40 collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}});51 });41 });52 });42 });53 it('execute setCollectionLimits with predefined params ', async () => {54 await usingApi(async (api: ApiPromise) => {55 tx = api.tx.nft.setCollectionLimits(56 collectionIdForTesting,57 {58 accountTokenOwnershipLimit,59 sponsoredDataSize,60 sponsoredMintSize,61 tokenLimit,62 },63 );64 const events = await submitTransactionAsync(alice, tx);65 const result = getCreateItemResult(events);66 // tslint:disable-next-line:no-unused-expression67 expect(result.success).to.be.true;68 });69 });70 it('get collection limits defined in previous test', async () => {43 it('execute setCollectionLimits with predefined params ', async () => {71 await usingApi(async (api: ApiPromise) => {44 await usingApi(async (api: ApiPromise) => {45 tx = api.tx.nft.setCollectionLimits(46 collectionIdForTesting,47 {48 AccountTokenOwnershipLimit: accountTokenOwnershipLimit,49 SponsoredMintSize: sponsoredDataSize,50 TokenLimit: tokenLimit,51 SponsorTimeout: sponsorTimeout,52 OwnerCanTransfer: true,53 OwnerCanDestroy: true54 },55 );56 const events = await submitTransactionAsync(alice, tx);57 const result = getCreateItemResult(events);5859 // get collection limits defined previously72 const collectionInfo = await getDetailedCollectionInfo(api, collectionIdForTesting) as ICollectionInterface;60 const collectionInfo = await getDetailedCollectionInfo(api, collectionIdForTesting) as ICollectionInterface;6162 // tslint:disable-next-line:no-unused-expression63 expect(result.success).to.be.true;73 expect(collectionInfo.Limits.AccountTokenOwnershipLimit.toNumber()).to.be.equal(accountTokenOwnershipLimit);64 expect(collectionInfo.Limits.AccountTokenOwnershipLimit.toNumber()).to.be.equal(accountTokenOwnershipLimit);74 expect(collectionInfo.Limits.SponsoredMintSize.toNumber()).to.be.equal(sponsoredMintSize);65 expect(collectionInfo.Limits.SponsoredMintSize.toNumber()).to.be.equal(sponsoredDataSize);75 expect(collectionInfo.Limits.TokenLimit.toNumber()).to.be.equal(tokenLimit);66 expect(collectionInfo.Limits.TokenLimit.toNumber()).to.be.equal(tokenLimit);76 expect(collectionInfo.Limits.SponsorTimeout.toNumber()).to.be.equal(sponsoredDataSize);67 expect(collectionInfo.Limits.SponsorTimeout.toNumber()).to.be.equal(sponsorTimeout);68 expect(collectionInfo.Limits.OwnerCanTransfer.valueOf()).to.be.true;69 expect(collectionInfo.Limits.OwnerCanDestroy.valueOf()).to.be.true;77 });70 });78 });71 });79});72});85 const keyring = new Keyring({ type: 'sr25519' });78 const keyring = new Keyring({ type: 'sr25519' });86 alice = keyring.addFromUri('//Alice');79 alice = keyring.addFromUri('//Alice');87 bob = keyring.addFromUri('//Bob');80 bob = keyring.addFromUri('//Bob');81 collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}});88 });82 });89 });83 });90 it('execute setCollectionLimits for not exists collection', async () => {84 it('execute setCollectionLimits for not exists collection', async () => {132 });126 });133 });127 });128129 it('fails when trying to enable OwnerCanTransfer after it was disabled', async () => {130 const collectionId = await createCollectionExpectSuccess();131 await setCollectionLimitsExpectSuccess(alice, collectionId, { 132 AccountTokenOwnershipLimit: accountTokenOwnershipLimit,133 SponsoredMintSize: sponsoredDataSize,134 TokenLimit: tokenLimit,135 SponsorTimeout: sponsorTimeout,136 OwnerCanTransfer: false,137 OwnerCanDestroy: true138 });139 await setCollectionLimitsExpectFailure(alice, collectionId, { 140 AccountTokenOwnershipLimit: accountTokenOwnershipLimit,141 SponsoredMintSize: sponsoredDataSize,142 TokenLimit: tokenLimit,143 SponsorTimeout: sponsorTimeout,144 OwnerCanTransfer: true,145 OwnerCanDestroy: true146 });147 });148149 it('fails when trying to enable OwnerCanDestroy after it was disabled', async () => {150 const collectionId = await createCollectionExpectSuccess();151 await setCollectionLimitsExpectSuccess(alice, collectionId, {152 AccountTokenOwnershipLimit: accountTokenOwnershipLimit,153 SponsoredMintSize: sponsoredDataSize,154 TokenLimit: tokenLimit,155 SponsorTimeout: sponsorTimeout,156 OwnerCanTransfer: true,157 OwnerCanDestroy: false158 });159 await setCollectionLimitsExpectFailure(alice, collectionId, { 160 AccountTokenOwnershipLimit: accountTokenOwnershipLimit,161 SponsoredMintSize: sponsoredDataSize,162 TokenLimit: tokenLimit,163 SponsorTimeout: sponsorTimeout,164 OwnerCanTransfer: true,165 OwnerCanDestroy: true166 });167 });134});168});135169tests/src/transferFrom.test.tsdiffbeforeafterboth3// file 'LICENSE', which is part of this source code package.3// file 'LICENSE', which is part of this source code package.4//4//5import { ApiPromise } from '@polkadot/api';5import { ApiPromise } from '@polkadot/api';6import { IKeyringPair } from '@polkadot/types/types';6import chai from 'chai';7import chai from 'chai';7import chaiAsPromised from 'chai-as-promised';8import chaiAsPromised from 'chai-as-promised';8import privateKey from './substrate/privateKey';9import privateKey from './substrate/privateKey';18 transferFromExpectFail,19 transferFromExpectFail,19 transferFromExpectSuccess,20 transferFromExpectSuccess,20 burnItemExpectSuccess,21 burnItemExpectSuccess,22 setCollectionLimitsExpectSuccess,21} from './util/helpers';23} from './util/helpers';222423chai.use(chaiAsPromised);25chai.use(chaiAsPromised);24const expect = chai.expect;26const expect = chai.expect;252726describe('Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => {28describe('Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => {29 let Alice: IKeyringPair;30 let Bob: IKeyringPair;31 let Charlie: IKeyringPair;3233 before(async () => {34 await usingApi(async (api) => {35 Alice = privateKey('//Alice');36 Bob = privateKey('//Bob');37 Charlie = privateKey('//Charlie');38 });39 });4027 it('Execute the extrinsic and check nftItemList - owner of token', async () => {41 it('Execute the extrinsic and check nftItemList - owner of token', async () => {28 await usingApi(async (api: ApiPromise) => {42 await usingApi(async (api: ApiPromise) => {29 const Alice = privateKey('//Alice');30 const Bob = privateKey('//Bob');31 const Charlie = privateKey('//Charlie');32 // nft43 // nft33 const nftCollectionId = await createCollectionExpectSuccess();44 const nftCollectionId = await createCollectionExpectSuccess();34 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');45 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');67 });78 });68 });79 });8081 it('can be called by collection owner on non-owned item when OwnerCanTransfer == true', async () => {82 const collectionId = await createCollectionExpectSuccess();83 const itemId = await createItemExpectSuccess(Alice, collectionId, 'NFT', Bob.address);8485 await transferFromExpectSuccess(collectionId, itemId, Alice, Bob, Charlie);86 });69});87});708871describe('Negative Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => {89describe('Negative Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => {90 let Alice: IKeyringPair;91 let Bob: IKeyringPair;92 let Charlie: IKeyringPair;9394 before(async () => {95 await usingApi(async (api) => {96 Alice = privateKey('//Alice');97 Bob = privateKey('//Bob');98 Charlie = privateKey('//Charlie');99 });100 });10172 it('transferFrom for a collection that does not exist', async () => {102 it('transferFrom for a collection that does not exist', async () => {73 await usingApi(async (api: ApiPromise) => {103 await usingApi(async (api: ApiPromise) => {74 const Alice = privateKey('//Alice');75 const Bob = privateKey('//Bob');76 const Charlie = privateKey('//Charlie');77 // nft104 // nft78 const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number;105 const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number;79 await approveExpectFail(nftCollectionCount + 1, 1, Alice, Bob);106 await approveExpectFail(nftCollectionCount + 1, 1, Alice, Bob);113140114 it('transferFrom for not approved address', async () => {141 it('transferFrom for not approved address', async () => {115 await usingApi(async (api: ApiPromise) => {142 await usingApi(async (api: ApiPromise) => {116 const Alice = privateKey('//Alice');117 const Bob = privateKey('//Bob');118 const Charlie = privateKey('//Charlie');119 // nft143 // nft120 const nftCollectionId = await createCollectionExpectSuccess();144 const nftCollectionId = await createCollectionExpectSuccess();121 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');145 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');137161138 it('transferFrom incorrect token count', async () => {162 it('transferFrom incorrect token count', async () => {139 await usingApi(async (api: ApiPromise) => {163 await usingApi(async (api: ApiPromise) => {140 const Alice = privateKey('//Alice');141 const Bob = privateKey('//Bob');142 const Charlie = privateKey('//Charlie');143 // nft164 // nft144 const nftCollectionId = await createCollectionExpectSuccess();165 const nftCollectionId = await createCollectionExpectSuccess();145 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');166 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');164185165 it('execute transferFrom from account that is not owner of collection', async () => {186 it('execute transferFrom from account that is not owner of collection', async () => {166 await usingApi(async (api: ApiPromise) => {187 await usingApi(async (api: ApiPromise) => {167 const Alice = privateKey('//Alice');168 const Bob = privateKey('//Bob');169 const Charlie = privateKey('//Charlie');170 const Dave = privateKey('//Dave');188 const Dave = privateKey('//Dave');171 // nft189 // nft172 const nftCollectionId = await createCollectionExpectSuccess();190 const nftCollectionId = await createCollectionExpectSuccess();206 });224 });207 it( 'transferFrom burnt token before approve NFT', async () => {225 it( 'transferFrom burnt token before approve NFT', async () => {208 await usingApi(async (api: ApiPromise) => {226 await usingApi(async (api: ApiPromise) => {209 const Alice = privateKey('//Alice');210 const Bob = privateKey('//Bob');211 const Charlie = privateKey('//Charlie');212 // nft227 // nft213 const nftCollectionId = await createCollectionExpectSuccess();228 const nftCollectionId = await createCollectionExpectSuccess();214 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');229 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');219 });234 });220 it( 'transferFrom burnt token before approve Fungible', async () => {235 it( 'transferFrom burnt token before approve Fungible', async () => {221 await usingApi(async (api: ApiPromise) => {236 await usingApi(async (api: ApiPromise) => {222 const Alice = privateKey('//Alice');223 const Bob = privateKey('//Bob');224 const Charlie = privateKey('//Charlie');225 const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});237 const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});226 const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');238 const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');227 await burnItemExpectSuccess(Alice, fungibleCollectionId, 1, 10);239 await burnItemExpectSuccess(Alice, fungibleCollectionId, 1, 10);232 }); 244 }); 233 it( 'transferFrom burnt token before approve ReFungible', async () => {245 it( 'transferFrom burnt token before approve ReFungible', async () => {234 await usingApi(async (api: ApiPromise) => {246 await usingApi(async (api: ApiPromise) => {235 const Alice = privateKey('//Alice');236 const Bob = privateKey('//Bob');237 const Charlie = privateKey('//Charlie');238 const reFungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});247 const reFungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});239 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');248 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');240 await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 1);249 await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 1);246 255 247 it( 'transferFrom burnt token after approve NFT', async () => {256 it( 'transferFrom burnt token after approve NFT', async () => {248 await usingApi(async (api: ApiPromise) => {257 await usingApi(async (api: ApiPromise) => {249 const Alice = privateKey('//Alice');250 const Bob = privateKey('//Bob');251 const Charlie = privateKey('//Charlie');252 // nft258 // nft253 const nftCollectionId = await createCollectionExpectSuccess();259 const nftCollectionId = await createCollectionExpectSuccess();254 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');260 const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');259 });265 });260 it( 'transferFrom burnt token after approve Fungible', async () => {266 it( 'transferFrom burnt token after approve Fungible', async () => {261 await usingApi(async (api: ApiPromise) => {267 await usingApi(async (api: ApiPromise) => {262 const Alice = privateKey('//Alice');263 const Bob = privateKey('//Bob');264 const Charlie = privateKey('//Charlie');265 const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});268 const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});266 const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');269 const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');267 await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob);270 await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob);272 }); 275 }); 273 it( 'transferFrom burnt token after approve ReFungible', async () => {276 it( 'transferFrom burnt token after approve ReFungible', async () => {274 await usingApi(async (api: ApiPromise) => {277 await usingApi(async (api: ApiPromise) => {275 const Alice = privateKey('//Alice');276 const Bob = privateKey('//Bob');277 const Charlie = privateKey('//Charlie');278 const reFungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});278 const reFungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});279 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');279 const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');280 await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob);280 await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob);284 });284 });285 }); 285 });286287 it('fails when called by collection owner on non-owned item when OwnerCanTransfer == false', async () => {288 const collectionId = await createCollectionExpectSuccess();289 const itemId = await createItemExpectSuccess(Alice, collectionId, 'NFT', Bob.address);290 await setCollectionLimitsExpectSuccess(Alice, collectionId, { OwnerCanTransfer: false });291292 await transferFromExpectFail(collectionId, itemId, Alice, Bob, Charlie);293 });286});294});287295tests/src/types.tsdiffbeforeafterboth17 SponsoredMintSize: BN;17 SponsoredMintSize: BN;18 TokenLimit: BN;18 TokenLimit: BN;19 SponsorTimeout: BN;19 SponsorTimeout: BN;20 OwnerCanTransfer: boolean;21 OwnerCanDestroy: boolean;20 };22 };21 MintMode: boolean;23 MintMode: boolean;22 Mode: {24 Mode: {tests/src/util/helpers.tsdiffbeforeafterboth36 success: boolean;36 success: boolean;37 collectionId: number;37 collectionId: number;38 itemId: number;38 itemId: number;39 recipient: string;39}40}4142interface TransferResult {43 success: boolean;44 collectionId: number;45 itemId: number;46 sender: string;47 recipient: string;48 value: bigint;49}405041interface IReFungibleOwner {51interface IReFungibleOwner {42 Fraction: BN;52 Fraction: BN;94 let success = false;104 let success = false;95 let collectionId: number = 0;105 let collectionId: number = 0;96 let itemId: number = 0;106 let itemId: number = 0;107 let recipient: string = '';97 events.forEach(({ phase, event: { data, method, section } }) => {108 events.forEach(({ phase, event: { data, method, section } }) => {98 // console.log(` ${phase}: ${section}.${method}:: ${data}`);109 // console.log(` ${phase}: ${section}.${method}:: ${data}`);99 if (method == 'ExtrinsicSuccess') {110 if (method == 'ExtrinsicSuccess') {100 success = true;111 success = true;101 } else if ((section == 'nft') && (method == 'ItemCreated')) {112 } else if ((section == 'nft') && (method == 'ItemCreated')) {102 collectionId = parseInt(data[0].toString());113 collectionId = parseInt(data[0].toString());103 itemId = parseInt(data[1].toString());114 itemId = parseInt(data[1].toString());115 recipient = data[2].toString();104 }116 }105 });117 });106 const result: CreateItemResult = {118 const result: CreateItemResult = {107 success,119 success,108 collectionId,120 collectionId,109 itemId,121 itemId,122 recipient,110 };123 };111 return result;124 return result;112}125}126127export function getTransferResult(events: EventRecord[]): TransferResult {128 const result: TransferResult = {129 success: false,130 collectionId: 0,131 itemId: 0,132 sender: '',133 recipient: '',134 value: 0n,135 };136137 events.forEach(({event: {data, method, section}}) => {138 if (method === 'ExtrinsicSuccess') {139 result.success = true;140 } else if (section === 'nft' && method === 'Transfer') {141 result.collectionId = +data[0].toString();142 result.itemId = +data[1].toString();143 result.sender = data[2].toString();144 result.recipient = data[3].toString();145 result.value = BigInt(data[4].toString());146 }147 });148149 return result;150}113151114interface Invalid {152interface Invalid {115 type: 'Invalid';153 type: 'Invalid';670 }708 }671 const transferTx = await api.tx.nft.transfer(recipient.address, collectionId, tokenId, value);709 const transferTx = await api.tx.nft.transfer(recipient.address, collectionId, tokenId, value);672 const events = await submitTransactionAsync(sender, transferTx);710 const events = await submitTransactionAsync(sender, transferTx);673 const result = getCreateItemResult(events);711 const result = getTransferResult(events);674 // tslint:disable-next-line:no-unused-expression712 // tslint:disable-next-line:no-unused-expression675 expect(result.success).to.be.true;713 expect(result.success).to.be.true;714 expect(result.collectionId).to.be.equal(collectionId);715 expect(result.itemId).to.be.equal(tokenId);716 expect(result.sender).to.be.equal(sender.address);717 expect(result.recipient).to.be.equal(recipient.address);718 expect(result.value.toString()).to.be.equal(value.toString());676 if (type === 'NFT') {719 if (type === 'NFT') {677 const nftItemData = await api.query.nft.nftItemList(collectionId, tokenId) as unknown as ITokenDataType;720 const nftItemData = await api.query.nft.nftItemList(collectionId, tokenId) as unknown as ITokenDataType;678 expect(nftItemData.Owner.toString()).to.be.equal(recipient.address);721 expect(nftItemData.Owner.toString()).to.be.equal(recipient.address);786 }829 }787 expect(collectionId).to.be.equal(result.collectionId);830 expect(collectionId).to.be.equal(result.collectionId);788 expect(BItemCount).to.be.equal(result.itemId);831 expect(BItemCount).to.be.equal(result.itemId);832 expect(owner).to.be.equal(result.recipient);789 newItemId = result.itemId;833 newItemId = result.itemId;790 });834 });791 return newItemId;835 return newItemId;