difftreelog
Merge commit '1f47f964a0fc5eaa160d9f6fc463551ad4dbd74e' into feature/NFTPAR-196_sponsor_setVariableMetadata
in: master
19 files changed
.gitignorediffbeforeafterboth--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,6 @@
einstein_key_file
*store_key*.json
-/.idea/
\ No newline at end of file
+/.idea/
+
+tests/.vscode
\ No newline at end of file
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3456,7 +3456,7 @@
[[package]]
name = "nft-runtime"
-version = "3.0.0"
+version = "2.1.0"
dependencies = [
"frame-benchmarking",
"frame-executive",
@@ -3785,7 +3785,7 @@
[[package]]
name = "pallet-nft"
-version = "3.0.0"
+version = "2.1.0"
dependencies = [
"frame-benchmarking",
"frame-support",
README.mddiffbeforeafterboth--- a/README.md
+++ b/README.md
@@ -4,44 +4,55 @@
## Project Description
-The 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.
+The 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.
-The 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.
+The 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.
The NFT Chain also provides:
-* Smart Contracts Pallet and example smart contract that interacts with NFT Runtime
-* ERC-1155 Functionality (currently PoC as Re-Fungible tokens, i.e. items that are still unique, but that can be split between multiple users)
-* 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.
+- Smart Contracts Pallet and example smart contract that interacts with NFT Runtime
+- ERC-1155 Functionality (currently PoC as Re-Fungible tokens, i.e. items that are still unique, but that can be split
+ between multiple users)
+- 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.
Wider NFT Ecosystem (most of it was developed during Hackusama):
-* [SubstraPunks Game hosted on IPFS](https://github.com/usetech-llc/substrapunks)
-* [NFT Wallet and UI](https://uniqueapps.usetech.com/#/nft)
-* [NFT Asset for Unity Framework](https://github.com/usetech-llc/nft_unity)
+- [SubstraPunks Game hosted on IPFS](https://github.com/usetech-llc/substrapunks)
+- [NFT Wallet and UI](https://uniqueapps.usetech.com/#/nft)
+- [NFT Asset for Unity Framework](https://github.com/usetech-llc/nft_unity)
+
Please see our [walk-thorugh instructions](doc/hackusama_walk_through.md) to try everything out!
## Hackusama Update
During the Kusama Hackaphon the following changes were made:
-* Enabled Smart Contracts Pallet
-* Enabled integration between Smart Contracts and NFT Pallet (required special edition of RC4 Substrate version)
-* Fixed misc. bugs in NFT Pallet
-* Deployed NFT TestNet. Public node available at wss://unique.usetech.com, custom UI types - see below in this README.
-* New Features:
- * Re-Fungible Token Mode
- * Off-Chain Schema to store token image URLs
- * Alternative economic model
- * White Lists and Public Mint Permission
-* Use example: [SubstraPunks Game](https://github.com/usetech-llc/substrapunks), fully hosted on IPFS and NFT Testnet Blockchain.
+- Enabled Smart Contracts Pallet
+- Enabled integration between Smart Contracts and NFT Pallet (required special edition of RC4 Substrate version)
+- Fixed misc. bugs in NFT Pallet
+- Deployed NFT TestNet. Public node available at wss://unique.usetech.com, custom UI types - see below in this README.
+- New Features:
+ - Re-Fungible Token Mode
+ - Off-Chain Schema to store token image URLs
+ - Alternative economic model
+ - White Lists and Public Mint Permission
+- Use example: [SubstraPunks Game](https://github.com/usetech-llc/substrapunks), fully hosted on IPFS and NFT Testnet
+ Blockchain.
+
## Application Development
If you are building an application that operates NFT tokens, use [this document](doc/application_development.md).
## Building
-Building 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.
+Building 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.
1. Install Rust:
@@ -60,7 +71,7 @@
rustup default nightly-2021-01-27
```
-4. Add wasm target for default toolchain:
+4. Add wasm target for nightly toolchain:
```bash
rustup target add wasm32-unknown-unknown --toolchain nightly-2021-01-27
@@ -84,11 +95,17 @@
cargo run -- --dev
```
-Detailed logs may be shown by running the node with the following environment variables set: `RUST_LOG=debug RUST_BACKTRACE=1 cargo run -- --dev`.
+Detailed logs may be shown by running the node with the following environment variables set:
+`RUST_LOG=debug RUST_BACKTRACE=1 cargo run -- --dev`.
-If 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.
+If 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.
-We'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:
+We'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:
```bash
cargo run -- \
@@ -100,7 +117,9 @@
--validator
```
-In 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:
+In 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:
```bash
cargo run -- \
@@ -152,4 +171,4 @@
## Running Integration Tests
-See [tests/README.md](./tests/README.md).
\ No newline at end of file
+See [tests/README.md](./tests/README.md).
doc/application_development.mddiffbeforeafterboth--- a/doc/application_development.md
+++ b/doc/application_development.md
@@ -2,17 +2,23 @@
## Architecture
-Both 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.
+Both 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.
### Server-based architecture
-In 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.
+In 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.

### Serverless architecture
-In 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.
+In 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.

@@ -27,199 +33,252 @@
#### CreateCollection
##### Description
-This 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.
+This 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.
+
##### Permissions
-Anyone
+- Anyone
+
##### Parameters
-customDataSz: Size of NFT properties data.
+- customDataSz: Size of NFT properties data.
+
##### Events
-CollectionCreated
-CollectionID: Globally unique identifier of newly created collection.
-Owner: Collection owner
+- CollectionCreated
+ - CollectionID: Globally unique identifier of newly created collection.
+ - Mode: Collection mode
+ - Owner: Collection owner
+
#### ChangeCollectionOwner
##### Description
+
Change the owner of the collection
##### Permissions
-Collection Owner
+- Collection Owner
+
##### Parameters
-CollectionId
+- CollectionId
+
#### DestroyCollection
##### Description
-DANGEROUS: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.
+DANGEROUS: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose
+real money.
+
##### Permissions
-Collection Owner
+- Collection Owner
+
##### Parameters
-CollectionId
+- CollectionId
+
#### CreateItem
##### Description
+
This method creates a concrete instance of NFT Collection created with CreateCollection method.
##### Permissions
-Collection Owner
-Collection Admin
+- Collection Owner
+- Collection Admin
+
##### Parameters
-CollectionID: ID of the collection
-Properties: Array of bytes that contains NFT properties. Since NFT Module is agnostic of properties’ meaning, it is treated purely as an array of bytes
-Owner: Address, initial owner of the NFT
+- CollectionID: ID of the collection
+- Properties: Array of bytes that contains NFT properties. Since NFT Module is agnostic of properties’ meaning, it is
+ treated purely as an array of bytes
+- Owner: Address, initial owner of the NFT
+
##### Events
ItemCreated
ItemId: 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.
+Recipient: Address, owner of newly created item
#### BurnItem
##### Description
+
This method destroys a concrete instance of NFT.
##### Permissions
-Collection Owner
-Collection Admin
-Current NFT Owner
+- Collection Owner
+- Collection Admin
+- Current NFT Owner
+
##### Parameters
-CollectionID: ID of the collection
-ItemID: ID of NFT to burn
+- CollectionID: ID of the collection
+- ItemID: ID of NFT to burn
+
##### Events
-ItemDestroyed
-CollectionID
-ItemId: Identifier of burned NFT
+- ItemDestroyed
+ - CollectionID
+ - ItemId: Identifier of burned NFT
#### AddCollectionAdmin
##### Description
-NFT 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.
+NFT 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.
+
This method adds an admin of the Collection.
##### Permissions
-Collection Owner
-Collection Admin
+- Collection Owner
+- Collection Admin
+
##### Parameters
-CollectionID: ID of the Collection to add admin for
-Admin: Address of new admin to add
+- CollectionID: ID of the Collection to add admin for
+- Admin: Address of new admin to add
+
#### RemoveCollectionAdmin
##### Description
-Remove 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.
+Remove 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.
+
##### Permissions
-Collection Owner
-Collection Admin
+- Collection Owner
+- Collection Admin
+
##### Parameters
-CollectionID: ID of the Collection to remove admin for
-Admin: Address of admin to remove
+- CollectionID: ID of the Collection to remove admin for
+- Admin: Address of admin to remove
+
### Item Ownership and Transfers
+
This group of methods allows managing NFT ownership.
#### GetOwner
##### Description
-Return the address of the NFT owner.
+Return the address of the NFT owner.
+
##### Permissions
-Anyone
+- Anyone
+
##### Parameters
-CollectionId
-ItemId: ID of the NFT
+- CollectionId
+- ItemId: ID of the NFT
+
##### Returns
+
Owner address
#### BalanceOf
##### Description
-This method is included for compatibility with ERC-721. Return the total count of NFTs of a given Collection that belong to a given address.
+This method is included for compatibility with ERC-721. Return the total count of NFTs of a given Collection that belong
+to a given address.
+
##### Permissions
-Anyone
+- Anyone
+
##### Parameters
-CollectionId
-Address to count NFTs for
+- CollectionId
+- Address to count NFTs for
+
##### Returns
+
Total count of NFTs for Address
-
#### Transfer
##### Description
+
Change ownership of the NFT.
##### Permissions
-Collection Owner
-Collection Admin
-Current NFT owner
+- Collection Owner
+- Collection Admin
+- Current NFT owner
+
##### Parameters
-Recipient: Address of token recipient
-ClassId: ID of item class
-ItemId: ID of the item
+- Recipient: Address of token recipient
+- ClassId: ID of item class
+- ItemId: ID of the item
+
#### TransferFrom
##### Description
-Change 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.
+Change 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.
+
##### Permissions
-Collection Owner
-Collection Admin
-Current NFT owner
-Address approved by current NFT owner
+- Collection Owner
+- Collection Admin
+- Current NFT owner
+- Address approved by current NFT owner
+
##### Parameters
-Recipient: Address of token recipient
-ClassId: ID of item class
-ItemId: ID of the item
+- Recipient: Address of token recipient
+- ClassId: ID of item class
+- ItemId: ID of the item
#### Approve
##### Description
+
Set, change, or remove approved address to transfer the ownership of the NFT.
##### Permissions
-Collection Owner
-Collection Admin
-Current NFT owner
+- Collection Owner
+- Collection Admin
+- Current NFT owner
+
##### Parameters
-Approved: Address that is approved to transfer this NFT or zero (if needed to remove approval)
-ClassId: ID of item class
-ItemId: ID of the item
+- Approved: Address that is approved to transfer this NFT or zero (if needed to remove approval)
+- ClassId: ID of item class
+- ItemId: ID of the item
+
#### GetApproved
##### Description
+
Get the approved address for a single NFT.
##### Permissions
-Anyone
+- Anyone
+
##### Parameters
-ClassId: ID of item class
-ItemId: ID of the item
+- ClassId: ID of item class
+- ItemId: ID of the item
+
##### Returns
+
Approved address
doc/demo_milestone1-2.mddiffbeforeafterboth--- a/doc/demo_milestone1-2.md
+++ b/doc/demo_milestone1-2.md
@@ -6,28 +6,41 @@
### A running chain
-**Substrate based blockchain node to host NFT Tracking Module created with substrate-up scripts (currently substrate-up only support Substrate 1.0)**
+**Substrate based blockchain node to host NFT Tracking Module created with substrate-up scripts (currently substrate-up
+only support Substrate 1.0)**
-We 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.
+We 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.
The node can be run using docker container:
+
```
docker-compose up -d
```
#### NFT Tracking Module
-Open 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.
-#### Balances Module, Other modules included by default
-The [same UI](https://polkadot.js.org/apps/#/extrinsics) allows verification that all other modules are also installed as needed.
+Open 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.
+
+#### Balances Module, Other modules included by default
+
+The [same UI](https://polkadot.js.org/apps/#/extrinsics) allows verification that all other modules are also installed
+as needed.
#### Configuration of runtime as needed (e.g. for hot module updates)
-The `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.
+The `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.
### NFT Tracking Module
-Before 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:
+Before 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:
+
```
{
"NftItemType": {
@@ -50,11 +63,16 @@
#### CreateCollection
-Before 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.
+Before 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.
-Open 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.
+Open 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.
-Also, 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:
+Also, 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:
```
nft.collection: CollectionType
@@ -67,7 +85,11 @@
### ChangeCollectionOwner
-Open 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:
+Open 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:
```
nft.collection: CollectionType
@@ -84,7 +106,9 @@
**Note: Before destroying collection, you can see Milestone 2 deliverables to avoid creating collection again**
-Run `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:
+Run `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:
+
```
nft.collection: CollectionType
{
@@ -108,20 +132,29 @@
**using Polkadot UI from https://substrate-ui.parity.io/**
-This document, except we are using Substrate v2, so we can use the newer version of the UI: https://polkadot.js.org/apps/#
+This document, except we are using Substrate v2, so we can use the newer version of the UI:
+https://polkadot.js.org/apps/#
## Milestone 2
+
### NFT Tracking Module
+
**Note:** If you have destroyed collection, create it again using `nft`.`createCollection` method.
-**Note 2:** The order of items in this section is different from the original spec to make the acceptance workflow more natural.
+**Note 2:** The order of items in this section is different from the original spec to make the acceptance workflow more
+natural.
-If you did not destroy collection while looking at Milestone 1 deliverables, use collection ID 1 in the further examples, otherwise use collection ID = 2.
+If you did not destroy collection while looking at Milestone 1 deliverables, use collection ID 1 in the further
+examples, otherwise use collection ID = 2.
#### CreateItem
-Before 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.
-Execute 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:
+Before 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.
+
+Execute 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:
```
nft.itemList: NftItemType
@@ -133,7 +166,10 @@
```
#### GetOwner
-Reading the ownership is done by reading chainstate `nft`.`itemList(<Collection ID>, 1)`. One of the returned fields is Owner:
+
+Reading the ownership is done by reading chainstate `nft`.`itemList(<Collection ID>, 1)`. One of the returned fields is
+Owner:
+
```
nft.itemList: NftItemType
{
@@ -144,7 +180,9 @@
```
#### Transfer
+
Execute `nft`.`transfer` from ALICE address to transfer token 1 to BOB and check the ownership again:
+
```
nft.itemList: NftItemType
{
@@ -159,14 +197,18 @@
#### BalanceOf
Read the chain state `nft`.`balance` for ALICE address and see that she owns 1 token:
+
```
nft.balance: u64
1
```
#### AddCollectionAdmin
-Execute `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:
+Execute `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:
+
```
nft.adminList: Vec<AccountId>
[
@@ -175,15 +217,21 @@
```
#### RemoveCollectionAdmin
-Execute `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:
+Execute `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:
+
```
nft.adminList: Vec<AccountId>
[]
```
#### BurnItem
-Execute `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.
+
+Execute `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.
+
```
nft.itemList: NftItemType
{
@@ -194,14 +242,15 @@
```
### Economic Model Specification
+
[Economic Model Specification](economic_model.md)
### Contracts Module Specification
+
https://docs.google.com/document/d/1gDtYjPR9C1VZChxEA-xAdQWQyEvMg245XrZR_MpE3cg/edit?usp=sharing
### Bonus goal
**Basic demo - Cryptopunks representation on the Substrate Chain.**
-See this repo, it has running and playing instructions:
-https://github.com/usetech-llc/substrapunks
+See this repo, it has running and playing instructions: https://github.com/usetech-llc/substrapunks
doc/economic_model.mddiffbeforeafterboth--- a/doc/economic_model.md
+++ b/doc/economic_model.md
@@ -2,26 +2,33 @@
## Economy Goals
-One 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).
+One 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).
## Summary
-We are going to offer several fee models for our users. Some models will be implemented immediately, some will wait until there is a demand.
+We are going to offer several fee models for our users. Some models will be implemented immediately, some will wait
+until there is a demand.
-The 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.
+The 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.
Initially we offer 4 models for consideration:
-* User paid fees (conventional blockchain method)
-* Sponsored (same as above, but paid by sponsor)
-* Prepaid plan
-* Resource purchase
+- User paid fees (conventional blockchain method)
+- Sponsored (same as above, but paid by sponsor)
+- Prepaid plan
+- Resource purchase
## Token and Accounts
Network Tokens are tracked via standard Balances module, which is included in NFT Parachain.
-User Account matches to user address in the network. Among other properties user Account has Balance, which will be used later on.
+User Account matches to user address in the network. Among other properties user Account has Balance, which will be used
+later on.
## Settings Economic Model
@@ -31,171 +38,234 @@
#### Description
-Set 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.
+Set 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.
##### List of transactions
+
Transactions that can be sponsored:
-* Transfer
-* Approve
-* TransferFrom
-* TransferWithData
-* TransferFromWithData
-* BurnItem
+- Transfer
+- Approve
+- TransferFrom
+- TransferWithData
+- TransferFromWithData
+- BurnItem
+
##### Rate Limiting
This is a design proposal, actual implementation may change.
Features:
-* 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.
-* 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.
-* A pallet method (SetCollectionRateLimits) will be added to set these parameters and enable rate limiting.
-* One idea to consider is deposits that must be made by an address in order to become white listed (instead of admin review).
+- 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.
+- 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.
+- A pallet method (SetCollectionRateLimits) will be added to set these parameters and enable rate limiting.
+- One idea to consider is deposits that must be made by an address in order to become white listed (instead of admin
+ review).
+
#### Permissions
-* Collection Owner
+- Collection Owner
#### Parameters
-* CollectionId: ID of the collection
-* Sponsor: Sponsor address
-* Fee Model ID: ID of selected economic model. Currently, only "Sponsored" mode is supported, which will be ID = 1.
+- CollectionId: ID of the collection
+- Sponsor: Sponsor address
+- Fee Model ID: ID of selected economic model. Currently, only "Sponsored" mode is supported, which will be ID = 1.
### ConfirmSponsorship
+
#### Description
+
Confirm sponsorship of a collection. This call is needed to protect sponsor address from malicious collection creators.
#### Permissions
-* Collection Sponsor only
+- Collection Sponsor only
+
#### Parameters
-* CollectionId: ID of the collection
+- CollectionId: ID of the collection
+
### RemoveCollectionSponsor
+
#### Description
+
Switch back to pay-per-own-transaction model.
#### Permissions
-* Collection owner
+- Collection owner
+
#### Parameters
-* collectionId: ID of the collection
+- collectionId: ID of the collection
+
### ClaimContract
+
#### Description
-Claim 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.
+Claim 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.
+
#### Permissions
-* Anyone
+- Anyone
+
#### Parameters
-* ContractAddress: Address of the contract
-* Sponsor: Sponsor address
+- ContractAddress: Address of the contract
+- Sponsor: Sponsor address
+
### SetContractSponsor
+
#### Description
-Set 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.
+Set 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.
+
#### Permissions
-* Contract Owner
+- Contract Owner
+
#### Parameters
-* ContractAddress: Address of the contract
-* Sponsor: Sponsor address
-* Fee Model ID: ID of selected economic model. Currently, only "Sponsored" mode is supported, which will be ID = 1.
+- ContractAddress: Address of the contract
+- Sponsor: Sponsor address
+- Fee Model ID: ID of selected economic model. Currently, only "Sponsored" mode is supported, which will be ID = 1.
+
### ConfirmContractSponsorship
+
#### Description
+
Confirm sponsorship of a contract. This call is needed to protect sponsor address from malicious sponsorship requests.
#### Permissions
-* Contract Sponsor only
+- Contract Sponsor only
+
#### Parameters
-* ContractAddress: Address of the contract
+- ContractAddress: Address of the contract
+
### RemoveContractSponsor
+
#### Description
+
Switch back to pay-per-own-transaction model.
#### Permissions
-* Contract owner
+- Contract owner
+
#### Parameters
-* contractAddress: Address of the contract
+- contractAddress: Address of the contract
+
### SetPublicAccessMode
+
#### Description
-Toggle 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.
-White 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.
+Toggle 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.
+
+White 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.
#### Permissions
-* Collection Owner
+- Collection Owner
+
#### Parameters
-* CollectionID: ID of the Collection to set access mode for
-* Mode
- * 0 = Normal
- * 1 = White list
+- CollectionID: ID of the Collection to set access mode for
+- Mode
+ - 0 = Normal
+ - 1 = White list
+
### AddToWhiteList
+
#### Description
+
Add an address to white list.
#### Permissions
-* Collection Owner
-* Collection Admin
+- Collection Owner
+- Collection Admin
+
#### Parameters
-* CollectionID: ID of the Collection
-* Address
+- CollectionID: ID of the Collection
+- Address
+
### RemoveFromWhiteList
+
#### Description
+
Remove an address from white list.
#### Permissions
-* Collection Owner
-* Collection Admin
+- Collection Owner
+- Collection Admin
+
#### Parameters
-* CollectionID: ID of the Collection
-* Address
+- CollectionID: ID of the Collection
+- Address
+
### SetCollectionRateLimits
+
#### Description
+
Enable or disable rate limits for a collection
#### Permissions
-* Collection Owner
-* Collection Admin
+- Collection Owner
+- Collection Admin
+
#### Parameters
-* CollectionID: ID of the Collection
-* TokenInterval: Number of seconds allowed between sponsored token trasnsactions. 0 means there is no limit.
-* AddressInterval: Number of seconds allowed between sponsored address trasnsactions. 0 means there is no limit.
+- CollectionID: ID of the Collection
+- TokenInterval: Number of seconds allowed between sponsored token trasnsactions. 0 means there is no limit.
+- AddressInterval: Number of seconds allowed between sponsored address trasnsactions. 0 means there is no limit.
## User Paid Fees
-This 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.
+This 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.
Each module call will have weight annotation that will determine the weight calculation for this call:
-* CreateCollection: Fixed
-* ChangeCollectionOwner: Fixed
-* DestroyCollection: Linear of number of items (or owner addresses, depending on the collection type) multiplied by Collection Data Size
-* CreateItem: Linear of Collection Data Size
-* BurnItem: Linear of Collection Data Size
-* AddCollectionAdmin: Fixed
-* RemoveCollectionAdmin: Fixed
-* Transfers and approvals: Fixed
-* Transfers with data: Fixed + linear of data size
-* All other: Fixed
+- CreateCollection: Fixed
+- ChangeCollectionOwner: Fixed
+- DestroyCollection: Linear of number of items (or owner addresses, depending on the collection type) multiplied by
+ Collection Data Size
+- CreateItem: Linear of Collection Data Size
+- BurnItem: Linear of Collection Data Size
+- AddCollectionAdmin: Fixed
+- RemoveCollectionAdmin: Fixed
+- Transfers and approvals: Fixed
+- Transfers with data: Fixed + linear of data size
+- All other: Fixed
-This model is default and will be set for each collection until collection owner decides to change it to some other model.
+This model is default and will be set for each collection until collection owner decides to change it to some other
+model.
### Fee Distribution
@@ -203,30 +273,32 @@
## Sponsored
-The 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.
+The 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.
### Fee Distribution
Same as in User Paid Fees.
-
## Prepaid Plan
-
-Collection Owner makes regular payments to prepay for some planned network load, i.e. some fixed number of transactions, created items, etc.
+Collection Owner makes regular payments to prepay for some planned network load, i.e. some fixed number of transactions,
+created items, etc.
### Fee Distribution
Same as in User Paid Fees.
-
## Resource Purchase
-Collection 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).
+Collection 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).
-Received 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.
+Received 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.
### Fee Distribution
-Fixed percentage of staking income is distributed between validators proportionally to their contribution to the network. The rest is credited to the network owner.
-
+Fixed percentage of staking income is distributed between validators proportionally to their contribution to the
+network. The rest is credited to the network owner.
doc/hackusama_walk_through.mddiffbeforeafterboth--- a/doc/hackusama_walk_through.md
+++ b/doc/hackusama_walk_through.md
@@ -8,46 +8,70 @@
### Deployed NFT TestNet
-The TestNet public node is avaiable at wss://unique.usetech.com. It is easy to verify that it functions with the standard AppsUI:
+The TestNet public node is avaiable at wss://unique.usetech.com. It is easy to verify that it functions with the
+standard AppsUI:
1. Open the [Apps UI](https://polkadot.js.org/apps/#)
2. Click on the network icon in the top left corner
3. Scroll down the list and input `wss://unique.usetech.com` under "custom endpoint"
-4. 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.
-5. Now the NFT node is connected with AppsUI.
-6. Go to [Chain State](https://polkadot.js.org/apps/#/chainstate) and verify that NFT pallet is visible and you can read information from it.
-7. 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.
+4. 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.
+5. Now the NFT node is connected with AppsUI.
+6. Go to [Chain State](https://polkadot.js.org/apps/#/chainstate) and verify that NFT pallet is visible and you can read
+ information from it.
+7. 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.
#### Getting some Unique Tokens
-1. 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.
+1. 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.
2. Create an address
-3. 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.
+3. 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.
### Smart Contracts Pallet
-1. Open [Contracts](https://polkadot.js.org/apps/#/contracts) tab. It only appears in the UI when Smart Contracts pallet is included in the runtime.
+1. Open [Contracts](https://polkadot.js.org/apps/#/contracts) tab. It only appears in the UI when Smart Contracts pallet
+ is included in the runtime.
2. Additionally, we can find an existing smart contract. Click on "Add an existing contract" link.
3. Input this contract address: `5GdNqKMv4Sszq3SRd3TkXNa6a9ct4D3nXvtTWTFR7rTyccVJ`
4. Input any contract name, for example `Claim Substrapunks`
-5. 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.
-6. 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".
+5. 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.
+6. 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".
### Integration Between Smart Contracts and NFT Pallet
-This 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!
+This 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!
-The 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.
+The 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.
### Re-Fungibility support
-This 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.
+This 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.
1. Open the [NFT Wallet](https://uniqueapps.usetech.com/#/nft)
2. Search for collection called "Artwork". Search can be done either by collection name or collection ID (which is 2).
3. Click on "+ Add collection" in search results. The collection will appear under "My collections"
4. Expand the "Artwork" collection and see that you own a token with a partial balance
-5. 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.
+5. 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.
6. Observe that your balance decreased by the amount you entered.
#### The Hard Way
@@ -84,15 +108,19 @@
fraction: 1,000
}
],
- Data:
+ Data:
}
```
-The 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.
+The 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.
5. Staying in Chain State, select "nft" - "colection"
6. Input "2" and click "+"
-7. Observe the following structure returned. It tells that collection is ReFungible and DecimalPoints field is equal to 4.
+7. Observe the following structure returned. It tells that collection is ReFungible and DecimalPoints field is equal
+ to 4.
```
{
@@ -111,21 +139,25 @@
### Off-Chain Schema to store token image URLs
-This 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.
+This 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.
1. Open the [NFT Wallet](https://uniqueapps.usetech.com/#/nft)
2. Search for collection called "Artwork". Search can be done either by collection name or collection ID (which is 2).
3. Expand "Artwork" collection
-4. Click on the image to zoom in.
+4. Click on the image to zoom in.
5. Right-click on the image and select "Open Image in New Tab"
6. Note the image URL:
+
```
https://ipfs-gateway.usetech.com/ipfs/QmUSv64cUmL2m44QYkUFWmH89qykC8VLPFwjhpeAScjejS/image1.jpg
-```
+```
+
7. Open [Chain State](https://uniqueapps.usetech.com/#/chainstate).
8. Select "nft" - "colection"
9. Input "2" and click "+"
10. Scroll down:
+
```
],
TokenPrefix: ARTu0000,
@@ -135,23 +167,32 @@
UnconfirmedSponsor: 5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM
}
```
-11. 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.
+11. 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.
+
### New economic models
-This feature is best demonstrated when you own a SubstraPunk character. Please proceed with the demonstration, and then come back after you claim one.
+This feature is best demonstrated when you own a SubstraPunk character. Please proceed with the demonstration, and then
+come back after you claim one.
-1. 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.
+1. 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.
2. Open the [NFT Wallet](https://uniqueapps.usetech.com/#/nft)
3. Search for collection called "SubstraPunks" (partial search also works, so you can just type "punk" and hit Enter)
4. Add collection
5. Expand substrapunks collection and transfer your PNK token to your "ZERO BALANCE" address.
-6. Now select your "ZERO BALANCE" address in the drop-down list and repeat searching and adding the collection for this address.
+6. Now select your "ZERO BALANCE" address in the drop-down list and repeat searching and adding the collection for this
+ address.
7. Transfer token back to your main address. Observe that despite the zero balance, the transfer is successful.
### White Lists and Public Mint Permission
-We 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:
+We 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:
[white_lists](https://github.com/usetech-llc/nft_parachain/blob/b7c59f0085ed2bc1922e937adf68ef4174a8ba36/pallets/nft/src/lib.rs#L659)
@@ -163,17 +204,18 @@
All features of the NFT Wallet were already demonstrated previously:
-* Enables adding favorite collections
-* Shows tokens owned
-* Allows NFT and Re-Fungible transfers
-* Shows Re-Fungible Balances
-* Shows Token Images
+- Enables adding favorite collections
+- Shows tokens owned
+- Allows NFT and Re-Fungible transfers
+- Shows Re-Fungible Balances
+- Shows Token Images
## Our version of AppsUI
The UI was also mainly demonstrated. We only need to show some configuration that happens behind the scenes:
### Loads appropriate custom API types
+
1. Open the [NFT Wallet Developer Settings](https://uniqueapps.usetech.com/#/settings/developer)
2. Observe that Custom UI types are already in place
@@ -190,31 +232,41 @@
In order to see Unity Asset in action, please follow the instructions in this README file:
[Demonstration](https://github.com/usetech-llc/nft_unity/blob/master/src/DemoApplication/readme.md)
-
## SubstraPunks Game
[Project Description](https://github.com/usetech-llc/substrapunks/blob/master/README.md)
-First, you will need some Unique balance. [This section](#getting-some-unique-tokens) tells how to get the TestNet currency.
+First, you will need some Unique balance. [This section](#getting-some-unique-tokens) tells how to get the TestNet
+currency.
-1. Open this [SubstraPunks Game](https://ipfs-gateway.usetech.com/ipns/QmaMtDqE9nhMX9RQLTpaCboqg7bqkb6Gi67iCKMe8NDpCE/) IPFS link
-2. Find a character you like that still has red background. Red indicates that the character was not yet claimed by anyone.
+1. Open this [SubstraPunks Game](https://ipfs-gateway.usetech.com/ipns/QmaMtDqE9nhMX9RQLTpaCboqg7bqkb6Gi67iCKMe8NDpCE/)
+ IPFS link
+2. Find a character you like that still has red background. Red indicates that the character was not yet claimed by
+ anyone.
3. Allow access for Polkadot{.js} to this site
4. Click "Claim" button
5. Select the account that has some Unique balance
-6. Click "Claim" again, sign transaction, and wait until it mines.
+6. Click "Claim" again, sign transaction, and wait until it mines.
-Let'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)
+Let'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)
-The 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.
+The 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.
+
+Here is how Claiming works: Claim Contract owns all characters in the game initially. Player sends a transaction calling
+the method claim:
-Here is how Claiming works: Claim Contract owns all characters in the game initially. Player sends a transaction calling the method claim:
```
fn claim(&mut self, collection_id: u64, item_id: u64, new_owner: AccountId)
```
Inside the method `claim` there is a call made to NFT pallet to transfer the token that is being claimed to the player:
+
```
runtime_calls::transfer(collection_id, item_id, new_owner);
```
@@ -222,6 +274,5 @@
7. Follow the link to [NFT Wallet](https://uniqueapps.usetech.com/#/nft)
8. Search for "SubstraPunks" collection, add it
9. Expand the collection and find your character in there!
-10. 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.
-
-
+10. 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.
node/Cargo.tomldiffbeforeafterboth--- a/node/Cargo.toml
+++ b/node/Cargo.toml
@@ -12,9 +12,9 @@
description = 'Substrate node nft'
edition = '2018'
homepage = 'https://usetech.com/blockchain'
-license = 'Unlicense'
+license = 'All Rights Reserved'
name = 'nft'
-repository = 'https://github.com/substrate-developer-hub/nft/'
+repository = 'https://github.com/usetech-llc/nft_private/'
version = '3.0.0'
[package.metadata.docs.rs]
@@ -29,7 +29,7 @@
jsonrpc-core = '15.0.0'
# Substrate dependencies
-nft-runtime = { path = '../runtime', version = '3.0.0' }
+nft-runtime = { path = '../runtime', version = '2.1.0' }
frame-benchmarking = {version = '2.0.1', git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique'}
frame-benchmarking-cli = {version = '2.0.1', git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique'}
pallet-transaction-payment-rpc = {version = '2.0.1', git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique'}
node/src/command.rsdiffbeforeafterboth--- a/node/src/command.rs
+++ b/node/src/command.rs
@@ -23,7 +23,7 @@
impl SubstrateCli for Cli {
fn impl_name() -> String {
- "Substrate Node".into()
+ "Unique Node".into()
}
fn impl_version() -> String {
pallets/nft/Cargo.tomldiffbeforeafterboth--- a/pallets/nft/Cargo.toml
+++ b/pallets/nft/Cargo.toml
@@ -6,7 +6,7 @@
license = 'Unlicense'
name = 'pallet-nft'
repository = 'https://github.com/substrate-developer-hub/nft/'
-version = '3.0.0'
+version = '2.1.0'
[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
pallets/nft/src/lib.rsdiffbeforeafterboth--- a/pallets/nft/src/lib.rs
+++ b/pallets/nft/src/lib.rs
@@ -198,7 +198,7 @@
Self {
account_token_ownership_limit: 10_000_000,
token_limit: u32::max_value(),
- sponsored_data_size: u32::max_value(),
+ sponsored_data_size: u32::MAX,
sponsored_data_rate_limit: None,
sponsor_transfer_timeout: 14400,
owner_can_transfer: true,
@@ -388,6 +388,8 @@
AccountTokenLimitExceeded,
/// Collection limit bounds per collection exceeded
CollectionLimitBoundsExceeded,
+ /// Tried to enable permissions which are only permitted to be disabled
+ OwnerPermissionsCantBeReverted,
/// Schema data size limit bound exceeded
SchemaDataLimitExceeded,
/// Maximum refungibility exceeded
@@ -504,7 +506,9 @@
/// * collection_id: Id of the collection where item was created.
///
/// * item_id: Id of an item. Unique within the collection.
- ItemCreated(CollectionId, TokenId),
+ ///
+ /// * recipient: Owner of newly created item
+ ItemCreated(CollectionId, TokenId, AccountId),
/// Collection item was burned.
///
@@ -514,6 +518,19 @@
///
/// item_id: Identifier of burned NFT.
ItemDestroyed(CollectionId, TokenId),
+
+ /// Item was transferred
+ ///
+ /// * collection_id: Id of collection to which item is belong
+ ///
+ /// * item_id: Id of an item
+ ///
+ /// * sender: Original owner of item
+ ///
+ /// * recipient: New owner of item
+ ///
+ /// * amount: Always 1 for NFT
+ Transfer(CollectionId, TokenId, AccountId, AccountId, u128),
}
);
@@ -568,8 +585,10 @@
_ => 0
};
+ let chain_limit = ChainLimit::get();
+
// bound Total number of collections
- ensure!(CollectionCount::get() < ChainLimit::get().collection_numbers_limit, Error::<T>::TotalCollectionsLimitExceeded);
+ ensure!(CollectionCount::get() < chain_limit.collection_numbers_limit, Error::<T>::TotalCollectionsLimitExceeded);
// check params
ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);
@@ -590,6 +609,11 @@
CreatedCollectionCount::put(next_id);
CollectionCount::put(total);
+ let limits = CollectionLimits {
+ sponsored_data_size: chain_limit.custom_data_limit,
+ ..Default::default()
+ };
+
// Create new collection
let new_collection = CollectionType {
owner: who.clone(),
@@ -606,7 +630,7 @@
sponsor_confirmed: false,
variable_on_chain_schema: Vec::new(),
const_on_chain_schema: Vec::new(),
- limits: CollectionLimits::default()
+ limits,
};
// Add new collection to map
@@ -633,6 +657,11 @@
let sender = ensure_signed(origin)?;
Self::check_owner_permissions(collection_id, sender)?;
+ let target_collection = <Collection<T>>::get(collection_id);
+ if !target_collection.limits.owner_can_destroy {
+ fail!(Error::<T>::NoPermission);
+ }
+
<AddressTokens<T>>::remove_prefix(collection_id);
<Allowances<T>>::remove_prefix(collection_id);
<Balance<T>>::remove_prefix(collection_id);
@@ -1015,9 +1044,14 @@
// Transfer permissions check
let target_collection = <Collection<T>>::get(collection_id).unwrap();
- ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||
- Self::is_owner_or_admin_permissions(collection_id, sender.clone()),
- Error::<T>::NoPermission);
+ ensure!(
+ Self::is_item_owner(sender.clone(), collection_id, item_id) ||
+ (
+ target_collection.limits.owner_can_transfer &&
+ Self::is_owner_or_admin_permissions(collection_id, sender.clone())
+ ),
+ Error::<T>::NoPermission
+ );
if target_collection.access == AccessMode::WhiteList {
Self::check_white_list(collection_id, &sender)?;
@@ -1091,9 +1125,23 @@
// Transfer permissions check
let target_collection = <Collection<T>>::get(collection_id).unwrap();
- ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||
- Self::is_owner_or_admin_permissions(collection_id, sender.clone()),
- Error::<T>::NoPermission);
+ let allowance_limit = if (
+ target_collection.limits.owner_can_transfer &&
+ Self::is_owner_or_admin_permissions(
+ collection_id,
+ sender.clone(),
+ )
+ ) {
+ None
+ } else if let Some(amount) = Self::owned_amount(
+ sender.clone(),
+ collection_id,
+ item_id,
+ ) {
+ Some(amount)
+ } else {
+ fail!(Error::<T>::NoPermission);
+ };
if target_collection.access == AccessMode::WhiteList {
Self::check_white_list(collection_id, &sender)?;
@@ -1105,6 +1153,9 @@
if allowance_exists {
allowance += <Allowances<T>>::get(collection_id, (item_id, &sender, &spender));
}
+ if let Some(limit) = allowance_limit {
+ ensure!(limit >= allowance, Error::<T>::TokenValueTooLow);
+ }
<Allowances<T>>::insert(collection_id, (item_id, sender.clone(), spender.clone()), allowance);
Ok(())
@@ -1149,8 +1200,14 @@
Self::is_correct_transfer(collection_id, &target_collection, &recipient)?;
// Transfer permissions check
- ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()),
- Error::<T>::NoPermission);
+ ensure!(
+ appoved_transfer ||
+ (
+ target_collection.limits.owner_can_transfer &&
+ Self::is_owner_or_admin_permissions(collection_id, sender.clone())
+ ),
+ Error::<T>::NoPermission
+ );
if target_collection.access == AccessMode::WhiteList {
Self::check_white_list(collection_id, &sender)?;
@@ -1517,25 +1574,31 @@
pub fn set_collection_limits(
origin,
collection_id: u32,
- limits: CollectionLimits<T::BlockNumber>,
+ new_limits: CollectionLimits<T::BlockNumber>,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
Self::check_owner_permissions(collection_id, sender.clone())?;
let mut target_collection = <Collection<T>>::get(collection_id).unwrap();
+ let old_limits = target_collection.limits;
let chain_limits = ChainLimit::get();
- let climits = target_collection.limits;
// collection bounds
- ensure!(limits.sponsor_transfer_timeout <= MAX_SPONSOR_TIMEOUT &&
- limits.account_token_ownership_limit <= MAX_TOKEN_OWNERSHIP,
+ ensure!(new_limits.sponsor_transfer_timeout <= MAX_SPONSOR_TIMEOUT &&
+ new_limits.account_token_ownership_limit <= MAX_TOKEN_OWNERSHIP &&
+ new_limits.sponsored_data_size <= chain_limits.custom_data_limit,
Error::<T>::CollectionLimitBoundsExceeded);
// token_limit check prev
- ensure!(climits.token_limit > limits.token_limit &&
- limits.token_limit <= chain_limits.account_token_ownership_limit,
- Error::<T>::AccountTokenLimitExceeded);
+ ensure!(old_limits.token_limit >= new_limits.token_limit, Error::<T>::CollectionTokenLimitExceeded);
+ ensure!(new_limits.token_limit > 0, Error::<T>::CollectionTokenLimitExceeded);
+
+ ensure!(
+ (old_limits.owner_can_transfer || !new_limits.owner_can_transfer) &&
+ (old_limits.owner_can_destroy || !new_limits.owner_can_destroy),
+ Error::<T>::OwnerPermissionsCantBeReverted,
+ );
- target_collection.limits = limits;
+ target_collection.limits = new_limits;
<Collection<T>>::insert(collection_id, target_collection);
Ok(())
@@ -1564,12 +1627,14 @@
match target_collection.mode
{
- CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient)?,
+ CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient.clone())?,
CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &sender, &recipient)?,
- CollectionMode::ReFungible => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient)?,
+ CollectionMode::ReFungible => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient.clone())?,
_ => ()
};
+ Self::deposit_event(RawEvent::Transfer(collection_id, item_id, sender, recipient, value));
+
Ok(())
}
@@ -1643,7 +1708,7 @@
{
CreateItemData::NFT(data) => {
let item = NftItemType {
- owner,
+ owner: owner.clone(),
const_data: data.const_data,
variable_data: data.variable_data
};
@@ -1668,7 +1733,7 @@
};
// call event
- Self::deposit_event(RawEvent::ItemCreated(collection_id, <ItemListIndex>::get(collection_id)));
+ Self::deposit_event(RawEvent::ItemCreated(collection_id, <ItemListIndex>::get(collection_id), owner));
Ok(())
}
@@ -1876,6 +1941,36 @@
Ok(())
}
+ fn owned_amount(
+ subject: T::AccountId,
+ collection_id: CollectionId,
+ item_id: TokenId,
+ ) -> Option<u128> {
+ let target_collection = <Collection<T>>::get(collection_id);
+
+ match target_collection.mode {
+ CollectionMode::NFT => {
+ if <NftItemList<T>>::get(collection_id, item_id).owner == subject {
+ return Some(1)
+ }
+ None
+ },
+ CollectionMode::Fungible(_) => {
+ if <FungibleItemList<T>>::contains_key(collection_id, &subject) {
+ return Some(<FungibleItemList<T>>::get(collection_id, &subject)
+ .value);
+ }
+ None
+ },
+ CollectionMode::ReFungible => <ReFungibleItemList<T>>::get(collection_id, item_id)
+ .owner
+ .iter()
+ .find(|i| i.owner == subject)
+ .map(|i| i.fraction),
+ CollectionMode::Invalid => None,
+ }
+ }
+
fn is_item_owner(subject: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> bool {
let target_collection = <Collection<T>>::get(collection_id).unwrap();
runtime/Cargo.tomldiffbeforeafterboth--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -1,11 +1,11 @@
[package]
-authors = ['Substrate DevHub <https://github.com/substrate-developer-hub>']
+authors = ['UseTech Professional <https://usetech.com/blockchain>']
edition = '2018'
homepage = 'https://substrate.io'
-license = 'Unlicense'
+license = 'All Rights Reserved'
name = 'nft-runtime'
repository = 'https://github.com/usetech-llc/nft_parachain/'
-version = '3.0.0'
+version = '2.1.0'
[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
@@ -25,7 +25,7 @@
serde = { features = ['derive'], optional = true, version = '1.0.101' }
# local dependencies
-pallet-nft = { path = '../pallets/nft', default-features = false, version = '3.0.0' }
+pallet-nft = { path = '../pallets/nft', default-features = false, version = '2.1.0' }
# Substrate dependencies
frame-benchmarking = { default-features = false, optional = true, version = '2.0.1' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'unique' }
tests/package.jsondiffbeforeafterboth--- a/tests/package.json
+++ b/tests/package.json
@@ -29,6 +29,7 @@
"testRemoveFromWhiteList": "mocha --timeout 9999999 -r ts-node/register ./**/removeFromWhiteList.test.ts",
"testConnection": "mocha --timeout 9999999 -r ts-node/register ./**/connection.test.ts",
"testCollection": "mocha --timeout 9999999 -r ts-node/register ./**/createCollection.test.ts",
+ "testCreateItem": "mocha --timeout 9999999 -r ts-node/register ./**/createItem.test.ts",
"testCreateMultipleItems": "mocha --timeout 9999999 -r ts-node/register ./**/createMultipleItems.test.ts",
"testApprove": "mocha --timeout 9999999 -r ts-node/register ./**/approve.test.ts",
"testTransferFrom": "mocha --timeout 9999999 -r ts-node/register ./**/transferFrom.test.ts",
tests/src/approve.test.tsdiffbeforeafterboth--- a/tests/src/approve.test.ts
+++ b/tests/src/approve.test.ts
@@ -2,8 +2,8 @@
// This file is subject to the terms and conditions defined in
// file 'LICENSE', which is part of this source code package.
//
+import { IKeyringPair } from '@polkadot/types/types';
import { ApiPromise } from '@polkadot/api';
-import BN from 'bn.js';
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import privateKey from './substrate/privateKey';
@@ -12,66 +12,85 @@
approveExpectFail,
approveExpectSuccess,
createCollectionExpectSuccess,
- createFungibleItemExpectSuccess,
createItemExpectSuccess,
destroyCollectionExpectSuccess,
- transferFromExpectSuccess,
- U128_MAX,
+ setCollectionLimitsExpectSuccess,
+ transferExpectSuccess,
} from './util/helpers';
chai.use(chaiAsPromised);
-const expect = chai.expect;
describe('Integration Test approve(spender, collection_id, item_id, amount):', () => {
+ let Alice: IKeyringPair;
+ let Bob: IKeyringPair;
+ let Charlie: IKeyringPair;
+
+ before(async () => {
+ await usingApi(async () => {
+ Alice = privateKey('//Alice');
+ Bob = privateKey('//Bob');
+ Charlie = privateKey('//Charlie');
+ });
+ });
+
it('Execute the extrinsic and check approvedList', async () => {
- await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const nftCollectionId = await createCollectionExpectSuccess();
- // nft
- const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
- await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob);
- // fungible
- const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
- const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');
- await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob);
- // reFungible
- const reFungibleCollectionId =
- await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
- const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');
- await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob);
- });
+ const nftCollectionId = await createCollectionExpectSuccess();
+ // nft
+ const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
+ await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob);
+ // fungible
+ const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+ const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');
+ await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob);
+ // reFungible
+ const reFungibleCollectionId =
+ await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+ const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');
+ await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob);
});
it('Remove approval by using 0 amount', async () => {
- await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const nftCollectionId = await createCollectionExpectSuccess();
- // nft
- const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
- await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1);
- await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 0);
- // fungible
- const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
- const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');
- await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 1);
- await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 0);
- // reFungible
- const reFungibleCollectionId =
- await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
- const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');
- await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 1);
- await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 0);
- });
+ const nftCollectionId = await createCollectionExpectSuccess();
+ // nft
+ const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
+ await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1);
+ await approveExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 0);
+ // fungible
+ const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+ const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');
+ await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 1);
+ await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob, 0);
+ // reFungible
+ const reFungibleCollectionId =
+ await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+ const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');
+ await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 1);
+ await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob, 0);
+ });
+
+ it('can be called by collection owner on non-owned item when OwnerCanTransfer == true', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ const itemId = await createItemExpectSuccess(Alice, collectionId, 'NFT', Bob.address);
+
+ await approveExpectSuccess(collectionId, itemId, Alice, Charlie);
});
});
describe('Negative Integration Test approve(spender, collection_id, item_id, amount):', () => {
+ let Alice: IKeyringPair;
+ let Bob: IKeyringPair;
+ let Charlie: IKeyringPair;
+
+ before(async () => {
+ await usingApi(async (api) => {
+ Alice = privateKey('//Alice');
+ Bob = privateKey('//Bob');
+ Charlie = privateKey('//Charlie');
+ });
+ });
+
it('Approve for a collection that does not exist', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
// nft
const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number;
await approveExpectFail(nftCollectionCount + 1, 1, Alice, Bob);
@@ -85,59 +104,79 @@
});
it('Approve for a collection that was destroyed', async () => {
- await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- // nft
- const nftCollectionId = await createCollectionExpectSuccess();
- await destroyCollectionExpectSuccess(nftCollectionId);
- await approveExpectFail(nftCollectionId, 1, Alice, Bob);
- // fungible
- const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
- await destroyCollectionExpectSuccess(fungibleCollectionId);
- await approveExpectFail(fungibleCollectionId, 1, Alice, Bob);
- // reFungible
- const reFungibleCollectionId =
- await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
- await destroyCollectionExpectSuccess(reFungibleCollectionId);
- await approveExpectFail(reFungibleCollectionId, 1, Alice, Bob);
- });
+ // nft
+ const nftCollectionId = await createCollectionExpectSuccess();
+ await destroyCollectionExpectSuccess(nftCollectionId);
+ await approveExpectFail(nftCollectionId, 1, Alice, Bob);
+ // fungible
+ const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+ await destroyCollectionExpectSuccess(fungibleCollectionId);
+ await approveExpectFail(fungibleCollectionId, 1, Alice, Bob);
+ // reFungible
+ const reFungibleCollectionId =
+ await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+ await destroyCollectionExpectSuccess(reFungibleCollectionId);
+ await approveExpectFail(reFungibleCollectionId, 1, Alice, Bob);
});
it('Approve transfer of a token that does not exist', async () => {
- await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- // nft
- const nftCollectionId = await createCollectionExpectSuccess();
- await approveExpectFail(nftCollectionId, 2, Alice, Bob);
- // fungible
- const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
- await approveExpectFail(fungibleCollectionId, 2, Alice, Bob);
- // reFungible
- const reFungibleCollectionId =
- await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
- await approveExpectFail(reFungibleCollectionId, 2, Alice, Bob);
- });
+ // nft
+ const nftCollectionId = await createCollectionExpectSuccess();
+ await approveExpectFail(nftCollectionId, 2, Alice, Bob);
+ // fungible
+ const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+ await approveExpectFail(fungibleCollectionId, 2, Alice, Bob);
+ // reFungible
+ const reFungibleCollectionId =
+ await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+ await approveExpectFail(reFungibleCollectionId, 2, Alice, Bob);
});
it('Approve using the address that does not own the approved token', async () => {
- await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const nftCollectionId = await createCollectionExpectSuccess();
- // nft
- const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
- await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice);
- // fungible
- const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
- const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');
- await approveExpectFail(fungibleCollectionId, newFungibleTokenId, Bob, Alice);
- // reFungible
- const reFungibleCollectionId =
- await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
- const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');
- await approveExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice);
- });
+ const nftCollectionId = await createCollectionExpectSuccess();
+ // nft
+ const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
+ await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice);
+ // fungible
+ const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+ const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');
+ await approveExpectFail(fungibleCollectionId, newFungibleTokenId, Bob, Alice);
+ // reFungible
+ const reFungibleCollectionId =
+ await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+ const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');
+ await approveExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice);
+ });
+
+ it('should fail if approved more NFTs than owned', async () => {
+ const nftCollectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+ const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
+ await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1, 'NFT');
+ await approveExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice);
+ await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice);
+ });
+
+ it('should fail if approved more ReFungibles than owned', async () => {
+ const nftCollectionId = await createCollectionExpectSuccess({ mode: { type: 'ReFungible' } });
+ const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'ReFungible');
+ await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 100, 'ReFungible');
+ await approveExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice, 100);
+ await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice, 1);
+ });
+
+ it('should fail if approved more Fungibles than owned', async () => {
+ const nftCollectionId = await createCollectionExpectSuccess({ mode: { type: 'Fungible', decimalPoints: 0 } });
+ const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'Fungible');
+ await transferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 10, 'Fungible');
+ await approveExpectSuccess(nftCollectionId, newNftTokenId, Bob, Alice, 10);
+ await approveExpectFail(nftCollectionId, newNftTokenId, Bob, Alice, 1);
+ });
+
+ it('fails when called by collection owner on non-owned item when OwnerCanTransfer == false', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ const itemId = await createItemExpectSuccess(Alice, collectionId, 'NFT', Bob.address);
+ await setCollectionLimitsExpectSuccess(Alice, collectionId, { OwnerCanTransfer: false });
+
+ await approveExpectFail(collectionId, itemId, Alice, Charlie);
});
});
tests/src/destroyCollection.test.tsdiffbeforeafterboth--- a/tests/src/destroyCollection.test.ts
+++ b/tests/src/destroyCollection.test.ts
@@ -3,10 +3,12 @@
// file 'LICENSE', which is part of this source code package.
//
+import { IKeyringPair } from '@polkadot/types/types';
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
+import privateKey from './substrate/privateKey';
import { default as usingApi } from "./substrate/substrate-api";
-import { createCollectionExpectSuccess, destroyCollectionExpectSuccess, destroyCollectionExpectFailure } from "./util/helpers";
+import { createCollectionExpectSuccess, destroyCollectionExpectSuccess, destroyCollectionExpectFailure, setCollectionLimitsExpectSuccess } from "./util/helpers";
chai.use(chaiAsPromised);
@@ -26,6 +28,14 @@
});
describe('(!negative test!) integration test: ext. destroyCollection():', () => {
+ let alice: IKeyringPair;
+
+ before(async () => {
+ await usingApi(async (api) => {
+ alice = privateKey('//Alice');
+ });
+ });
+
it('(!negative test!) Destroy a collection that never existed', async () => {
await usingApi(async (api) => {
// Find the collection that never existed
@@ -43,4 +53,10 @@
await destroyCollectionExpectFailure(collectionId, '//Bob');
await destroyCollectionExpectSuccess(collectionId, '//Alice');
});
+ it('fails when OwnerCanDestroy == false', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionLimitsExpectSuccess(alice, collectionId, { OwnerCanDestroy: false });
+
+ await destroyCollectionExpectFailure(collectionId, '//Alice');
+ });
});
tests/src/setCollectionLimits.test.tsdiffbeforeafterboth--- a/tests/src/setCollectionLimits.test.ts
+++ b/tests/src/setCollectionLimits.test.ts
@@ -14,6 +14,8 @@
createCollectionExpectSuccess, getCreatedCollectionCount,
getCreateItemResult,
getDetailedCollectionInfo,
+ setCollectionLimitsExpectFailure,
+ setCollectionLimitsExpectSuccess,
} from './util/helpers';
chai.use(chaiAsPromised);
@@ -26,21 +28,8 @@
const accountTokenOwnershipLimit = 0;
const sponsoredDataSize = 0;
const sponsoredMintSize = 0;
-const tokenLimit = 0;
-
-describe('hooks', () => {
- before(async () => {
- await usingApi(async () => {
- const keyring = new Keyring({ type: 'sr25519' });
- alice = keyring.addFromUri('//Alice');
- });
- });
- it('choose or create collection for testing', async () => {
- await usingApi(async () => {
- collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}});
- });
- });
-});
+const sponsorTimeout = 1;
+const tokenLimit = 1;
describe('setCollectionLimits positive', () => {
let tx;
@@ -48,6 +37,7 @@
await usingApi(async () => {
const keyring = new Keyring({ type: 'sr25519' });
alice = keyring.addFromUri('//Alice');
+ collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}});
});
});
it('execute setCollectionLimits with predefined params ', async () => {
@@ -55,25 +45,28 @@
tx = api.tx.nft.setCollectionLimits(
collectionIdForTesting,
{
- accountTokenOwnershipLimit,
- sponsoredDataSize,
- sponsoredMintSize,
- tokenLimit,
+ AccountTokenOwnershipLimit: accountTokenOwnershipLimit,
+ SponsoredMintSize: sponsoredDataSize,
+ TokenLimit: tokenLimit,
+ SponsorTimeout: sponsorTimeout,
+ OwnerCanTransfer: true,
+ OwnerCanDestroy: true
},
);
const events = await submitTransactionAsync(alice, tx);
const result = getCreateItemResult(events);
+
+ // get collection limits defined previously
+ const collectionInfo = await getDetailedCollectionInfo(api, collectionIdForTesting) as ICollectionInterface;
+
// tslint:disable-next-line:no-unused-expression
expect(result.success).to.be.true;
- });
- });
- it('get collection limits defined in previous test', async () => {
- await usingApi(async (api: ApiPromise) => {
- const collectionInfo = await getDetailedCollectionInfo(api, collectionIdForTesting) as ICollectionInterface;
expect(collectionInfo.Limits.AccountTokenOwnershipLimit.toNumber()).to.be.equal(accountTokenOwnershipLimit);
- expect(collectionInfo.Limits.SponsoredMintSize.toNumber()).to.be.equal(sponsoredMintSize);
+ expect(collectionInfo.Limits.SponsoredMintSize.toNumber()).to.be.equal(sponsoredDataSize);
expect(collectionInfo.Limits.TokenLimit.toNumber()).to.be.equal(tokenLimit);
- expect(collectionInfo.Limits.SponsorTimeout.toNumber()).to.be.equal(sponsoredDataSize);
+ expect(collectionInfo.Limits.SponsorTimeout.toNumber()).to.be.equal(sponsorTimeout);
+ expect(collectionInfo.Limits.OwnerCanTransfer.valueOf()).to.be.true;
+ expect(collectionInfo.Limits.OwnerCanDestroy.valueOf()).to.be.true;
});
});
});
@@ -85,6 +78,7 @@
const keyring = new Keyring({ type: 'sr25519' });
alice = keyring.addFromUri('//Alice');
bob = keyring.addFromUri('//Bob');
+ collectionIdForTesting = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}});
});
});
it('execute setCollectionLimits for not exists collection', async () => {
@@ -131,4 +125,44 @@
await expect(submitTransactionExpectFailAsync(alice, tx)).to.be.rejected;
});
});
+
+ it('fails when trying to enable OwnerCanTransfer after it was disabled', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionLimitsExpectSuccess(alice, collectionId, {
+ AccountTokenOwnershipLimit: accountTokenOwnershipLimit,
+ SponsoredMintSize: sponsoredDataSize,
+ TokenLimit: tokenLimit,
+ SponsorTimeout: sponsorTimeout,
+ OwnerCanTransfer: false,
+ OwnerCanDestroy: true
+ });
+ await setCollectionLimitsExpectFailure(alice, collectionId, {
+ AccountTokenOwnershipLimit: accountTokenOwnershipLimit,
+ SponsoredMintSize: sponsoredDataSize,
+ TokenLimit: tokenLimit,
+ SponsorTimeout: sponsorTimeout,
+ OwnerCanTransfer: true,
+ OwnerCanDestroy: true
+ });
+ });
+
+ it('fails when trying to enable OwnerCanDestroy after it was disabled', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionLimitsExpectSuccess(alice, collectionId, {
+ AccountTokenOwnershipLimit: accountTokenOwnershipLimit,
+ SponsoredMintSize: sponsoredDataSize,
+ TokenLimit: tokenLimit,
+ SponsorTimeout: sponsorTimeout,
+ OwnerCanTransfer: true,
+ OwnerCanDestroy: false
+ });
+ await setCollectionLimitsExpectFailure(alice, collectionId, {
+ AccountTokenOwnershipLimit: accountTokenOwnershipLimit,
+ SponsoredMintSize: sponsoredDataSize,
+ TokenLimit: tokenLimit,
+ SponsorTimeout: sponsorTimeout,
+ OwnerCanTransfer: true,
+ OwnerCanDestroy: true
+ });
+ });
});
tests/src/transferFrom.test.tsdiffbeforeafterboth--- a/tests/src/transferFrom.test.ts
+++ b/tests/src/transferFrom.test.ts
@@ -3,6 +3,7 @@
// file 'LICENSE', which is part of this source code package.
//
import { ApiPromise } from '@polkadot/api';
+import { IKeyringPair } from '@polkadot/types/types';
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import privateKey from './substrate/privateKey';
@@ -18,17 +19,27 @@
transferFromExpectFail,
transferFromExpectSuccess,
burnItemExpectSuccess,
+ setCollectionLimitsExpectSuccess,
} from './util/helpers';
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => {
+ let Alice: IKeyringPair;
+ let Bob: IKeyringPair;
+ let Charlie: IKeyringPair;
+
+ before(async () => {
+ await usingApi(async (api) => {
+ Alice = privateKey('//Alice');
+ Bob = privateKey('//Bob');
+ Charlie = privateKey('//Charlie');
+ });
+ });
+
it('Execute the extrinsic and check nftItemList - owner of token', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
// nft
const nftCollectionId = await createCollectionExpectSuccess();
const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
@@ -66,14 +77,30 @@
expect((await getAllowance(fungibleCollectionId, newFungibleTokenId, alice.address, bob.address)).toString()).to.equal('0');
});
});
+
+ it('can be called by collection owner on non-owned item when OwnerCanTransfer == true', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ const itemId = await createItemExpectSuccess(Alice, collectionId, 'NFT', Bob.address);
+
+ await transferFromExpectSuccess(collectionId, itemId, Alice, Bob, Charlie);
+ });
});
describe('Negative Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => {
+ let Alice: IKeyringPair;
+ let Bob: IKeyringPair;
+ let Charlie: IKeyringPair;
+
+ before(async () => {
+ await usingApi(async (api) => {
+ Alice = privateKey('//Alice');
+ Bob = privateKey('//Bob');
+ Charlie = privateKey('//Charlie');
+ });
+ });
+
it('transferFrom for a collection that does not exist', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
// nft
const nftCollectionCount = await api.query.nft.createdCollectionCount() as unknown as number;
await approveExpectFail(nftCollectionCount + 1, 1, Alice, Bob);
@@ -113,9 +140,6 @@
it('transferFrom for not approved address', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
// nft
const nftCollectionId = await createCollectionExpectSuccess();
const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
@@ -137,9 +161,6 @@
it('transferFrom incorrect token count', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
// nft
const nftCollectionId = await createCollectionExpectSuccess();
const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
@@ -164,9 +185,6 @@
it('execute transferFrom from account that is not owner of collection', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
const Dave = privateKey('//Dave');
// nft
const nftCollectionId = await createCollectionExpectSuccess();
@@ -206,9 +224,6 @@
});
it( 'transferFrom burnt token before approve NFT', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
// nft
const nftCollectionId = await createCollectionExpectSuccess();
const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
@@ -219,9 +234,6 @@
});
it( 'transferFrom burnt token before approve Fungible', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');
await burnItemExpectSuccess(Alice, fungibleCollectionId, 1, 10);
@@ -232,9 +244,6 @@
});
it( 'transferFrom burnt token before approve ReFungible', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
const reFungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');
await burnItemExpectSuccess(Alice, reFungibleCollectionId, newReFungibleTokenId, 1);
@@ -246,9 +255,6 @@
it( 'transferFrom burnt token after approve NFT', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
// nft
const nftCollectionId = await createCollectionExpectSuccess();
const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
@@ -259,9 +265,6 @@
});
it( 'transferFrom burnt token after approve Fungible', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
const fungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
const newFungibleTokenId = await createItemExpectSuccess(Alice, fungibleCollectionId, 'Fungible');
await approveExpectSuccess(fungibleCollectionId, newFungibleTokenId, Alice, Bob);
@@ -272,9 +275,6 @@
});
it( 'transferFrom burnt token after approve ReFungible', async () => {
await usingApi(async (api: ApiPromise) => {
- const Alice = privateKey('//Alice');
- const Bob = privateKey('//Bob');
- const Charlie = privateKey('//Charlie');
const reFungibleCollectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
const newReFungibleTokenId = await createItemExpectSuccess(Alice, reFungibleCollectionId, 'ReFungible');
await approveExpectSuccess(reFungibleCollectionId, newReFungibleTokenId, Alice, Bob);
@@ -282,5 +282,13 @@
await transferFromExpectFail(reFungibleCollectionId, newReFungibleTokenId, Bob, Alice, Charlie, 1);
});
- });
+ });
+
+ it('fails when called by collection owner on non-owned item when OwnerCanTransfer == false', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ const itemId = await createItemExpectSuccess(Alice, collectionId, 'NFT', Bob.address);
+ await setCollectionLimitsExpectSuccess(Alice, collectionId, { OwnerCanTransfer: false });
+
+ await transferFromExpectFail(collectionId, itemId, Alice, Bob, Charlie);
+ });
});
tests/src/types.tsdiffbeforeafterboth--- a/tests/src/types.ts
+++ b/tests/src/types.ts
@@ -17,6 +17,8 @@
SponsoredMintSize: BN;
TokenLimit: BN;
SponsorTimeout: BN;
+ OwnerCanTransfer: boolean;
+ OwnerCanDestroy: boolean;
};
MintMode: boolean;
Mode: {
tests/src/util/helpers.tsdiffbeforeafterboth1//2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.4//56import { ApiPromise, Keyring } from '@polkadot/api';7import { Enum, Struct } from '@polkadot/types/codec';8import type { AccountId, EventRecord } from '@polkadot/types/interfaces';9import { u128 } from '@polkadot/types/primitive';10import { IKeyringPair } from '@polkadot/types/types';11import { BigNumber } from 'bignumber.js';12import BN from 'bn.js';13import chai from 'chai';14import chaiAsPromised from 'chai-as-promised';15import { alicesPublicKey, nullPublicKey } from '../accounts';16import privateKey from '../substrate/privateKey';17import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from '../substrate/substrate-api';18import { ICollectionInterface } from '../types';19import { hexToStr, strToUTF16, utf16ToStr } from './util';2021chai.use(chaiAsPromised);22const expect = chai.expect;2324export const U128_MAX = (1n << 128n) - 1n;2526type GenericResult = {27 success: boolean,28};2930interface CreateCollectionResult {31 success: boolean;32 collectionId: number;33}3435interface CreateItemResult {36 success: boolean;37 collectionId: number;38 itemId: number;39}4041interface IReFungibleOwner {42 Fraction: BN;43 Owner: number[];44}4546interface ITokenDataType {47 Owner: number[];48 ConstData: number[];49 VariableData: number[];50}5152interface IFungibleTokenDataType {53 Value: BN;54}5556export interface IReFungibleTokenDataType {57 Owner: IReFungibleOwner[];58 ConstData: number[];59 VariableData: number[];60}6162export function getGenericResult(events: EventRecord[]): GenericResult {63 const result: GenericResult = {64 success: false,65 };66 events.forEach(({ phase, event: { data, method, section } }) => {67 // console.log(` ${phase}: ${section}.${method}:: ${data}`);68 if (method === 'ExtrinsicSuccess') {69 result.success = true;70 }71 });72 return result;73}7475export function getCreateCollectionResult(events: EventRecord[]): CreateCollectionResult {76 let success = false;77 let collectionId: number = 0;78 events.forEach(({ phase, event: { data, method, section } }) => {79 // console.log(` ${phase}: ${section}.${method}:: ${data}`);80 if (method == 'ExtrinsicSuccess') {81 success = true;82 } else if ((section == 'nft') && (method == 'Created')) {83 collectionId = parseInt(data[0].toString());84 }85 });86 const result: CreateCollectionResult = {87 success,88 collectionId,89 };90 return result;91}9293export function getCreateItemResult(events: EventRecord[]): CreateItemResult {94 let success = false;95 let collectionId: number = 0;96 let itemId: number = 0;97 events.forEach(({ phase, event: { data, method, section } }) => {98 // console.log(` ${phase}: ${section}.${method}:: ${data}`);99 if (method == 'ExtrinsicSuccess') {100 success = true;101 } else if ((section == 'nft') && (method == 'ItemCreated')) {102 collectionId = parseInt(data[0].toString());103 itemId = parseInt(data[1].toString());104 }105 });106 const result: CreateItemResult = {107 success,108 collectionId,109 itemId,110 };111 return result;112}113114interface Invalid {115 type: 'Invalid';116}117118interface Nft {119 type: 'NFT';120}121122interface Fungible {123 type: 'Fungible';124 decimalPoints: number;125}126127interface ReFungible {128 type: 'ReFungible';129}130131type CollectionMode = Nft | Fungible | ReFungible | Invalid;132133export type CreateCollectionParams = {134 mode: CollectionMode,135 name: string,136 description: string,137 tokenPrefix: string,138};139140const defaultCreateCollectionParams: CreateCollectionParams = {141 description: 'description',142 mode: { type: 'NFT' },143 name: 'name',144 tokenPrefix: 'prefix',145}146147export async function createCollectionExpectSuccess(params: Partial<CreateCollectionParams> = {}): Promise<number> {148 const {name, description, mode, tokenPrefix } = {...defaultCreateCollectionParams, ...params};149150 let collectionId: number = 0;151 await usingApi(async (api) => {152 // Get number of collections before the transaction153 const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString(), 10);154155 // Run the CreateCollection transaction156 const alicePrivateKey = privateKey('//Alice');157158 let modeprm = {};159 if (mode.type === 'NFT') {160 modeprm = {nft: null};161 } else if (mode.type === 'Fungible') {162 modeprm = {fungible: mode.decimalPoints};163 } else if (mode.type === 'ReFungible') {164 modeprm = {refungible: null};165 } else if (mode.type === 'Invalid') {166 modeprm = {invalid: null};167 }168169 const tx = api.tx.nft.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), modeprm);170 const events = await submitTransactionAsync(alicePrivateKey, tx);171 const result = getCreateCollectionResult(events);172173 // Get number of collections after the transaction174 const BcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString(), 10);175176 // Get the collection177 const collection: any = (await api.query.nft.collection(result.collectionId)).toJSON();178179 // What to expect180 // tslint:disable-next-line:no-unused-expression181 expect(result.success).to.be.true;182 expect(result.collectionId).to.be.equal(BcollectionCount);183 // tslint:disable-next-line:no-unused-expression184 expect(collection).to.be.not.null;185 expect(BcollectionCount).to.be.equal(AcollectionCount + 1, 'Error: NFT collection NOT created.');186 expect(collection.Owner).to.be.equal(alicesPublicKey);187 expect(utf16ToStr(collection.Name)).to.be.equal(name);188 expect(utf16ToStr(collection.Description)).to.be.equal(description);189 expect(hexToStr(collection.TokenPrefix)).to.be.equal(tokenPrefix);190191 collectionId = result.collectionId;192 });193194 return collectionId;195}196197export async function createCollectionExpectFailure(params: Partial<CreateCollectionParams> = {}) {198 const {name, description, mode, tokenPrefix } = {...defaultCreateCollectionParams, ...params};199200 let modeprm = {};201 if (mode.type === 'NFT') {202 modeprm = {nft: null};203 } else if (mode.type === 'Fungible') {204 modeprm = {fungible: mode.decimalPoints};205 } else if (mode.type === 'ReFungible') {206 modeprm = {refungible: null};207 } else if (mode.type === 'Invalid') {208 modeprm = {invalid: null};209 }210211 await usingApi(async (api) => {212 // Get number of collections before the transaction213 const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString());214215 // Run the CreateCollection transaction216 const alicePrivateKey = privateKey('//Alice');217 const tx = api.tx.nft.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), modeprm);218 const events = await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;219 const result = getCreateCollectionResult(events);220221 // Get number of collections after the transaction222 const BcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString());223224 // What to expect225 // tslint:disable-next-line:no-unused-expression226 expect(result.success).to.be.false;227 expect(BcollectionCount).to.be.equal(AcollectionCount, 'Error: Collection with incorrect data created.');228 });229}230231export async function findUnusedAddress(api: ApiPromise, seedAddition = ''): Promise<IKeyringPair> {232 let bal = new BigNumber(0);233 let unused;234 do {235 const randomSeed = 'seed' + Math.floor(Math.random() * Math.floor(10000)) + seedAddition;236 const keyring = new Keyring({ type: 'sr25519' });237 unused = keyring.addFromUri(`//${randomSeed}`);238 bal = new BigNumber((await api.query.system.account(unused.address)).data.free.toString());239 } while (bal.toFixed() != '0');240 return unused;241}242243export async function getAllowance(collectionId: number, tokenId: number, owner: string, approved: string) {244 return await usingApi(async (api) => {245 const bn = await api.query.nft.allowances(collectionId, [tokenId, owner, approved]) as unknown as BN;246 return BigInt(bn.toString());247 });248}249250export function findUnusedAddresses(api: ApiPromise, amount: number): Promise<IKeyringPair[]> {251 return Promise.all(new Array(amount).fill(null).map(() => findUnusedAddress(api, '_' + Date.now())));252}253254export async function findNotExistingCollection(api: ApiPromise): Promise<number> {255 const totalNumber = parseInt((await api.query.nft.createdCollectionCount()).toString(), 10) as unknown as number;256 const newCollection: number = totalNumber + 1;257 return newCollection;258}259260function getDestroyResult(events: EventRecord[]): boolean {261 let success: boolean = false;262 events.forEach(({ phase, event: { data, method, section } }) => {263 // console.log(` ${phase}: ${section}.${method}:: ${data}`);264 if (method == 'ExtrinsicSuccess') {265 success = true;266 }267 });268 return success;269}270271export async function destroyCollectionExpectFailure(collectionId: number, senderSeed: string = '//Alice') {272 await usingApi(async (api) => {273 // Run the DestroyCollection transaction274 const alicePrivateKey = privateKey(senderSeed);275 const tx = api.tx.nft.destroyCollection(collectionId);276 await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;277 });278}279280export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {281 await usingApi(async (api) => {282 // Run the DestroyCollection transaction283 const alicePrivateKey = privateKey(senderSeed);284 const tx = api.tx.nft.destroyCollection(collectionId);285 const events = await submitTransactionAsync(alicePrivateKey, tx);286 const result = getDestroyResult(events);287288 // Get the collection289 const collection: any = (await api.query.nft.collection(collectionId)).toJSON();290291 // What to expect292 expect(result).to.be.true;293 expect(collection).to.be.not.null;294 expect(collection.Owner).to.be.equal(nullPublicKey);295 });296}297298export async function queryCollectionLimits(collectionId: number) {299 return await usingApi(async (api) => {300 return ((await api.query.nft.collection(collectionId)).toJSON() as any).Limits;301 });302}303304export async function setCollectionLimitsExpectSuccess(sender: IKeyringPair, collectionId: number, limits: any) {305 await usingApi(async (api) => {306 const oldLimits = await queryCollectionLimits(collectionId);307 const newLimits = { ...oldLimits as any, ...limits };308 const tx = api.tx.nft.setCollectionLimits(collectionId, newLimits);309 const events = await submitTransactionAsync(sender, tx);310 const result = getGenericResult(events);311312 expect(result.success).to.be.true;313 });314}315316export async function setCollectionLimitsExpectFailure(sender: IKeyringPair, collectionId: number, limits: any) {317 await usingApi(async (api) => {318 const oldLimits = await queryCollectionLimits(collectionId);319 const newLimits = { ...oldLimits as any, ...limits };320 const tx = api.tx.nft.setCollectionLimits(collectionId, newLimits);321 const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;322 const result = getGenericResult(events);323324 expect(result.success).to.be.false;325 });326}327328export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string) {329 await usingApi(async (api) => {330331 // Run the transaction332 const alicePrivateKey = privateKey('//Alice');333 const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor);334 const events = await submitTransactionAsync(alicePrivateKey, tx);335 const result = getGenericResult(events);336337 // Get the collection338 const collection: any = (await api.query.nft.collection(collectionId)).toJSON();339340 // What to expect341 expect(result.success).to.be.true;342 expect(collection.Sponsor.toString()).to.be.equal(sponsor.toString());343 expect(collection.SponsorConfirmed).to.be.false;344 });345}346347export async function removeCollectionSponsorExpectSuccess(collectionId: number) {348 await usingApi(async (api) => {349350 // Run the transaction351 const alicePrivateKey = privateKey('//Alice');352 const tx = api.tx.nft.removeCollectionSponsor(collectionId);353 const events = await submitTransactionAsync(alicePrivateKey, tx);354 const result = getGenericResult(events);355356 // Get the collection357 const collection: any = (await api.query.nft.collection(collectionId)).toJSON();358359 // What to expect360 expect(result.success).to.be.true;361 expect(collection.Sponsor).to.be.equal(nullPublicKey);362 expect(collection.SponsorConfirmed).to.be.false;363 });364}365366export async function removeCollectionSponsorExpectFailure(collectionId: number) {367 await usingApi(async (api) => {368369 // Run the transaction370 const alicePrivateKey = privateKey('//Alice');371 const tx = api.tx.nft.removeCollectionSponsor(collectionId);372 await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;373 });374}375376export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed: string = '//Alice') {377 await usingApi(async (api) => {378379 // Run the transaction380 const alicePrivateKey = privateKey(senderSeed);381 const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor);382 await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;383 });384}385386export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {387 await usingApi(async (api) => {388389 // Run the transaction390 const sender = privateKey(senderSeed);391 const tx = api.tx.nft.confirmSponsorship(collectionId);392 const events = await submitTransactionAsync(sender, tx);393 const result = getGenericResult(events);394395 // Get the collection396 const collection: any = (await api.query.nft.collection(collectionId)).toJSON();397398 // What to expect399 expect(result.success).to.be.true;400 expect(collection.Sponsor).to.be.equal(sender.address);401 expect(collection.SponsorConfirmed).to.be.true;402 });403}404405406export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed: string = '//Alice') {407 await usingApi(async (api) => {408409 // Run the transaction410 const sender = privateKey(senderSeed);411 const tx = api.tx.nft.confirmSponsorship(collectionId);412 await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;413 });414}415416export async function enableContractSponsoringExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) {417 await usingApi(async (api) => {418 const tx = api.tx.nft.enableContractSponsoring(contractAddress, enable);419 const events = await submitTransactionAsync(sender, tx);420 const result = getGenericResult(events);421422 expect(result.success).to.be.true;423 });424}425426export async function enableContractSponsoringExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) {427 await usingApi(async (api) => {428 const tx = api.tx.nft.enableContractSponsoring(contractAddress, enable);429 const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;430 const result = getGenericResult(events);431432 expect(result.success).to.be.false;433 });434}435436export async function setContractSponsoringRateLimitExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) {437 await usingApi(async (api) => {438 const tx = api.tx.nft.setContractSponsoringRateLimit(contractAddress, rateLimit);439 const events = await submitTransactionAsync(sender, tx);440 const result = getGenericResult(events);441442 expect(result.success).to.be.true;443 });444}445446export async function setContractSponsoringRateLimitExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) {447 await usingApi(async (api) => {448 const tx = api.tx.nft.setContractSponsoringRateLimit(contractAddress, rateLimit);449 const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;450 const result = getGenericResult(events);451452 expect(result.success).to.be.false;453 });454}455456export async function toggleContractWhitelistExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, enabled: boolean) {457 await usingApi(async (api) => {458 const tx = api.tx.nft.toggleContractWhiteList(contractAddress, true);459 const events = await submitTransactionAsync(sender, tx);460 const result = getGenericResult(events);461462 expect(result.success).to.be.true;463 });464}465466export async function isWhitelistedInContract(contractAddress: AccountId | string, user: string) {467 let whitelisted: boolean = false;468 await usingApi(async (api) => {469 whitelisted = (await api.query.nft.contractWhiteList(contractAddress, user)).toJSON() as boolean;470 });471 return whitelisted;472}473474export async function addToContractWhiteListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: string) {475 await usingApi(async (api) => {476 const tx = api.tx.nft.addToContractWhiteList(contractAddress, user);477 const events = await submitTransactionAsync(sender, tx);478 const result = getGenericResult(events);479480 expect(result.success).to.be.true;481 });482}483484export async function removeFromContractWhiteListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: string) {485 await usingApi(async (api) => {486 const tx = api.tx.nft.removeFromContractWhiteList(contractAddress, user);487 const events = await submitTransactionAsync(sender, tx);488 const result = getGenericResult(events);489490 expect(result.success).to.be.true;491 });492}493494export async function removeFromContractWhiteListExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, user: string) {495 await usingApi(async (api) => {496 const tx = api.tx.nft.removeFromContractWhiteList(contractAddress, user);497 const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;498 const result = getGenericResult(events);499500 expect(result.success).to.be.false;501 });502}503504export async function setVariableMetaDataExpectSuccess(sender: IKeyringPair, collectionId: number, itemId: number, data: number[]) {505 await usingApi(async (api) => {506 const tx = api.tx.nft.setVariableMetaData(collectionId, itemId, '0x' + Buffer.from(data).toString('hex'));507 const events = await submitTransactionAsync(sender, tx);508 const result = getGenericResult(events);509510 expect(result.success).to.be.true;511 });512}513514export async function setVariableMetaDataExpectFailure(sender: IKeyringPair, collectionId: number, itemId: number, data: number[]) {515 await usingApi(async (api) => {516 const tx = api.tx.nft.setVariableMetaData(collectionId, itemId, '0x' + Buffer.from(data).toString('hex'));517 await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;518 });519}520521export async function setVariableMetaDataSponsoringRateLimitExpectSuccess(sender: IKeyringPair, collectionId: number, rateLimit: number) {522 await usingApi(async (api) => {523 const tx = api.tx.nft.setVariableMetaDataSponsoringRateLimit(collectionId, rateLimit);524 const events = await submitTransactionAsync(sender, tx);525 const result = getGenericResult(events);526527 expect(result.success).to.be.true;528 });529}530531export async function setVariableMetaDataSponsoringRateLimitExpectFailure(sender: IKeyringPair, collectionId: number, rateLimit: number) {532 await usingApi(async (api) => {533 const tx = api.tx.nft.setVariableMetaDataSponsoringRateLimit(collectionId, rateLimit);534 const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;535 const result = getGenericResult(events);536537 expect(result.success).to.be.false;538 });539}540541export async function setOffchainSchemaExpectSuccess(sender: IKeyringPair, collectionId: number, data: number[]) {542 await usingApi(async (api) => {543 const tx = api.tx.nft.setOffchainSchema(collectionId, '0x' + Buffer.from(data).toString('hex'));544 const events = await submitTransactionAsync(sender, tx);545 const result = getGenericResult(events);546547 expect(result.success).to.be.true;548 });549}550551export async function setOffchainSchemaExpectFailure(sender: IKeyringPair, collectionId: number, data: number[]) {552 await usingApi(async (api) => {553 const tx = api.tx.nft.setOffchainSchema(collectionId, '0x' + Buffer.from(data).toString('hex'));554 await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;555 });556}557558export interface CreateFungibleData {559 readonly Value: bigint;560}561562export interface CreateReFungibleData { }563export interface CreateNftData { }564565export type CreateItemData = {566 NFT: CreateNftData;567} | {568 Fungible: CreateFungibleData;569} | {570 ReFungible: CreateReFungibleData;571};572573export async function burnItemExpectSuccess(owner: IKeyringPair, collectionId: number, tokenId: number, value = 0) {574 await usingApi(async (api) => {575 const tx = api.tx.nft.burnItem(collectionId, tokenId, value);576 const events = await submitTransactionAsync(owner, tx);577 const result = getGenericResult(events);578 // Get the item579 const item: any = (await api.query.nft.nftItemList(collectionId, tokenId)).toJSON();580 // What to expect581 // tslint:disable-next-line:no-unused-expression582 expect(result.success).to.be.true;583 // tslint:disable-next-line:no-unused-expression584 expect(item).to.be.not.null;585 expect(item.Owner).to.be.equal(nullPublicKey);586 });587}588589export async function590approveExpectSuccess(collectionId: number,591 tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number | bigint = 1) {592 await usingApi(async (api: ApiPromise) => {593 const allowanceBefore =594 await api.query.nft.allowances(collectionId, [tokenId, owner.address, approved.address]) as unknown as BN;595 const approveNftTx = await api.tx.nft.approve(approved.address, collectionId, tokenId, amount);596 const events = await submitTransactionAsync(owner, approveNftTx);597 const result = getCreateItemResult(events);598 // tslint:disable-next-line:no-unused-expression599 expect(result.success).to.be.true;600 const allowanceAfter =601 await api.query.nft.allowances(collectionId, [tokenId, owner.address, approved.address]) as unknown as BN;602 expect(allowanceAfter.sub(allowanceBefore).toString()).to.be.equal(amount.toString());603 });604}605606export async function607transferFromExpectSuccess(collectionId: number,608 tokenId: number,609 accountApproved: IKeyringPair,610 accountFrom: IKeyringPair,611 accountTo: IKeyringPair,612 value: number | bigint = 1,613 type: string = 'NFT') {614 await usingApi(async (api: ApiPromise) => {615 let balanceBefore = new BN(0);616 if (type === 'Fungible') {617 balanceBefore = await api.query.nft.balance(collectionId, accountTo.address) as unknown as BN;618 }619 const transferFromTx = await api.tx.nft.transferFrom(620 accountFrom.address, accountTo.address, collectionId, tokenId, value);621 const events = await submitTransactionAsync(accountApproved, transferFromTx);622 const result = getCreateItemResult(events);623 // tslint:disable-next-line:no-unused-expression624 expect(result.success).to.be.true;625 if (type === 'NFT') {626 const nftItemData = await api.query.nft.nftItemList(collectionId, tokenId) as unknown as ITokenDataType;627 expect(nftItemData.Owner.toString()).to.be.equal(accountTo.address);628 }629 if (type === 'Fungible') {630 const balanceAfter = await api.query.nft.balance(collectionId, accountTo.address) as unknown as BN;631 expect(balanceAfter.sub(balanceBefore).toString()).to.be.equal(value.toString());632 }633 if (type === 'ReFungible') {634 const nftItemData =635 await api.query.nft.reFungibleItemList(collectionId, tokenId) as unknown as IReFungibleTokenDataType;636 expect(nftItemData.Owner[0].Owner.toString()).to.be.equal(accountTo.address);637 expect(nftItemData.Owner[0].Fraction.toNumber()).to.be.equal(value);638 }639 });640}641642export async function643transferFromExpectFail(collectionId: number,644 tokenId: number,645 accountApproved: IKeyringPair,646 accountFrom: IKeyringPair,647 accountTo: IKeyringPair,648 value: number | bigint = 1) {649 await usingApi(async (api: ApiPromise) => {650 const transferFromTx = await api.tx.nft.transferFrom(651 accountFrom.address, accountTo.address, collectionId, tokenId, value);652 const events = await expect(submitTransactionExpectFailAsync(accountApproved, transferFromTx)).to.be.rejected;653 const result = getCreateCollectionResult(events);654 // tslint:disable-next-line:no-unused-expression655 expect(result.success).to.be.false;656 });657}658659export async function660transferExpectSuccess(collectionId: number,661 tokenId: number,662 sender: IKeyringPair,663 recipient: IKeyringPair,664 value: number | bigint = 1,665 type: string = 'NFT') {666 await usingApi(async (api: ApiPromise) => {667 let balanceBefore = new BN(0);668 if (type === 'Fungible') {669 balanceBefore = await api.query.nft.balance(collectionId, recipient.address) as unknown as BN;670 }671 const transferTx = await api.tx.nft.transfer(recipient.address, collectionId, tokenId, value);672 const events = await submitTransactionAsync(sender, transferTx);673 const result = getCreateItemResult(events);674 // tslint:disable-next-line:no-unused-expression675 expect(result.success).to.be.true;676 if (type === 'NFT') {677 const nftItemData = await api.query.nft.nftItemList(collectionId, tokenId) as unknown as ITokenDataType;678 expect(nftItemData.Owner.toString()).to.be.equal(recipient.address);679 }680 if (type === 'Fungible') {681 const balanceAfter = await api.query.nft.balance(collectionId, recipient.address) as unknown as BN;682 expect(balanceAfter.sub(balanceBefore).toString()).to.be.equal(value.toString());683 }684 if (type === 'ReFungible') {685 const nftItemData =686 await api.query.nft.reFungibleItemList(collectionId, tokenId) as unknown as IReFungibleTokenDataType;687 expect(nftItemData.Owner[0].Owner.toString()).to.be.equal(recipient.address);688 expect(nftItemData.Owner[0].Fraction.toNumber()).to.be.equal(value);689 }690 });691}692693export async function694transferExpectFail(collectionId: number,695 tokenId: number,696 sender: IKeyringPair,697 recipient: IKeyringPair,698 value: number | bigint = 1,699 type: string = 'NFT') {700 await usingApi(async (api: ApiPromise) => {701 const transferTx = await api.tx.nft.transfer(recipient.address, collectionId, tokenId, value);702 const events = await expect(submitTransactionExpectFailAsync(sender, transferTx)).to.be.rejected;703 if (events && Array.isArray(events)) {704 const result = getCreateCollectionResult(events);705 // tslint:disable-next-line:no-unused-expression706 expect(result.success).to.be.false;707 }708 });709}710711export async function712approveExpectFail(collectionId: number,713 tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number | bigint = 1) {714 await usingApi(async (api: ApiPromise) => {715 const approveNftTx = await api.tx.nft.approve(approved.address, collectionId, tokenId, amount);716 const events = await expect(submitTransactionExpectFailAsync(owner, approveNftTx)).to.be.rejected;717 const result = getCreateCollectionResult(events);718 // tslint:disable-next-line:no-unused-expression719 expect(result.success).to.be.false;720 });721}722723export async function getFungibleBalance(724 collectionId: number,725 owner: string,726) {727 return await usingApi(async (api) => {728 const response = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON() as unknown as {Value: string};729 return BigInt(response.Value);730 });731}732733export async function createFungibleItemExpectSuccess(734 sender: IKeyringPair,735 collectionId: number,736 data: CreateFungibleData,737 owner: string = sender.address,738) {739 return await usingApi(async (api) => {740 const tx = api.tx.nft.createItem(collectionId, owner, { Fungible: data });741742 const events = await submitTransactionAsync(sender, tx);743 const result = getCreateItemResult(events);744745 expect(result.success).to.be.true;746 return result.itemId;747 });748}749750export async function createItemExpectSuccess(751 sender: IKeyringPair, collectionId: number, createMode: string, owner: string = '') {752 let newItemId: number = 0;753 await usingApi(async (api) => {754 const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString(), 10);755 const Aitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON();756 const AItemBalance = new BigNumber(Aitem.Value);757758 if (owner === '') {759 owner = sender.address;760 }761762 let tx;763 if (createMode === 'Fungible') {764 const createData = {fungible: {value: 10}};765 tx = api.tx.nft.createItem(collectionId, owner, createData);766 } else if (createMode === 'ReFungible') {767 const createData = {refungible: {const_data: [], variable_data: [], pieces: 100}};768 tx = api.tx.nft.createItem(collectionId, owner, createData);769 } else {770 tx = api.tx.nft.createItem(collectionId, owner, createMode);771 }772 const events = await submitTransactionAsync(sender, tx);773 const result = getCreateItemResult(events);774775 const BItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString(), 10);776 const Bitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON();777 const BItemBalance = new BigNumber(Bitem.Value);778779 // What to expect780 // tslint:disable-next-line:no-unused-expression781 expect(result.success).to.be.true;782 if (createMode === 'Fungible') {783 expect(BItemBalance.minus(AItemBalance).toNumber()).to.be.equal(10);784 } else {785 expect(BItemCount).to.be.equal(AItemCount + 1);786 }787 expect(collectionId).to.be.equal(result.collectionId);788 expect(BItemCount).to.be.equal(result.itemId);789 newItemId = result.itemId;790 });791 return newItemId;792}793794export async function createItemExpectFailure(795 sender: IKeyringPair, collectionId: number, createMode: string, owner: string = sender.address) {796 await usingApi(async (api) => {797 const tx = api.tx.nft.createItem(collectionId, owner, createMode);798 const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;799 const result = getCreateItemResult(events);800801 expect(result.success).to.be.false;802 });803}804805export async function setPublicAccessModeExpectSuccess(806 sender: IKeyringPair, collectionId: number,807 accessMode: 'Normal' | 'WhiteList',808) {809 await usingApi(async (api) => {810811 // Run the transaction812 const tx = api.tx.nft.setPublicAccessMode(collectionId, accessMode);813 const events = await submitTransactionAsync(sender, tx);814 const result = getGenericResult(events);815816 // Get the collection817 const collection: any = (await api.query.nft.collection(collectionId)).toJSON();818819 // What to expect820 // tslint:disable-next-line:no-unused-expression821 expect(result.success).to.be.true;822 expect(collection.Access).to.be.equal(accessMode);823 });824}825826export async function enableWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number) {827 await setPublicAccessModeExpectSuccess(sender, collectionId, 'WhiteList');828}829830export async function disableWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number) {831 await setPublicAccessModeExpectSuccess(sender, collectionId, 'Normal');832}833834export async function setMintPermissionExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) {835 await usingApi(async (api) => {836837 // Run the transaction838 const tx = api.tx.nft.setMintPermission(collectionId, enabled);839 const events = await submitTransactionAsync(sender, tx);840 const result = getGenericResult(events);841842 // Get the collection843 const collection: any = (await api.query.nft.collection(collectionId)).toJSON();844845 // What to expect846 // tslint:disable-next-line:no-unused-expression847 expect(result.success).to.be.true;848 expect(collection.MintMode).to.be.equal(enabled);849 });850}851852export async function enablePublicMintingExpectSuccess(sender: IKeyringPair, collectionId: number) {853 await setMintPermissionExpectSuccess(sender, collectionId, true);854}855856export async function setMintPermissionExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) {857 await usingApi(async (api) => {858 // Run the transaction859 const tx = api.tx.nft.setMintPermission(collectionId, enabled);860 const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;861 const result = getCreateCollectionResult(events);862 // tslint:disable-next-line:no-unused-expression863 expect(result.success).to.be.false;864 });865}866867export async function isWhitelisted(collectionId: number, address: string) {868 let whitelisted: boolean = false;869 await usingApi(async (api) => {870 whitelisted = (await api.query.nft.whiteList(collectionId, address)).toJSON() as unknown as boolean;871 });872 return whitelisted;873}874875export async function addToWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string) {876 await usingApi(async (api) => {877878 const whiteListedBefore = (await api.query.nft.whiteList(collectionId, address)).toJSON();879880 // Run the transaction881 const tx = api.tx.nft.addToWhiteList(collectionId, address);882 const events = await submitTransactionAsync(sender, tx);883 const result = getGenericResult(events);884885 const whiteListedAfter = (await api.query.nft.whiteList(collectionId, address)).toJSON();886887 // What to expect888 // tslint:disable-next-line:no-unused-expression889 expect(result.success).to.be.true;890 // tslint:disable-next-line: no-unused-expression891 expect(whiteListedBefore).to.be.false;892 // tslint:disable-next-line: no-unused-expression893 expect(whiteListedAfter).to.be.true;894 });895}896897export async function removeFromWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string) {898 await usingApi(async (api) => {899 // Run the transaction900 const tx = api.tx.nft.removeFromWhiteList(collectionId, address);901 const events = await submitTransactionAsync(sender, tx);902 const result = getGenericResult(events);903904 // What to expect905 // tslint:disable-next-line:no-unused-expression906 expect(result.success).to.be.true;907 });908}909910export async function removeFromWhiteListExpectFailure(sender: IKeyringPair, collectionId: number, address: string) {911 await usingApi(async (api) => {912 // Run the transaction913 const tx = api.tx.nft.removeFromWhiteList(collectionId, address);914 const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;915 const result = getGenericResult(events);916917 // What to expect918 // tslint:disable-next-line:no-unused-expression919 expect(result.success).to.be.false;920 });921}922923export const getDetailedCollectionInfo = async (api: ApiPromise, collectionId: number)924 : Promise<ICollectionInterface | null> => {925 return await api.query.nft.collection(collectionId) as unknown as ICollectionInterface;926};927928export const getCreatedCollectionCount = async (api: ApiPromise): Promise<number> => {929 // set global object - collectionsCount930 return (await api.query.nft.createdCollectionCount() as unknown as BN).toNumber();931};932933export async function queryCollectionExpectSuccess(collectionId: number): Promise<ICollectionInterface> {934 return await usingApi(async (api) => {935 return (await api.query.nft.collection(collectionId)) as unknown as ICollectionInterface;936 });937}