├── .github
└── CODEOWNERS
├── .gitignore
├── LICENSE
├── NOTICE.md
├── README.md
├── contracts
├── AdvancedCatalog.sol
├── MergedEquippable
│ ├── AdvancedEquippable.sol
│ ├── README.md
│ └── SimpleEquippable.sol
├── MultiAsset
│ ├── AdvancedMultiAsset.sol
│ ├── README.md
│ └── SimpleMultiAsset.sol
├── Nestable
│ ├── AdvancedNestable.sol
│ ├── README.md
│ └── SimpleNestable.sol
├── NestableMultiAsset
│ ├── AdvancedNestableMultiAsset.sol
│ ├── README.md
│ └── SimpleNestableMultiAsset.sol
├── README.md
├── SimpleCatalog.sol
└── SplitEquippable
│ ├── AdvancedExternalEquip.sol
│ ├── AdvancedNestableExternalEquip.sol
│ ├── README.md
│ ├── SimpleExternalEquip.sol
│ └── SimpleNestableExternalEquip.sol
├── hardhat.config.ts
├── img
├── 4.jpg
├── 5.jpg
├── 6.jpg
├── 7.jpg
├── 8.jpg
├── 9.jpg
├── RMRKLegoInfo.png
├── RMRKLegoInfographics.png
└── RMRKLegos.png
├── package-lock.json
├── package.json
├── scripts
├── deployEquippable.ts
├── deployMultiAsset.ts
├── deployNestable.ts
├── deployNestableMultiAsset.ts
├── deploySplitEquippable.ts
├── mergedEquippableUserJourney.ts
├── multiAssetUserJourney.ts
├── nestableMultiAssetUserJourney.ts
├── nestableUserJourney.ts
└── splitEquippableUserJourney.ts
└── tsconfig.json
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # The Github accounts listed here will automatically be requested to review PRs.
2 | * @steven2308 @ThunderDeliverer
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | coverage
4 | coverage.json
5 | typechain
6 | typechain-types
7 |
8 | #Hardhat files
9 | cache
10 | artifacts
11 |
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | Copyright 2023 RMRK Team
180 |
181 | Licensed under the Apache License, Version 2.0 (the "License");
182 | you may not use this file except in compliance with the License.
183 | You may obtain a copy of the License at
184 |
185 | http://www.apache.org/licenses/LICENSE-2.0
186 |
187 | Unless required by applicable law or agreed to in writing, software
188 | distributed under the License is distributed on an "AS IS" BASIS,
189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190 | See the License for the specific language governing permissions and
191 | limitations under the License.
--------------------------------------------------------------------------------
/NOTICE.md:
--------------------------------------------------------------------------------
1 | Any derivative software shall both in code and in terminal or
2 | command line output contain the following disclaimer, verbatim
3 |
4 | /***********************************************/
5 | /** Using software by RMRK.app **/
6 | /** contact@rmrk.app **/
7 | /***********************************************/
8 |
9 |
10 | Should RMRK technology be used this must be clearly
11 | stated in an obvious location (e.g. footer), in the following
12 | way: `Using software by RMRK.app | contact@rmrk.app` and if
13 | technically possible, the part “RMRK.app" must be hyperlinked
14 | to https://rmrk.app. Implementers may approach the RMRK team
15 | via contact@rmrk.app for alternative ways of expressing
16 | attribution.
17 |
18 | The above two conditions may be waived on a case by case basis
19 | by obtaining a written whitelabel permission from the RMRK team
20 | via contact through contact@rmrk.app
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RMRK Solidity
2 |
3 | A set of Solidity sample contracts using the RMRK standard implementation on EVM.
4 |
5 | ## RMRK Legos
6 |
7 | RMRK is a set of NFT standards which compose several "NFT 2.0 lego" primitives. Putting these legos together allows a
8 | user to create NFT systems of arbitrary complexity.
9 |
10 | There are various possibilities on how to combine these legos, all of which are ERC721 compatible:
11 |
12 | 1. MultiAsset (Context-Dependent Multi-Asset Tokens)
13 | - Only uses the MultiAsset RMRK lego
14 | 2. Nestable (Parent-Governed Nestable Non-Fungible Tokens)
15 | - Only uses the Nestable RMRK lego
16 | 3. Nestable with MultiAsset
17 | - Uses both Nestable and MultiAsset RMRK legos
18 | 4. Equippable MultiAsset with Nestable and Catalog
19 | - Merged equippable is a more compact RMRK lego composite that uses less smart contracts, but has less space for
20 | custom logic implementation
21 | - Split equippable is a more customizable RMRK lego composite that uses more smart contracts, but has more space for
22 | custom logic implementation
23 |
24 | The first 3 use cases have stand alone versions with both minimal and ready to use implementations. The latter two, due
25 | to Solidity contract size constraints, MultiAsset and Equippable logic are included in a simgle smart contract, while
26 | Nestable and ownership are handled by either the same smart contract or a separate one. Catalog is also a separate smart
27 | contract for practical reasons, since one Catalog can be used by multiple tokens.
28 |
29 | 
30 |
31 | ## Installation
32 |
33 | You can start using the RMRK EVM implementation smart contracts by installing the dependency to your project:
34 |
35 | ```
36 | npm install @rmrk-team/evm-contracts
37 | ```
38 |
39 | Once you have installed the `@rmrk-team/evm-contracts` dependency, you can refer to one of the samples residing in this
40 | repository's [`contracts/`](./contracts/README.md) directory. The versions starting with `Simple` keyword are ready to
41 | use; you can simply extend those for your own contracts and pass fixed or variable parameters to the constructor. The
42 | examples starting with `Advanced` keyword showcase the implementation where you have more freedom in implementing custom
43 | business logic. The available internal functions when building it are outlined within the examples.
44 |
45 | For each of the lego combinations we have sample versions:
46 |
47 | 1. [`MultiAsset`](./contracts/MultiAsset/README.md)
48 | 2. [`Nestable`](./contracts/Nestable/README.md)
49 | 3. [`Nestable with MultiAsset`](./contracts/NestableMultiAsset/README.md)
50 | 4. [`MergedEquippable`](./contracts/MergedEquippable/README.md)
51 | 5. [`SplitEquippable`](./contracts/SplitEquippable/README.md)
52 |
53 | Additionally we have render util contracts. The reason these are separate is to save contract space. You can have a single deploy of those and use them on every contract or even use the exising ones (We'll provide them in the future):
54 |
55 | 1. [`MultiAsset render utils`](@rmrk-team/evm-contracts/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol)
56 | provides utilities to get asset objects from IDs, and accepted or pending asset objects for a given token. The
57 | MultiAsset lego provides only IDs for the latter.
58 | 2. [`Equip render utils`](@rmrk-team/evm-contracts/contracts/RMRK/utils/RMRKEquipRenderUtils.sol) provides the same
59 | shorcuts on extended assets (with equip information). This utility smart contract also has views to get information
60 | about what is currently equipped to a token and to compose equippables for a token asset.
--------------------------------------------------------------------------------
/contracts/AdvancedCatalog.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | pragma solidity ^0.8.18;
4 |
5 | import "@rmrk-team/evm-contracts/contracts/RMRK/catalog/RMRKCatalog.sol";
6 |
7 | contract AdvancedCatalog is RMRKCatalog {
8 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
9 | constructor(string memory symbol, string memory type_)
10 | RMRKCatalog(symbol, type_)
11 | {
12 | // Custom optional: constructor logic
13 | }
14 |
15 | // Custom expected: external gated functions to add parts.
16 | // Available internal functions:
17 | // _addPart(IntakeStruct memory intakeStruct)
18 | // _addPartList(IntakeStruct[] memory intakeStructs)
19 |
20 | // Custom expected: external gated functions to manage equippable addresses
21 | // Available internal functions:
22 | // _addEquippableAddresses(uint64 partId, address[] memory equippableAddresses)
23 | // _setEquippableAddresses( uint64 partId, address[] memory equippableAddresses)
24 | // _setEquippableToAll(uint64 partId)
25 | // _resetEquippableAddresses(uint64 partId)
26 | }
27 |
--------------------------------------------------------------------------------
/contracts/MergedEquippable/AdvancedEquippable.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | pragma solidity ^0.8.18;
4 |
5 | import "@rmrk-team/evm-contracts/contracts/RMRK/equippable/RMRKEquippable.sol";
6 |
7 | contract AdvancedEquippable is RMRKEquippable {
8 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
9 | constructor(string memory name, string memory symbol)
10 | RMRKEquippable(name, symbol)
11 | {
12 | // Custom optional: constructor logic
13 | }
14 |
15 | // Custom expected: external, optionally gated, functions to mint.
16 | // Available internal functions:
17 | // _mint(address to, uint256 tokenId)
18 | // _safeMint(address to, uint256 tokenId)
19 | // _safeMint(address to, uint256 tokenId, bytes memory data)
20 |
21 | // Custom expected: external, optionally gated, functions to nest mint.
22 | // Available internal functions:
23 | // _nestMint(address to, uint256 tokenId, uint256 destinationId, bytes memory data)
24 |
25 | // Custom expected: external gated function to burn.
26 | // Available internal functions:
27 | // _burn(uint256 tokenId, uint256 maxChildrenBurns)
28 |
29 | // Custom optional: utility functions to transfer and nest transfer from caller
30 | // Available public functions:
31 | // transferFrom(address from, address to, uint256 tokenId)
32 | // nestTransferFrom(address from, address to, uint256 tokenId, uint256 destinationId, bytes memory data)
33 |
34 | // Custom expected: external, optionally gated, function to add assets.
35 | // Available internal functions:
36 | // _addAssetEntry(uint64 id, uint64 equippableGroupId, address catalogAddress, string memory metadataURI, uint64[] calldata partIds)
37 |
38 | // Custom expected: external, optionally gated, function to add assets to tokens.
39 | // Available internal functions:
40 | // _addAssetToToken(uint256 tokenId, uint64 assetId, uint64 replacesAssetWithId)
41 |
42 | // Custom expected: external, optionally gated, function to add set valid parent reference Id.
43 | // Available internal functions:
44 | // _setValidParentForEquippableGroup(uint64 equippableGroupId, address parentAddress, uint64 slotPartId)
45 | }
46 |
--------------------------------------------------------------------------------
/contracts/MergedEquippable/SimpleEquippable.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.18;
3 |
4 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKEquippableImpl.sol";
5 | // We import it just so it's included on typechain. We'll need it to compose NFTs
6 | import "@rmrk-team/evm-contracts/contracts/RMRK/utils/RMRKEquipRenderUtils.sol";
7 |
8 | contract SimpleEquippable is RMRKEquippableImpl {
9 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
10 | constructor(
11 | string memory name,
12 | string memory symbol,
13 | string memory collectionMetadata,
14 | string memory tokenURI,
15 | InitData memory data
16 | )
17 | RMRKEquippableImpl(
18 | name,
19 | symbol,
20 | collectionMetadata,
21 | tokenURI,
22 | data
23 | )
24 | {}
25 | }
26 |
--------------------------------------------------------------------------------
/contracts/MultiAsset/AdvancedMultiAsset.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | pragma solidity ^0.8.18;
4 |
5 | import "@rmrk-team/evm-contracts/contracts/RMRK/multiasset/RMRKMultiAsset.sol";
6 |
7 | contract AdvancedMultiAsset is RMRKMultiAsset {
8 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
9 | constructor(string memory name, string memory symbol)
10 | RMRKMultiAsset(name, symbol)
11 | {
12 | // Custom optional: constructor logic
13 | }
14 |
15 | // Custom expected: external, optionally gated, functions to mint.
16 | // Available internal functions:
17 | // _mint(address to, uint256 tokenId)
18 | // _safeMint(address to, uint256 tokenId)
19 | // _safeMint(address to, uint256 tokenId, bytes memory data)
20 |
21 | // Custom expected: external gated function to burn.
22 | // Available internal functions:
23 | // _burn(uint256 tokenId)
24 |
25 | // Custom expected: external, optionally gated, function to add assets.
26 | // Available internal functions:
27 | // _addAssetEntry(uint64 id, string memory metadataURI)
28 |
29 | // Custom expected: external, optionally gated, function to add assets to tokens.
30 | // Available internal functions:
31 | // _addAssetToToken(uint256 tokenId, uint64 assetId, uint64 replacesAssetWithId)
32 |
33 | // Custom optional: utility functions to transfer from caller
34 | // Available public functions:
35 | // transferFrom(address from, address to, uint256 tokenId)
36 | }
37 |
--------------------------------------------------------------------------------
/contracts/MultiAsset/README.md:
--------------------------------------------------------------------------------
1 | # MultiAsset
2 |
3 | 
4 |
5 | An *asset* is a type of output for an NFT, usually a media file.
6 |
7 | An asset can be an image, a movie, a PDF file, device config file... A multi-asset NFT is one that can output a
8 | different asset based on specific contextual information, e.g. load a PDF if loaded into a PDF reader, vs. loading an
9 | image in a virtual gallery, vs. loading hardware configuration in an IoT control hub.
10 |
11 | An asset is NOT an NFT or a standalone entity you can reference. It is part of an NFT - one of several outputs it can
12 | have.
13 |
14 | Every RMRK NFT has zero or more assets. When it has zero assets, the metadata is "root level". Any new asset
15 | added to this NFT will override the root metadata, making this NFT
16 | [revealable](https://docs.rmrk.app/usecases/revealable).
17 |
18 | **NOTE: To dig deeper into the MultiAsset RMRK lego, you can also refer to the
19 | [EIP-5773]([inheritance](https://eips.ethereum.org/EIPS/eip-5773)) that we published.**
20 |
21 | ## Abstract
22 |
23 | In this example we will examine the MultiAsset RMRK block using two examples:
24 |
25 | - [SimpleMultiAsset](./SimpleMultiAsset.sol) is a minimal implementation of the MultiAsset RMRK block.
26 | - [AdvancedMultiAsset](./AdvancedMultiAsset.sol) is a more customizable implementation of the MultiAsset RMRK
27 | block.
28 |
29 | Let's first examine the simple, minimal, implementation and then move on to the advanced one.
30 |
31 | ## SimpleMultiAsset
32 |
33 | The `SimpleMultiAsset` example uses the
34 | [`RMRKMultiAssetImpl`](https://github.com/rmrk-team/evm/blob/dev/contracts/implementations/nativeTokenPay/RMRKMultiAssetImpl.sol).
35 | It is used by importing it using the `import` statement below the `pragma` definition:
36 |
37 | ````solidity
38 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKMultiAssetImpl.sol";
39 | ````
40 |
41 | Once the `RMRKMultiAssetImpl.sol` is imported into our file, we can set the inheritance of our smart contract:
42 |
43 | ````solidity
44 | contract SimpleMultiAsset is RMRKMultiAssetImpl {
45 |
46 | }
47 | ````
48 |
49 | We won't be passing all of the required parameters, to intialize `RMRKMultiAssetImpl` contract, to the constructor,
50 | but will hardcode some of the values. The values that we will pass are:
51 |
52 | - `data`: struct type of argument providing a number of initialization values, used to avoid initialization transaction
53 | being reverted due to passing too many parameters
54 |
55 | The parameters that we will hardcode to the initialization of `RMRKMultiAssetImpl` are:
56 |
57 | - `name`: `string` type of argument representing the name of the collection will be set to `SimpleMultiAsset`
58 | - `symbol`: `string` type od argument representing the symbol of the collection will be set to `SMA`
59 | - `collectionMetadata_`: `string` type of argument representing the metadata URI of the collection will be set to
60 | `ipfs://meta`
61 | - `tokenURI_`: `string` type of argument representing the base metadata URI of tokens will be set to `ipfs://tokenMeta`
62 |
63 | **NOTE: The `InitData` struct is used to pass the initialization parameters to the implementation smart contract. This
64 | is done so that the execution of the deploy transaction doesn't revert because we are trying to pass too many arguments.**
65 |
66 | **The `InitData` struct contains the following fields:**
67 |
68 | ````solidity
69 | [
70 | erc20TokenAddress,
71 | tokenUriIsEnumerable,
72 | royaltyRecipient,
73 | royaltyPercentageBps, // Expressed in basis points
74 | maxSupply,
75 | pricePerMint
76 | ]
77 | ````
78 |
79 | **NOTE: Basis points are the smallest supported denomination of percent. In our case this is one hundreth of a percent.
80 | This means that 1 basis point equals 0.01% and 10000 basis points equal 100%. So for example, if you want to set royalty
81 | percentage to 5%, the `royaltyPercentageBps` value should be 500.**
82 |
83 | So the constructor of the `SimpleMultiAsset` should look like this:
84 |
85 | ````solidity
86 | constructor(InitData memory data)
87 | RMRKMultiAssetImpl(
88 | "SimpleMultiAsset",
89 | "SMA",
90 | "ipfs://meta",
91 | "ipfs://tokenMeta",
92 | data
93 | )
94 | {}
95 | ````
96 |
97 |
98 | The SimpleMultiAsset.sol should look like this:
99 |
100 | ````solidity
101 | // SPDX-License-Identifier: UNLICENSED
102 | pragma solidity ^0.8.18;
103 |
104 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKMultiAssetImpl.sol";
105 |
106 | contract SimpleMultiAsset is RMRKMultiAssetImpl {
107 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
108 | constructor(
109 | uint256 maxSupply,
110 | uint256 pricePerMint
111 | ) RMRKMultiAssetImpl(
112 | "SimpleMultiAsset",
113 | "SMA",
114 | maxSupply,
115 | pricePerMint,
116 | "ipfs://meta",
117 | "ipfs://tokenMeta",
118 | msg.sender,
119 | 10
120 | ) {}
121 | }
122 | ````
123 |
124 |
125 |
126 | ### RMRKMultiAssetImpl
127 |
128 | Let's take a moment to examine the core of this implementation, the `RMRKMultiAssetImpl`.
129 |
130 | It uses the `RMRKRoyalties`, `RMRKMultiAsset`, `RMRKCollectionMetadata` and `RMRKMintingUtils` smart contracts from
131 | RMRK stack. To dive deeper into their operation, please refer to their respective documentation.
132 |
133 | Two errors are defined:
134 |
135 | ````solidity
136 | error RMRKMintUnderpriced();
137 | error RMRKMintZero();
138 | ````
139 |
140 | `RMRKMintUnderpriced()` is used when not enough value is used when attempting to mint a token and `RMRKMintZero()` is
141 | used when attempting to mint 0 tokens.
142 |
143 | The `RMRKMultiAssetImpl` implements all of the required functionality of the MultiAsset lego. It implements
144 | standard NFT methods like `mint`, `transfer`, `approve`, `burn`,... In addition to these methods it also implements the
145 | methods specific to MultiAsset RMRK lego:
146 |
147 | - `addAssetToToken`
148 | - `addAssetEntry`
149 | - `totalAssets`
150 | - `tokenURI`
151 | - `updateRoyaltyRecipient`
152 |
153 | **WARNING: The `RMRKMultiAssetImpl` only has minimal access control implemented. If you intend to use it, make sure
154 | to define your own, otherwise your smart contracts are at risk of unexpected behaviour.**
155 |
156 | #### `mint`
157 |
158 | The `mint` function is used to mint parent NFTs and accepts two arguments:
159 |
160 | - `to`: `address` type of argument that specifies who should receive the newly minted tokens
161 | - `numToMint`: `uint256` type of argument that specifies how many tokens should be minted
162 |
163 | There are a few constraints to this function:
164 |
165 | - after minting, the total number of tokens should not exceed the maximum allowed supply
166 | - attempting to mint 0 tokens is not allowed as it makes no sense to pay for the gas without any effect
167 | - value should accompany transaction equal to a price per mint multiplied by the `numToMint`
168 |
169 | #### `addAssetToToken`
170 |
171 | The `addAssetToToken` is used to add a new asset to the token and accepts three arguments:
172 |
173 | - `tokenId`: `uint256` type of argument specifying the ID of the token we are adding asset to
174 | - `assetId`: `uint64` type of argument specifying the ID of the asset we are adding to the token
175 | - `replacesAssetWithId`: `uint64` type of argument specifying the ID of the asset we are overwriting with the desired asset
176 |
177 | #### `addAssetEntry`
178 |
179 | The `addAssetEntry` is used to add a new URI for the new asset of the token and accepts one argument:
180 |
181 | - `metadataURI`: `string` type of argument specifying the metadata URI of a new asset
182 |
183 | #### `totalAssets`
184 |
185 | The `totalAssets` is used to retrieve a total number of assets defined in the collection.
186 |
187 | #### `tokenURI`
188 |
189 | The `tokenURI` is used to retrieve the metadata URI of the desired token and accepts one argument:
190 |
191 | - `tokenId`: `uint256` type of argument representing the token ID of which we are retrieving the URI
192 |
193 | #### `updateRoyaltyRecipient`
194 |
195 | The `updateRoyaltyRecipient` function is used to update the royalty recipient and accepts one argument:
196 |
197 | - `newRoyaltyRecipient`: `address` type of argument specifying the address of the new beneficiary recipient
198 |
199 | ### Deploy script
200 |
201 | The deploy script for the `SimpleMultiAsset` smart contract resides in the
202 | [`deployMultiAsset.ts`](../../scripts/deployMultiAsset.ts).
203 |
204 | The script uses the `ethers`, `SimpleMultiAsset` and `ContractTransaction` imports. The empty deploy script should look like
205 | this:
206 |
207 | ````typescript
208 | import { ethers } from "hardhat";
209 | import { SimpleMultiAsset } from "../typechain-types";
210 | import { ContractTransaction } from "ethers";
211 |
212 | async function main() {
213 |
214 | }
215 |
216 | main().catch((error) => {
217 | console.error(error);
218 | process.exitCode = 1;
219 | });
220 | ````
221 |
222 | Before we can deploy the parent and child smart contracts, we should prepare the constants that we will use in the
223 | script:
224 |
225 | ````typescript
226 | const pricePerMint = ethers.utils.parseEther("0.0001");
227 | const totalTokens = 5;
228 | const [owner] = await ethers.getSigners();
229 | ````
230 |
231 |
232 |
233 | Now that the constants are ready, we can deploy the smart contract and log the address of the contract to the console:
234 |
235 | ````typescript
236 | const contractFactory = await ethers.getContractFactory(
237 | "SimpleMultiAsset"
238 | );
239 | const token: SimpleMultiAsset = await contractFactory.deploy(
240 | {
241 | erc20TokenAddress: ethers.constants.AddressZero,
242 | tokenUriIsEnumerable: true,
243 | royaltyRecipient: ethers.constants.AddressZero,
244 | royaltyPercentageBps: 0,
245 | maxSupply: 1000,
246 | pricePerMint: pricePerMint
247 | }
248 | );
249 |
250 | await token.deployed();
251 | console.log(`Sample contract deployed to ${token.address}`);
252 | ````
253 |
254 | A custom script added to [`package.json`](../../package.json) allows us to easily run the script:
255 |
256 | ````json
257 | "scripts": {
258 | "deploy-multi-asset": "hardhat run scripts/deployMultiAsset.ts"
259 | }
260 | ````
261 |
262 | Using the script with `npm run deploy-multi-asset` should return the following output:
263 |
264 | ````shell
265 | npm run deploy-multi-asset
266 |
267 | > @rmrk-team/evm-contract-samples@0.1.0 deploy-multi-asset
268 | > hardhat run scripts/deployMultiAsset.ts
269 |
270 | Compiled 47 Solidity files successfully
271 | Sample contract deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
272 | ````
273 |
274 | ### User journey
275 |
276 | With the deploy script ready, we can examine how the journey of a user using multi asset would look like using this
277 | smart contract.
278 |
279 | The base of it is the same as the deploy script, as we need to deploy the smart contract in order to interact with it:
280 |
281 | ````typescript
282 | import { ethers } from "hardhat";
283 | import { SimpleMultiAsset } from "../typechain-types";
284 | import { ContractTransaction } from "ethers";
285 |
286 | async function main() {
287 | const pricePerMint = ethers.utils.parseEther("0.0001");
288 | const totalTokens = 5;
289 | const [ , tokenOwner] = await ethers.getSigners();
290 |
291 | const contractFactory = await ethers.getContractFactory(
292 | "SimpleMultiAsset"
293 | );
294 | const token: SimpleMultiAsset = await contractFactory.deploy(
295 | {
296 | erc20TokenAddress: ethers.constants.AddressZero,
297 | tokenUriIsEnumerable: true,
298 | royaltyRecipient: ethers.constants.AddressZero,
299 | royaltyPercentageBps: 0,
300 | maxSupply: 1000,
301 | pricePerMint: pricePerMint
302 | }
303 | );
304 |
305 | await token.deployed();
306 | console.log(`Sample contract deployed to ${token.address}`);
307 | }
308 |
309 | main().catch((error) => {
310 | console.error(error);
311 | process.exitCode = 1;
312 | });
313 | ````
314 |
315 | **NOTE: We assign the `tokenOwner` the second available signer, so that the assets are not automatically accepted when added
316 | to the token. This happens when an account adding an asset to a token is also the owner of said token.**
317 |
318 | First thing that needs to be done after the smart contract is deployed it to mint the NFT. We will use the `totalTokens`
319 | constant to specify how many tokens to mint:
320 |
321 | ````typescript
322 | console.log("Minting tokens");
323 | let tx = await token.mint(tokenOwner.address, totalTokens, {
324 | value: pricePerMint.mul(totalTokens),
325 | });
326 | await tx.wait();
327 | console.log(`Minted ${totalTokens} tokens`);
328 | const totalSupply = await token.totalSupply();
329 | console.log("Total tokens: %s", totalSupply);
330 | ````
331 |
332 | Now that the tokens are minted, we can add new assets to the smart contract. We will prepare a batch of transactions
333 | that will add simple IPFS metadata link for the assets in the smart contract. Once the transactions are ready, we
334 | will send them and get all of the assets to output to the console:
335 |
336 | ````typescript
337 | console.log("Adding assets");
338 | let allTx: ContractTransaction[] = [];
339 | for (let i = 1; i <= totalTokens; i++) {
340 | let tx = await token.addAssetEntry(`ipfs://metadata/${i}.json`);
341 | allTx.push(tx);
342 | }
343 | console.log(`Added ${totalTokens} assets`);
344 |
345 | console.log("Awaiting for all tx to finish...");
346 | await Promise.all(allTx.map((tx) => tx.wait()));
347 | ````
348 |
349 | Once the assets are added to the smart contract we can assign each asset to one of the tokens:
350 |
351 | ````typescript
352 | console.log("Adding assets to tokens");
353 | allTx = [];
354 | for (let i = 1; i <= totalTokens; i++) {
355 | // We give each token a asset id with the same number. This is just a coincidence, not a restriction.
356 | let tx = await token.addAssetToToken(i, i, 0);
357 | allTx.push(tx);
358 | console.log(`Added asset ${i} to token ${i}.`);
359 | }
360 | console.log("Awaiting for all tx to finish...");
361 | await Promise.all(allTx.map((tx) => tx.wait()));
362 | ````
363 |
364 | After the assets are added to the NFTs, we have to accept them. We will do this by once again building a batch of
365 | transactions for each of the tokens and send them at the end:
366 |
367 | ````typescript
368 | console.log("Accepting token assets");
369 | allTx = [];
370 | for (let i = 1; i <= totalTokens; i++) {
371 | // Accept pending asset for each token (on index 0)
372 | let tx = await token.connect(tokenOwner).acceptAsset(i, 0, i);
373 | allTx.push(tx);
374 | console.log(`Accepted first pending asset for token ${i}.`);
375 | }
376 | console.log("Awaiting for all tx to finish...");
377 | await Promise.all(allTx.map((tx) => tx.wait()));
378 | ````
379 |
380 | **NOTE: Accepting assets is done in a array that gets elements, new assets, appended to the end of it. Once the asset is
381 | accepted, the asset that was added last, takes its place. For example:**
382 |
383 | **We have assets `A`, `B`, `C` and `D` in the pending array organised like this: [`A`, `B`, `C`, `D`].**
384 |
385 | **Accepting the asset `A` updates the array to look like this: [`D`, `B`, `C`].**
386 |
387 | **Accepting the asset `B` updates the array to look like this: [`A`, `D`, `C`].**
388 |
389 | Finally we can check wether the URI are assigned as expected and output the values to the console:
390 |
391 | ````typescript
392 | console.log("Getting URIs");
393 | const uriToken1 = await token.tokenURI(1);
394 | const uriFinalToken = await token.tokenURI(totalTokens);
395 |
396 | console.log("Token 1 URI: ", uriToken1);
397 | console.log("Token totalTokens URI: ", uriFinalToken);
398 | ````
399 |
400 | With the user journey script concluded, we can add a custom helper to the [`package.json`](../../package.json) to make
401 | running it easier:
402 |
403 | ````json
404 | "user-journey-multi-asset": "hardhat run scripts/multiAssetUserJourney.ts"
405 | ````
406 |
407 | Running it using `npm run user-journey-multi-asset` should return the following output:
408 |
409 | ````shell
410 | npm run user-journey-multi-asset
411 |
412 | > @rmrk-team/evm-contract-samples@0.1.0 user-journey-multi-asset
413 | > hardhat run scripts/multiAssetUserJourney.ts
414 |
415 | Sample contract deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
416 | Minting tokens
417 | Minted 5 tokens
418 | Total tokens: 5
419 | Adding assets
420 | Added 5 assets
421 | Awaiting for all tx to finish...
422 | All assets: [
423 | BigNumber { value: "1" },
424 | BigNumber { value: "2" },
425 | BigNumber { value: "3" },
426 | BigNumber { value: "4" },
427 | BigNumber { value: "5" }
428 | ]
429 | Adding assets to tokens
430 | Added asset 1 to token 1.
431 | Added asset 2 to token 2.
432 | Added asset 3 to token 3.
433 | Added asset 4 to token 4.
434 | Added asset 5 to token 5.
435 | Awaiting for all tx to finish...
436 | Accepting token assets
437 | Accepted first pending asset for token 1.
438 | Accepted first pending asset for token 2.
439 | Accepted first pending asset for token 3.
440 | Accepted first pending asset for token 4.
441 | Accepted first pending asset for token 5.
442 | Awaiting for all tx to finish...
443 | Getting URIs
444 | Token 1 URI: ipfs://metadata/1.json
445 | Token totalTokens URI: ipfs://metadata/5.json
446 | ````
447 |
448 | This concludes our work on the [`SimpleMultiAsset.sol`](./SimpleMultiAsset.sol). We can now move on to examining the
449 | [`AdvancedMultiAsset.sol`](./AdvancedMultiAsset.sol).
450 |
451 | ## AdvancedMultiAsset
452 |
453 | The `AdvancedMultiAsset` smart contract allows for more flexibility when using the multi asset lego. It implements
454 | minimum required implementation in order to be compatible with RMRK multi asset, but leaves more business logic
455 | implementation freedom to the developer. It uses the
456 | [`RMRKMultiAsset.sol`](https://github.com/rmrk-team/evm/blob/dev/contracts/RMRK/multiasset/RMRKMultiAsset.sol)
457 | import to gain access to the Multi asset lego:
458 |
459 | ````solidity
460 | import "@rmrk-team/evm-contracts/contracts/RMRK/multiasset/RMRKMultiAsset.sol";
461 | ````
462 |
463 | We only need `name` and `symbol` of the NFT in order to properly initialize it after the `AdvancedMultiAsset`
464 | inherits it:
465 |
466 | ````solidity
467 | contract AdvancedMultiAsset is RMRKMultiAsset {
468 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
469 | constructor(
470 | string memory name,
471 | string memory symbol
472 | )
473 | RMRKMultiAsset(name, symbol)
474 | {
475 | // Custom optional: constructor logic
476 | }
477 | }
478 | ````
479 |
480 | This is all that is required to get you started with implementing the Multi asset RMRK lego.
481 |
482 |
483 | The minimal AdvancedMultiAsset.sol should look like this:
484 |
485 | ````solidity
486 | // SPDX-License-Identifier: Apache-2.0
487 |
488 | pragma solidity ^0.8.18;
489 |
490 | import "@rmrk-team/evm-contracts/contracts/RMRK/multiasset/RMRKMultiAsset.sol";
491 |
492 | contract AdvancedMultiAsset is RMRKMultiAsset {
493 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
494 | constructor(
495 | string memory name,
496 | string memory symbol
497 | )
498 | RMRKMultiAsset(name, symbol)
499 | {
500 | // Custom optional: constructor logic
501 | }
502 | }
503 | ````
504 |
505 |
506 |
507 | Using `RMRKMultiAsset` requires custom implementation of minting logic. Available internal functions to use when
508 | writing it are:
509 |
510 | - `_mint(address to, uint256 tokenId)`
511 | - `_safeMint(address to, uint256 tokenId)`
512 | - `_safeMint(address to, uint256 tokenId, bytes memory data)`
513 |
514 | In addition to the minting functions, you should also implement the burning, transfer and asset management functions if they apply to your use case:
515 |
516 | - `_burn(uint256 tokenId)`
517 | - `_addAssetEntry(uint64 id, string memory metadataURI)`
518 | - `_addAssetToToken(uint256 tokenId, uint64 assetId, uint64 replacesAssetWithId)`
519 | - `transferFrom(address from, address to, uint256 tokenId)`
520 |
521 | Any additional functions supporting your NFT use case and utility can also be added. Remember to thoroughly test your
522 | smart contracts with extensive test suites and define strict access control rules for the functions that you implement.
523 |
524 | Happy multiassetting! 🫧
--------------------------------------------------------------------------------
/contracts/MultiAsset/SimpleMultiAsset.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.18;
3 |
4 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKMultiAssetImpl.sol";
5 |
6 | contract SimpleMultiAsset is RMRKMultiAssetImpl {
7 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
8 | constructor(InitData memory data)
9 | RMRKMultiAssetImpl(
10 | "SimpleMultiAsset",
11 | "SMA",
12 | "ipfs://meta",
13 | "ipfs://tokenMeta",
14 | data
15 | )
16 | {}
17 | }
18 |
--------------------------------------------------------------------------------
/contracts/Nestable/AdvancedNestable.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | pragma solidity ^0.8.18;
4 |
5 | import "@rmrk-team/evm-contracts/contracts/RMRK/nestable/RMRKNestable.sol";
6 |
7 | contract AdvancedNestable is RMRKNestable {
8 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
9 | constructor(string memory name, string memory symbol)
10 | RMRKNestable(name, symbol)
11 | {
12 | // Custom optional: constructor logic
13 | }
14 |
15 | // Custom expected: external, optionally gated, functions to mint.
16 | // Available internal functions:
17 | // _mint(address to, uint256 tokenId)
18 | // _safeMint(address to, uint256 tokenId)
19 | // _safeMint(address to, uint256 tokenId, bytes memory data)
20 |
21 | // Custom expected: external, optionally gated, functions to nest mint.
22 | // Available internal functions:
23 | // _nestMint(address to, uint256 tokenId, uint256 destinationId)
24 |
25 | // Custom expected: external gated function to burn.
26 | // Available internal functions:
27 | // _burn(uint256 tokenId)
28 |
29 | // Custom optional: utility functions to transfer and nest transfer from caller
30 | // Available public functions:
31 | // transferFrom(address from, address to, uint256 tokenId)
32 | // nestTransfer(address from, address to, uint256 tokenId, uint256 destinationId)
33 | }
34 |
--------------------------------------------------------------------------------
/contracts/Nestable/README.md:
--------------------------------------------------------------------------------
1 | # Nestable
2 |
3 | 
4 |
5 | The concept of nested NFTs refers to NFTs being able to own other NFTs.
6 |
7 | At its core, the principle is simple: the owner of an NFT does not have to be an externally owned account or a smart
8 | contract, it can also be a specific NFT.
9 |
10 | The process of sending an NFT into another is functionally identical to sending it to another user. The process of
11 | sending an NFT out of another NFT involves issuing a transaction from the address which owns the parent.
12 |
13 | Some NFTs can be configured with special conditions for parent-child relationships. For example:
14 |
15 | - some parent NFTs will allow the owner of a child NFT to withdraw that child at any time (e.g. virtual land containing
16 | an avatar)
17 | - some parent NFTs will be prohibited from executing certain interactions on a child (e.g. the owner of a house in which
18 | someone else's avatar is a guest should not be able to BURN the guest... probably)
19 | - some parent NFTs will have special withdrawal conditions, like a music NFT that accepts music stems. The stems can be
20 | removed by their owners until a certain number of co-composers upvote a stem enough, or until the owner of the parent
21 | music track seals and "publishes" it
22 |
23 | ## Abstract
24 |
25 | In this tutorial we will examine the Nestable RMRK block using two examples:
26 |
27 | - [SimpleNestable](./SimpleNestable.sol) is a minimal implementation of the Nestable RMRK block.
28 | - [AdvancedNestable](./AdvancedNestable.sol) is a more customizable implementation of the Nestable RMRK block.
29 |
30 | Let's first examine the simple, minimal, implementation and then move on to the advanced one.
31 |
32 | ## SimpleNestable
33 |
34 | The `SimpleNestable` example uses the
35 | [`RMRKNestableImpl`](https://github.com/rmrk-team/evm/blob/dev/contracts/implementations/nativeTokenPay/RMRKNestableImpl.sol). It is
36 | used by importing it using the `import` statement below the `pragma` definition:
37 |
38 | ````solidity
39 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKNestableImpl.sol";
40 | ````
41 |
42 | Once the `RMRKNestableImpl.sol` is imported into our file, we can set the inheritance of our smart contract:
43 |
44 | ````solidity
45 | contract SimpleNestable is RMRKNestableImpl {
46 |
47 | }
48 | ````
49 |
50 | The `RMRKNestableImpl` implements all of the required functionality of the Nested RMRK lego. It implements minting of
51 | parent NFTs as well as child NFTs. Transferring and burning the NFTs is also implemented.
52 |
53 | **WARNING: The `RMRKNestableImpl` only has minimal access control implemented. If you intend to use it, make sure to
54 | define your own, otherwise your smart contracts are at risk of unexpected behaviour.**
55 |
56 | The `constructor` to initialize the `RMRKNestableImpl` accepts the following arguments:
57 |
58 | - `name_`: `string` argument that should represent the name of the NFT collection
59 | - `symbol_`: `string` argument that should represent the symbol of the NFT collection
60 | - `collectionMetadata_`: `string` argument that defines the metadata URI of the whole collection
61 | - `tokenURI_`: `string` argument that defines the base URI of the token metadata
62 | - `data`: struct type of argument providing a number of initialization values, used to avoid initialization transaction
63 | being reverted due to passing too many parameters
64 |
65 | **NOTE: The `InitData` struct is used to pass the initialization parameters to the implementation smart contract. This
66 | is done so that the execution of the deploy transaction doesn't revert because we are trying to pass too many
67 | arguments.**
68 |
69 | **The `InitData` struct contains the following fields:**
70 |
71 | ````solidity
72 | [
73 | erc20TokenAddress,
74 | tokenUriIsEnumerable,
75 | royaltyRecipient,
76 | royaltyPercentageBps, // Expressed in basis points
77 | maxSupply,
78 | pricePerMint
79 | ]
80 | ````
81 |
82 | **NOTE: Basis points are the smallest supported denomination of percent. In our case this is one hundreth of a percent.
83 | This means that 1 basis point equals 0.01% and 10000 basis points equal 100%. So for example, if you want to set royalty
84 | percentage to 5%, the `royaltyPercentageBps` value should be 500.**
85 |
86 | In order to properly initiate the inherited smart contract, our smart contract needs to accept the arguments, mentioned
87 | above, in the `constructor` and pass them to `RMRKNestableImpl`:
88 |
89 | ````solidity
90 | constructor(
91 | string memory name,
92 | string memory symbol,
93 | string memory collectionMetadata,
94 | string memory tokenURI,
95 | InitData memory data
96 | )
97 | RMRKNestableImpl(
98 | name,
99 | symbol,
100 | collectionMetadata,
101 | tokenURI,
102 | data
103 | )
104 | {}
105 | ````
106 |
107 |
108 | The SimpleNestable.sol should look like this:
109 |
110 | ````solidity
111 | // SPDX-License-Identifier: UNLICENSED
112 | pragma solidity ^0.8.18;
113 |
114 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKNestableImpl.sol";
115 |
116 | contract SimpleNestable is RMRKNestableImpl {
117 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
118 | constructor(
119 | string memory name,
120 | string memory symbol,
121 | string memory collectionMetadata,
122 | string memory tokenURI,
123 | InitData memory data
124 | )
125 | RMRKNestableImpl(
126 | name,
127 | symbol,
128 | collectionMetadata,
129 | tokenURI,
130 | data
131 | )
132 | {}
133 | }
134 | ````
135 |
136 |
137 |
138 | ### RMRKNestableImpl
139 |
140 | Let's take a moment to examine the core of this implementation, the `RMRKNestableImpl`.
141 |
142 | It uses the `RMRKRoyalties`, `RMRKNestable`, `RMRKCollectionMetadata` and `RMRKMintingUtils` smart contracts from `RMRK`
143 | stack. To dive deeper into their operation, please refer to their respective documentation.
144 |
145 | Two errors are defined:
146 |
147 | ````solidity
148 | error RMRKMintUnderpriced();
149 | error RMRKMintZero();
150 | ````
151 |
152 | `RMRKMintUnderpriced()` is used when not enough value is used when attempting to mint a token and `RMRKMintZero()` is
153 | used when attempting to mint 0 tokens.
154 |
155 | #### `mint`
156 |
157 | The `mint` function is used to mint parent NFTs and accepts two arguments:
158 |
159 | - `to`: `address` type of argument that specifies who should receive the newly minted tokens
160 | - `numToMint`: `uint256` type of argument that specifies how many tokens should be minted
161 |
162 | There are a few constraints to this function:
163 |
164 | - after minting, the total number of tokens should not exceed the maximum allowed supply
165 | - attempting to mint 0 tokens is not allowed as it makes no sense to pay for the gas without any effect
166 | - value should accompany transaction equal to a price per mint multiplied by the `numToMint`
167 |
168 | #### `nestMint`
169 |
170 | The `nestMint` function is used to mint child NFTs to be owned by the parent NFT and accepts three arguments:
171 |
172 | - `to`: `address` type of argument specifying the address of the smart contract to which the parent NFT belongs to
173 | - `numToMint`: `uint256` type of argument specifying the amount of tokens to be minted
174 | - `destinationId`: `uint256` type of argument specifying the ID of the parent NFT to which to mint the child NFT
175 |
176 | The constraints of `nestMint` are similar to the ones set out for `mint` function.
177 |
178 | #### `transfer`
179 |
180 | Can only be called by a direct owner or a parent NFT's smart contract or a caller that was given the allowance and is
181 | used to transfer the NFT to the specified address.
182 |
183 | #### `nestTransfer`
184 |
185 | Can only be called by a direct owner or a parent NFT's smart contract or a caller that was given the allowance and is
186 | used to transfer the NFT to another NFT residing in a specified contract. This will nest the given NFT into the
187 | specified one.
188 |
189 | #### `tokenURI`
190 |
191 | The `tokenURI` function is used to get the metadata URI of the given token and accepts one argument:
192 |
193 | - `uint256` type of argument specifying the ID of the token
194 |
195 | #### `updateRoyaltyRecipient`
196 |
197 | The `updateRoyaltyRecipient` function is used to update the royalty recipient and accepts one argument:
198 |
199 | - `newRoyaltyRecipient`: `address` type of argument specifying the address of the new beneficiary recipient
200 |
201 | ### Deploy script
202 |
203 | The deploy script for the `SimpleNestable` smart contract resides in the
204 | [`deployNestable.ts`](../../scripts/deployNestable.ts).
205 |
206 | The script uses the `ethers`, `SimpleNestable` and `ContractTransactio` imports. The empty deploy script should look like
207 | this:
208 |
209 | ````typescript
210 | import { ethers } from "hardhat";
211 | import { SimpleNestable } from "../typechain-types";
212 | import { ContractTransaction } from "ethers";
213 |
214 | async function main() {
215 |
216 | }
217 |
218 | main().catch((error) => {
219 | console.error(error);
220 | process.exitCode = 1;
221 | });
222 | ````
223 |
224 | Before we can deploy the parent and child smart contracts, we should prepare the constants that we will use in the
225 | script:
226 |
227 | ````typescript
228 | const pricePerMint = ethers.utils.parseEther("0.0001");
229 | const totalTokens = 5;
230 | const [owner] = await ethers.getSigners();
231 | ````
232 |
233 | Now that the constants are ready, we can deploy the smart contracts and log the addresses of the contracts to the
234 | console:
235 |
236 | ````typescript
237 | const contractFactory = await ethers.getContractFactory("SimpleNestable");
238 | const parent: SimpleNestable = await contractFactory.deploy(
239 | "Kanaria",
240 | "KAN",
241 | "ipfs://collectionMeta",
242 | "ipfs://tokenMeta",
243 | {
244 | erc20TokenAddress: ethers.constants.AddressZero,
245 | tokenUriIsEnumerable: true,
246 | royaltyRecipient: await owner.getAddress(),
247 | royaltyPercentageBps: 10,
248 | maxSupply: 1000,
249 | pricePerMint: pricePerMint
250 | }
251 | );
252 | const child: SimpleNestable = await contractFactory.deploy(
253 | "Chunky",
254 | "CHN",
255 | "ipfs://collectionMeta",
256 | "ipfs://tokenMeta",
257 | {
258 | erc20TokenAddress: ethers.constants.AddressZero,
259 | tokenUriIsEnumerable: true,
260 | royaltyRecipient: await owner.getAddress(),
261 | royaltyPercentageBps: 10,
262 | maxSupply: 1000,
263 | pricePerMint: pricePerMint
264 | }
265 | );
266 |
267 |
268 | await parent.deployed();
269 | await child.deployed();
270 | console.log(
271 | `Sample contracts deployed to ${parent.address} and ${child.address}`
272 | );
273 | ````
274 |
275 | A custom script added to [`package.json`](../../package.json) allows us to easily run the script:
276 |
277 | ````json
278 | "scripts": {
279 | "deploy-nestable": "hardhat run scripts/deployNestable.ts"
280 | }
281 | ````
282 |
283 | Using the script with `npm run deploy-nestable` should return the following output:
284 |
285 | ````shell
286 | npm run deploy-nestable
287 |
288 | > @rmrk-team/evm-contract-samples@0.1.0 deploy-nestable
289 | > hardhat run scripts/deployNestable.ts
290 |
291 | Compiled 47 Solidity files successfully
292 | Sample contracts deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3 and 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
293 | ````
294 |
295 | ### User journey
296 |
297 | With the deploy script ready, we can examine how the journey of a user using nestable would look like using these two
298 | smart contracts.
299 |
300 | The base of it is the same as the deploy script, as we need to deploy the smart contracts in order to interact with
301 | them:
302 |
303 | ````typescript
304 | import { ethers } from "hardhat";
305 | import { SimpleNestable } from "../typechain-types";
306 | import { ContractTransaction } from "ethers";
307 |
308 | async function main() {
309 | const pricePerMint = ethers.utils.parseEther("0.0001");
310 | const totalTokens = 5;
311 | const [owner] = await ethers.getSigners();
312 |
313 | const contractFactory = await ethers.getContractFactory("SimpleNestable");
314 | const parent: SimpleNestable = await contractFactory.deploy(
315 | "Kanaria",
316 | "KAN",
317 | "ipfs://collectionMeta",
318 | "ipfs://tokenMeta",
319 | {
320 | erc20TokenAddress: ethers.constants.AddressZero,
321 | tokenUriIsEnumerable: true,
322 | royaltyRecipient: await owner.getAddress(),
323 | royaltyPercentageBps: 10,
324 | maxSupply: 1000,
325 | pricePerMint: pricePerMint
326 | }
327 | );
328 | const child: SimpleNestable = await contractFactory.deploy(
329 | "Chunky",
330 | "CHN",
331 | "ipfs://collectionMeta",
332 | "ipfs://tokenMeta",
333 | {
334 | erc20TokenAddress: ethers.constants.AddressZero,
335 | tokenUriIsEnumerable: true,
336 | royaltyRecipient: await owner.getAddress(),
337 | royaltyPercentageBps: 10,
338 | maxSupply: 1000,
339 | pricePerMint: pricePerMint
340 | }
341 | );
342 |
343 | await parent.deployed();
344 | await child.deployed();
345 | console.log(
346 | `Sample contracts deployed to ${parent.address} and ${child.address}`
347 | );
348 | }
349 |
350 | main().catch((error) => {
351 | console.error(error);
352 | process.exitCode = 1;
353 | });
354 | ````
355 |
356 | First thing that needs to be done after the smart contracts are deployed is to mint the NFTs. Minting the parent NFT is
357 | a straightforward process. We will use the `totalTokens` constant in order to specify how many of the parent tokens to
358 | mint:
359 |
360 | ````typescript
361 | console.log("Minting parent NFTs");
362 | let tx = await parent.mint(owner.address, totalTokens, {
363 | value: pricePerMint.mul(totalTokens),
364 | });
365 | await tx.wait();
366 | console.log("Minted totalTokens tokens");
367 | let totalSupply = await parent.totalSupply();
368 | console.log("Total parent tokens: %s", totalSupply);
369 | ````
370 |
371 | Minting child NFTs that should be nested is a different process. We will mint 2 nested NFTs for each parent NFT. If we
372 | examine the `nestMint` call that is being prepared, we can see that the first argument is the parent smart contract
373 | address, the second one is the amount of child NFTs to be nested to the given token and third is the ID of the parent
374 | token to which to nest the child. In this script, we will build a set of transactions to mint the nested tokens and then
375 | send them once they are all ready:
376 |
377 | ````typescript
378 | console.log("Minting child NFTs");
379 | let allTx: ContractTransaction[] = [];
380 | for (let i = 1; i <= totalTokens; i++) {
381 | let tx = await child.nestMint(parent.address, 2, i, {
382 | value: pricePerMint.mul(2),
383 | });
384 | allTx.push(tx);
385 | }
386 | console.log("Added 2 chunkies per kanaria");
387 | console.log("Awaiting for all tx to finish...");
388 | await Promise.all(allTx.map((tx) => tx.wait()));
389 |
390 | totalSupply = await child.totalSupply();
391 | console.log("Total child tokens: %s", totalSupply);
392 | ````
393 |
394 | Once the child NFTs are minted, we can examine the difference between `ownerOf` and `directOwnerOf` functions. The
395 | former should return the address of the root owner (which should be the `owner`'s address in our case) and the latter
396 | should return the array of values related to intended parent. The array is structured like this:
397 |
398 | ````json
399 | [
400 | address of the owner,
401 | token ID of the parent NFT,
402 | isNft boolean value
403 | ]
404 | ````
405 |
406 | In our case, the address of the owner should equal the parent token's smart contract, the ID should equal the parent
407 | NFT's ID and the boolean value of `isNft` should be set to `true`. If we would be calling the `directOwnerOf` one the
408 | parent NFT, the owner should be the same as the one returned from the `ownerOf`, ID should equal 0 and the `isNft` value
409 | should be set to `false`. The section covering these calls should look like this:
410 |
411 | ````typescript
412 | console.log("Inspecting child NFT with the ID of 1");
413 | let parentId = await child.ownerOf(1);
414 | let rmrkParent = await child.directOwnerOf(1);
415 | console.log("Chunky's id 1 owner is ", parentId);
416 | console.log("Chunky's id 1 rmrk owner is ", rmrkParent);
417 | console.log("Parent address: ", parent.address);
418 | ````
419 |
420 | For the nestable process to be completed, the `acceptChild` method should be called on the parent NFT:
421 |
422 | ````typescript
423 | console.log("Accepting the fist child NFT for the parent NFT with ID 1");
424 | tx = await parent.acceptChild(1, 0, child.address, 1);
425 | await tx.wait();
426 | ````
427 |
428 | The section of the script above accepted the child NFT with the ID of `1` at the index `0` for the parent NFT with the
429 | ID of `1` in the parent NFT's smart contract.
430 |
431 | **NOTE: When accepting the nested NFTs, the index of the pending NFT represents its index in a FIFO like stack. So
432 | having 2 NFTs in the pending stack, and accepting the one with the index of 0 will move the next one to this spot.
433 | Accepting the second NFT from the stack, after the first one was already accepted, should then be done by accepting
434 | the pending NFT with index of 0. So two identical calls in succession should accept both pending NFTs.**
435 |
436 | The parent NFT with ID 1 now has one accepted and one pending child NFTs. We can examine both using the `childrenOf` and
437 | `pendingChildren` methods:
438 |
439 | ````typescript
440 | console.log("Exaimning accepted and pending children of parent NFT with ID 1");
441 | console.log("Children: ", await parent.childrenOf(1));
442 | console.log("Pending: ", await parent.pendingChildrenOf(1));
443 | ````
444 |
445 | Both of these methods return the array of tokens contained in the list, be it for child NFTs or for pending NFTs. The
446 | array contains two values:
447 |
448 | - `contractAddress` is the address of the child NFT's smart contract
449 | - `tokenId` is the ID of the child NFT in its smart contract
450 |
451 | Once the NFT is nested, it can also be unnested. When doing so, the owner of the token should be specified, as they will
452 | be the ones owning the token from that point on (or until they nest or sell it). Additionally pending status has to be
453 | passed, as the procedure to unnest differs for the NFTs that have already been accepted from those that are still
454 | pending (passing `false` indicates that the child NFT has already been nested). We will remove the nested NFT with
455 | nestable ID of 0 from the parent NFT with ID 1:
456 |
457 | ````typescript
458 | console.log("Removing the nested NFT from the parent token with the ID of 1");
459 | tx = await parent.transferChild(1, owner.address, 0, 0, child.address, 1, false, "0x");
460 | await tx.wait();
461 | ````
462 |
463 | **NOTE: Unnesting the child NFT is done in the similar manner as accepting a pending child NFT. Once the nested NFT at
464 | ID 0 has been unnested the following NFT's IDs are reduced by 1.**
465 |
466 | Finally, let's observe the child NFT that we just unnested. We will use the `ownerOf` and `directOwnerOf` methods to
467 | observe it:
468 |
469 | ````typescript
470 | parentId = await child.ownerOf(1);
471 | rmrkParent = await child.directOwnerOf(1);
472 | console.log("Chunky's id 1 parent is ", parentId);
473 | console.log("Chunky's id 1 rmrk owner is ", rmrkParent);
474 | ````
475 |
476 | The `directOwnerOf` should return the address of the `owner` and the ID should be `0` as well as `isNft` should be
477 | `false`.
478 |
479 | With the user journey script concluded, we can add a custom helper to the [`package.json`](../../package.json) to make
480 | running it easier:
481 |
482 | ````json
483 | "user-journey-nestable": "hardhat run scripts/nestableUserJourney.ts"
484 | ````
485 |
486 | Running it using `npm run user-journey-nestable` should return the following output:
487 |
488 | ````shell
489 | npm run user-journey-nestable
490 |
491 | > @rmrk-team/evm-contract-samples@0.1.0 user-journey-nestable
492 | > hardhat run scripts/nestableUserJourney.ts
493 |
494 | Sample contracts deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3 and 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
495 | Minting parent NFTs
496 | Minted totalTokens tokens
497 | Total parent tokens: 5
498 | Minting child NFTs
499 | Added 2 chunkies per kanaria
500 | Awaiting for all tx to finish...
501 | Total child tokens: 10
502 | Inspecting child NFT with the ID of 1
503 | Chunky's id 1 owner is 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
504 | Chunky's id 1 rmrk owner is [
505 | '0x5FbDB2315678afecb367f032d93F642f64180aa3',
506 | BigNumber { value: "1" },
507 | true
508 | ]
509 | Parent address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
510 | Accepting the fist child NFT for the parent NFT with ID 1
511 | Exaimning accepted and pending children of parent NFT with ID 1
512 | Children: [
513 | [
514 | BigNumber { value: "1" },
515 | '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
516 | tokenId: BigNumber { value: "1" },
517 | contractAddress: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512'
518 | ]
519 | ]
520 | Pending: [
521 | [
522 | BigNumber { value: "2" },
523 | '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
524 | tokenId: BigNumber { value: "2" },
525 | contractAddress: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512'
526 | ]
527 | ]
528 | Removing the nested NFT from the parent token with the ID of 1
529 | Chunky's id 1 parent is 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
530 | Chunky's id 1 rmrk owner is [
531 | '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
532 | BigNumber { value: "0" },
533 | false
534 | ]
535 | ````
536 |
537 | This concludes our work on the [`SimpleNestable.sol`](./SimpleNestable.sol). We can now move on to examining the
538 | [`AdvancedNestable.sol`](./AdvancedNestable.sol).
539 |
540 | ## AdvancedNestable
541 |
542 | The `AdvancedNestable` smart contract allows for more flexibility when using the nestable lego. It implements minimum
543 | required implementation in order to be compatible with RMRK nestable, but leaves more business logic implementation
544 | freedom to the developer. It uses the
545 | [`RMRKNestable.sol`](https://github.com/rmrk-team/evm/blob/dev/contracts/RMRK/nestable/RMRKNestable.sol) import to gain
546 | access to the Nestable lego:
547 |
548 | ````solidity
549 | import "@rmrk-team/evm-contracts/contracts/RMRK/nestable/RMRKNestable.sol";
550 | ````
551 |
552 | We only need `name` and `symbol` of the NFT in order to properly initialize it after the `AdvancedNestable` inherits it:
553 |
554 | ````solidity
555 | contract AdvancedNestable is RMRKNestable {
556 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
557 | constructor(
558 | string memory name,
559 | string memory symbol
560 | )
561 | RMRKNestable(name, symbol)
562 | {
563 | // Custom optional: constructor logic
564 | }
565 | }
566 | ````
567 |
568 | This is all that is required in order to get you started with implementing the Nested RMRK lego.
569 |
570 |
571 | The minimal AdvancedNestable.sol should look like this:
572 |
573 | ````solidity
574 | // SPDX-License-Identifier: Apache-2.0
575 |
576 | pragma solidity ^0.8.18;
577 |
578 | import "@rmrk-team/evm-contracts/contracts/RMRK/nestable/RMRKNestable.sol";
579 |
580 |
581 | contract AdvancedNestable is RMRKNestable {
582 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
583 | constructor(
584 | string memory name,
585 | string memory symbol
586 | )
587 | RMRKNestable(name, symbol)
588 | {
589 | // Custom optional: constructor logic
590 | }
591 | }
592 | ````
593 |
594 |
595 |
596 | Using `RMRKNestable` requires custom implementation of minting logic. Available internal functions to use when writing
597 | it are:
598 |
599 | - `_mint(address to, uint256 tokenId)`
600 | - `_safeMint(address to, uint256 tokenId)`
601 | - `_safeMint(address to, uint256 tokenId, bytes memory data)`
602 | - `_nestMint(address to, uint256 tokenId, uint256 destinationId)`
603 |
604 | The latter is used to nest mint the NFT directly to the parent NFT. If you intend to support it at the minting stage,
605 | you should implement it in your smart contract.
606 |
607 | In addition to the minting functions, you should also implement the burning and transfer functions if they apply to your
608 | use case:
609 |
610 | - `_burn(uint256 tokenId)`
611 | - `transferFrom(address from, address to, uint256 tokenId)`
612 | - `nestTransfer(address from, address to, uint256 tokenId, uint256 destinationId)`
613 |
614 | Any additional function supporting your NFT use case and utility can also be added. Remember to thoroughly test your
615 | smart contracts with extensive test suites and define strict access control rules for the functions that you implement.
616 |
617 | Happy nesting! 🐣
--------------------------------------------------------------------------------
/contracts/Nestable/SimpleNestable.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.18;
3 |
4 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKNestableImpl.sol";
5 |
6 | contract SimpleNestable is RMRKNestableImpl {
7 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
8 | constructor(
9 | string memory name,
10 | string memory symbol,
11 | string memory collectionMetadata,
12 | string memory tokenURI,
13 | InitData memory data
14 | )
15 | RMRKNestableImpl(
16 | name,
17 | symbol,
18 | collectionMetadata,
19 | tokenURI,
20 | data
21 | )
22 | {}
23 | }
24 |
--------------------------------------------------------------------------------
/contracts/NestableMultiAsset/AdvancedNestableMultiAsset.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | pragma solidity ^0.8.18;
4 |
5 | import "@rmrk-team/evm-contracts/contracts/RMRK/nestable/RMRKNestableMultiAsset.sol";
6 |
7 | contract AdvancedNestableMultiAsset is RMRKNestableMultiAsset {
8 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
9 | constructor(string memory name, string memory symbol)
10 | RMRKNestableMultiAsset(name, symbol)
11 | {
12 | // Custom optional: constructor logic
13 | }
14 |
15 | // Custom expected: external, optionally gated, functions to mint.
16 | // Available internal functions:
17 | // _mint(address to, uint256 tokenId)
18 | // _safeMint(address to, uint256 tokenId)
19 | // _safeMint(address to, uint256 tokenId, bytes memory data)
20 |
21 | // Custom expected: external, optionally gated, functions to nest mint.
22 | // Available internal functions:
23 | // _nestMint(address to, uint256 tokenId, uint256 destinationId)
24 |
25 | // Custom expected: external gated function to burn.
26 | // Available internal functions:
27 | // _burn(uint256 tokenId)
28 |
29 | // Custom optional: utility functions to transfer and nest transfer from caller
30 | // Available public functions:
31 | // transferFrom(address from, address to, uint256 tokenId)
32 | // nestTransfer(address from, address to, uint256 tokenId, uint256 destinationId)
33 |
34 | // Custom expected: external, optionally gated, function to add assets.
35 | // Available internal functions:
36 | // _addAssetEntry(uint64 id, string memory metadataURI)
37 |
38 | // Custom expected: external, optionally gated, function to add assets to tokens.
39 | // Available internal functions:
40 | // _addAssetToToken(uint256 tokenId, uint64 assetId, uint64 replacesAssetWithId)
41 | }
42 |
--------------------------------------------------------------------------------
/contracts/NestableMultiAsset/README.md:
--------------------------------------------------------------------------------
1 | # Nestable with MultiAsset
2 |
3 | 
4 |
5 | [Nestable](../Nestable/README.md) and [MultiAsset](../MultiAsset/README.md) RMRK legos can be used together to
6 | provide more utility to the NFT. To examine each separately, please refer to their respective examples.
7 |
8 | ## Abstract
9 |
10 | In this tutorial we will examine the joined operation of the Nestable and MultiAsset RMRK blocks using two examples:
11 |
12 | - [SimpleNestableMultiAsset](./SimpleNestableMultiAsset.sol) is a minimal implementation of the Nestable and
13 | MultiAsset RMRK blocks operating together.
14 | - [AdvancedNestableMultiAsset](./AdvancedNestableMultiAsset.sol) is a more customizable implementation of the
15 | Nestable and MultiAsset RMRK blocks operating together.
16 |
17 | ## SimpleNestableMultiAsset
18 |
19 | The `SimpleNestableMultiasset` example uses the
20 | [`RMRKNestableMultiAssetImpl`](https://github.com/rmrk-team/evm/blob/dev/contracts/implementations/nativeTokenPay/RMRKNestableMultiAssetImpl.sol).
21 | It is used by using the `import` statement below the `pragma` definition:
22 |
23 | ````solidity
24 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKNestableMultiAssetImpl.sol";
25 | ````
26 |
27 | Once the `RMRKNestableMultiAsset.sol` is imported into our file, we can set the inheritance of our smart contract:
28 |
29 | ````solidity
30 | contract SimpleNestableMultiAsset is RMRKNestableMultiAssetImpl {
31 |
32 | }
33 | ````
34 |
35 | The `RMRKNestableMultiAssetImpl` implements all of the required functionality of the Nested and MultiAsset RMRK
36 | legos. It implements minting of parent NFTS as well as child NFTs. Management of NFT assets is also implemented
37 | alongside the classic NFT functionality.
38 |
39 | **WARNING: The `RMRKNestableMultiAssetImpl` only has minimal access control implemented. If you intend to use it, make
40 | sure to define your own, otherwise your smart contracts are at risk of unexpected behaviour.**
41 |
42 | The `constructor` in this case accepts no arguments as most of the arguments required to properly initialize
43 | `RMRKNestableMultiAssetImpl` are hardcoded:
44 |
45 | - `RMRKNestableMultiAssetImpl`: represents the `name` argument and sets the name of the collection
46 | - `SNMA`: represents the `symbol` argument and sets the symbol of the collection
47 | - `ipfs://meta`: represents the `collectionMetadata_` argument and sets the URI of the collection metadata
48 | - `ipfs://tokenMeta`: represents the `tokenURI_` argument and sets the base URI of the token metadata
49 |
50 | The only available variable to pass to the `constructor` is:
51 |
52 | - `data`: struct type of argument providing a number of initialization values, used to avoid initialization transaction
53 | being reverted due to passing too many parameters
54 |
55 | **NOTE: The `InitData` struct is used to pass the initialization parameters to the implementation smart contract. This
56 | is done so that the execution of the deploy transaction doesn't revert because we are trying to pass too many
57 | arguments.**
58 |
59 | **The `InitData` struct contains the following fields:**
60 |
61 | ````solidity
62 | [
63 | erc20TokenAddress,
64 | tokenUriIsEnumerable,
65 | royaltyRecipient,
66 | royaltyPercentageBps, // Expressed in basis points
67 | maxSupply,
68 | pricePerMint
69 | ]
70 | ````
71 |
72 | **NOTE: Basis points are the smallest supported denomination of percent. In our case this is one hundreth of a percent.
73 | This means that 1 basis point equals 0.01% and 10000 basis points equal 100%. So for example, if you want to set royalty
74 | percentage to 5%, the `royaltyPercentageBps` value should be 500.**
75 |
76 | With the arguments passed upon initialization defined, we can add our constructor:
77 |
78 | ````solidity
79 | constructor(InitData memory data)
80 | RMRKNestableMultiAssetImpl(
81 | "SimpleNestableMultiAsset",
82 | "SNMA",
83 | "ipfs://meta",
84 | "ipfs://tokenMeta",
85 | data
86 | )
87 | {}
88 | ````
89 |
90 |
91 | The SimpleNestableMultiAsset.sol should look like this:
92 |
93 | ````solidity
94 | // SPDX-License-Identifier: UNLICENSED
95 | pragma solidity ^0.8.18;
96 |
97 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKNestableMultiAssetImpl.sol";
98 |
99 | contract SimpleNestableMultiAsset is RMRKNestableMultiAssetImpl {
100 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
101 | constructor(InitData memory data)
102 | RMRKNestableMultiAssetImpl(
103 | "SimpleNestableMultiAsset",
104 | "SNMA",
105 | "ipfs://meta",
106 | "ipfs://tokenMeta",
107 | data
108 | )
109 | {}
110 | }
111 | ````
112 |
113 |
114 |
115 | ### RMRKNestableMultiAssetImpl
116 |
117 | Let's take a moment to examine the core of this implementation, the `RMRKNestableMultiAssetImpl`.
118 |
119 | It uses `RMRKRoyalties`, `RMRKNestableMultiAsset`, `RMRKCollectionMetadata` and `RMRKMintingUtils` smart contracts
120 | from `RMRK` stack. To dive deeper into their operation, please refer to their respective documentation.
121 |
122 | Two errors are defined:
123 |
124 | ````solidity
125 | error RMRKMintUnderpriced();
126 | error RMRKMintZero();
127 | ````
128 |
129 | `RMRKMintUnderpriced()` is used when not enough value is used when attempting to mint a token and `RMRKMintZero()` is
130 | used when attempting to mint 0 tokens.
131 |
132 | #### `mint`
133 |
134 | The `mint` function is used to mint parent NFTs and accepts two arguments:
135 |
136 | - `to`: `address` type of argument that specifies who should receive the newly minted tokens
137 | - `numToMint`: `uint256` type of argument that specifies how many tokens should be minted
138 |
139 | There are a few constraints to this function:
140 |
141 | - after minting, the total number of tokens should not exceed the maximum allowed supply
142 | - attempting to mint 0 tokens is not allowed as it makes no sense to pay for the gas without any effect
143 | - value should accompany transaction equal to a price per mint multiplied by the `numToMint`
144 |
145 | #### `nestMint`
146 |
147 | The `nestMint` function is used to mint child NFTs to be owned by the parent NFT and accepts three arguments:
148 |
149 | - `to`: `address` type of argument specifying the address of the smart contract to which the parent NFT belongs to
150 | - `numToMint`: `uint256` type of argument specifying the amount of tokens to be minted
151 | - `destinationId`: `uint256` type of argument specifying the ID of the parent NFT to which to mint the child NFT
152 |
153 | The constraints of `nestMint` are similar to the ones set out for `mint` function.
154 |
155 | #### `addAssetToToken`
156 |
157 | The `addAssetToToken` is used to add a new asset to the token and accepts three arguments:
158 |
159 | - `tokenId`: `uint256` type of argument specifying the ID of the token we are adding asset to
160 | - `assetId`: `uint64` type of argument specifying the ID of the asset we are adding to the token
161 | - `replacesAssetWithId`: `uint64` type of argument specifying the ID of the asset we are overwriting with the desired asset
162 |
163 | #### `addAssetEntry`
164 |
165 | The `addAssetEntry` function is used to add a new URI for the new asset of the token and accepts one argument:
166 |
167 | - `metadataURI`: `string` type of argument specifying the metadata URI of a new asset
168 |
169 | #### `totalAssets`
170 |
171 | The `totalAssets` function is used to retrieve a total number of assets defined in the collection.
172 |
173 | #### `transfer`
174 |
175 | The `transfer` function is used to transfer one token from one account to another and accepts two arguments:
176 |
177 | - `to`: `address` type of argument specifying the address of the account to which the token should be transferred to
178 | - `tokenId`: `uint256` type of argument specifying the token ID of the token to be transferred
179 |
180 | #### `nestTransfer`
181 |
182 | The `nestTransfer` is used to transfer the NFT to another NFT residing in a specified contract. It can only be called by
183 | a direct owner or a parent NFT's smart contract or a caller that was given the allowance. This will nest the given NFT
184 | into the specified one. It accepts three arguments:
185 |
186 | - `to`: `address` type of argument specifying the address of the intended parent NFT's smart contract
187 | - `tokenId`: `uint256` type of argument specifying the ID of the token we want to send to be nested
188 | - `destinationId`: `uint256` type of argument specifying the ID of the intended parent token NFT
189 |
190 | #### `tokenURI`
191 |
192 | The `tokenURI` is used to retrieve the metadata URI of the desired token and accepts one argument:
193 |
194 | - `tokenId`: `uint256` type of argument representing the token ID of which we are retrieving the URI
195 |
196 | #### `updateRoyaltyRecipient`
197 |
198 | The `updateRoyaltyRecipient` function is used to update the royalty recipient and accepts one argument:
199 |
200 | - `newRoyaltyRecipient`: `address` type of argument specifying the address of the new beneficiary recipient
201 |
202 | ### Deploy script
203 |
204 | The deploy script for the `SimpleNestableMultiAsset` smart contract resides in the
205 | [`deployNestableMultiAsset.ts`](../../scripts/deployNestableMultiAsset.ts).
206 |
207 | The script uses the `ethers`, `SimpleNestable` and `ContractTransaction` imports. The empty deploy script should look like
208 | this:
209 |
210 | ````typescript
211 | import { ethers } from "hardhat";
212 | import { SimpleNestable } from "../typechain-types";
213 | import { ContractTransaction } from "ethers";
214 |
215 | async function main() {
216 |
217 | }
218 |
219 | main().catch((error) => {
220 | console.error(error);
221 | process.exitCode = 1;
222 | });
223 | ````
224 |
225 | Before we can deploy the parent and child smart contracts, we should prepare the constants that we will use in the
226 | script:
227 |
228 | ````typescript
229 | const pricePerMint = ethers.utils.parseEther("0.0000000001");
230 | const totalTokens = 5;
231 | const [owner] = await ethers.getSigners();
232 | ````
233 |
234 | Now that the constants are ready, we can deploy the smart contract and log the addresses of the contracts to the
235 | console:
236 |
237 | ````typescript
238 | const contractFactory = await ethers.getContractFactory(
239 | "SimpleNestableMultiAsset"
240 | );
241 | const token: SimpleNestableMultiAsset = await contractFactory.deploy(
242 | {
243 | erc20TokenAddress: ethers.constants.AddressZero,
244 | tokenUriIsEnumerable: true,
245 | royaltyRecipient: await owner.getAddress(),
246 | royaltyPercentageBps: 10,
247 | maxSupply: 1000,
248 | pricePerMint: pricePerMint
249 | }
250 | );
251 |
252 | await token.deployed();
253 | console.log(`Sample contract deployed to ${token.address}`);
254 | ````
255 |
256 | A custom script added to [`package.json`](../../package.json) allows us to easily run the script:
257 |
258 | ````json
259 | "scripts": {
260 | "deploy-nestable-multi-asset": "hardhat run scripts/deployNestableMultiAsset.ts"
261 | }
262 | ````
263 |
264 | Using the script with `npm run deploy-nestable-multi-asset` should return the following output:
265 |
266 | ````shell
267 | npm run deploy-nestable-multi-asset
268 |
269 | > @rmrk-team/evm-contract-samples@0.1.0 deploy-nestable-multi-asset
270 | > hardhat run scripts/deployNestableMultiAsset.ts
271 |
272 | Sample contract deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
273 | ````
274 |
275 | ### User journey
276 |
277 | With the deploy script ready, we can examine how the journey of a user using nestable with multi asset would look like
278 | using this smart contract.
279 |
280 | The base of it is the same as the deploy script, as we need to deploy the smart contract in order to interact with it:
281 |
282 | ````typescript
283 | import { ethers } from "hardhat";
284 | import { SimpleNestableMultiAsset } from "../typechain-types";
285 | import { ContractTransaction } from "ethers";
286 |
287 | async function main() {
288 | const pricePerMint = ethers.utils.parseEther("0.0000000001");
289 | const totalTokens = 5;
290 | const [ owner, tokenOwner] = await ethers.getSigners();
291 |
292 | const contractFactory = await ethers.getContractFactory(
293 | "SimpleNestableMultiAsset"
294 | );
295 | const token: SimpleNestableMultiAsset = await contractFactory.deploy(
296 | {
297 | erc20TokenAddress: ethers.constants.AddressZero,
298 | tokenUriIsEnumerable: true,
299 | royaltyRecipient: await owner.getAddress(),
300 | royaltyPercentageBps: 10,
301 | maxSupply: 1000,
302 | pricePerMint: pricePerMint
303 | }
304 | );
305 |
306 | await token.deployed();
307 | console.log(`Sample contract deployed to ${token.address}`);
308 | }
309 |
310 | main().catch((error) => {
311 | console.error(error);
312 | process.exitCode = 1;
313 | });
314 | ````
315 |
316 | **NOTE: We assign the `tokenOwner` the second available signer, so that the assets are not automatically accepted when
317 | added to the token. This happens when an account adding an asset to a token is also the owner of said token.**
318 |
319 | First thing that needs to be done after the smart contracts are deployed is to mint the NFTs. We will use the
320 | `totalTokens` constant in order to specify how many of the tokens to mint:
321 |
322 | ````typescript
323 | console.log("Minting NFTs");
324 | let tx = await token.mint(tokenOwner.address, totalTokens, {
325 | value: pricePerMint.mul(totalTokens),
326 | });
327 | await tx.wait();
328 | console.log(`Minted ${totalTokens} tokens`);
329 | const totalSupply = await token.totalSupply();
330 | console.log("Total tokens: %s", totalSupply);
331 | ````
332 |
333 | Now that the tokens are minted, we can add new assets to the smart contract. We will prepare a batch of transactions
334 | that will add simple IPFS metadata link for the assets in the smart contract. Once the transactions are ready, we
335 | will send them and get all of the assets to output to the console:
336 |
337 | ````typescript
338 | console.log("Adding assets");
339 | let allTx: ContractTransaction[] = [];
340 | for (let i = 1; i <= totalTokens; i++) {
341 | let tx = await token.addAssetEntry(`ipfs://metadata/${i}.json`);
342 | allTx.push(tx);
343 | }
344 | console.log(`Added ${totalTokens} assets`);
345 |
346 | console.log("Awaiting for all tx to finish...");
347 | await Promise.all(allTx.map((tx) => tx.wait()));
348 | ````
349 |
350 | Once the assets are added to the smart contract we can assign each asset to one of the tokens:
351 |
352 | ````typescript
353 | console.log("Adding assets to tokens");
354 | allTx = [];
355 | for (let i = 1; i <= totalTokens; i++) {
356 | // We give each token a asset id with the same number. This is just a coincidence, not a restriction.
357 | let tx = await token.addAssetToToken(i, i, 0);
358 | allTx.push(tx);
359 | console.log(`Added asset ${i} to token ${i}.`);
360 | }
361 | console.log("Awaiting for all tx to finish...");
362 | await Promise.all(allTx.map((tx) => tx.wait()));
363 | ````
364 |
365 | After the assets are added to the NFTs, we have to accept them. We will do this by once again building a batch of
366 | transactions for each of the tokens and send them out one by one at the end:
367 |
368 | ````typescript
369 | console.log("Accepting assets to tokens");
370 | allTx = [];
371 | for (let i = 1; i <= totalTokens; i++) {
372 | // Accept pending asset for each token (on index 0)
373 | let tx = await token.connect(tokenOwner).acceptAsset(i, 0, i);
374 | allTx.push(tx);
375 | console.log(`Accepted first pending asset for token ${i}.`);
376 | }
377 | console.log("Awaiting for all tx to finish...");
378 | await Promise.all(allTx.map((tx) => tx.wait()));
379 | ````
380 |
381 | **NOTE: Accepting assets is done in a array that gets elements, new assets, appended to the end of it. Once the asset is
382 | accepted, the asset that was added lats, takes its place. For example:**
383 |
384 | **We have assets `A`, `B`, `C` and `D` in the pending array organised like this: [`A`, `B`, `C`, `D`].**
385 |
386 | **Accepting the asset `A` updates the array to look like this: [`D`, `B`, `C`].**
387 |
388 | **Accepting the asset `B` updates the array to look like this: [`A`, `D`, `C`].**
389 |
390 | Having accepted the assets, we can check that the URIs are assigned as expected:
391 |
392 | ````typescript
393 | console.log("Getting URIs");
394 | const uriToken1 = await token.tokenURI(1);
395 | const uriToken5 = await token.tokenURI(totalTokens);
396 |
397 | console.log("Token 1 URI: ", uriToken1);
398 | console.log("Token totalTokens URI: ", uriToken5);
399 | ````
400 |
401 | With the assets properly assigned to the tokens, we can now nest the token with ID 5 into the token with ID 1 and
402 | check their ownership to verify successful nesting:
403 |
404 | ````typescript
405 | console.log("Nesting token with ID 5 into token with ID 1");
406 | await token.connect(tokenOwner).nestTransferFrom(tokenOwner.address, token.address, 5, 1, "0x");
407 | const parentId = await token.ownerOf(5);
408 | const rmrkParent = await token.directOwnerOf(5);
409 | console.log("Token's id 5 owner is ", parentId);
410 | console.log("Token's id 5 rmrk owner is ", rmrkParent);
411 | ````
412 |
413 | We can now add a custom helper to the [`package.json`](../../package.json) to make running it easier:
414 |
415 | ````json
416 | "user-journey-nestable-multi-asset": "hardhat run scripts/nestableMultiAssetUserJourney.ts"
417 | ````
418 |
419 | Running it using `npm run user-journey-nestable-multi-asset` should return the following output:
420 |
421 | ````shell
422 | npm run user-journey-nestable-multi-asset
423 |
424 | > @rmrk-team/evm-contract-samples@0.1.0 user-journey-nestable-multi-asset
425 | > hardhat run scripts/nestableMultiAssetUserJourney.ts
426 |
427 | Sample contract deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
428 | Minting NFTs
429 | Minted 5 tokens
430 | Total tokens: 5
431 | Adding assets
432 | Added 5 assets
433 | Awaiting for all tx to finish...
434 | All assets: [
435 | BigNumber { value: "1" },
436 | BigNumber { value: "2" },
437 | BigNumber { value: "3" },
438 | BigNumber { value: "4" },
439 | BigNumber { value: "5" }
440 | ]
441 | Adding assets to tokens
442 | Added asset 1 to token 1.
443 | Added asset 2 to token 2.
444 | Added asset 3 to token 3.
445 | Added asset 4 to token 4.
446 | Added asset 5 to token 5.
447 | Awaiting for all tx to finish...
448 | Accepting assets to tokens
449 | Accepted first pending asset for token 1.
450 | Accepted first pending asset for token 2.
451 | Accepted first pending asset for token 3.
452 | Accepted first pending asset for token 4.
453 | Accepted first pending asset for token 5.
454 | Awaiting for all tx to finish...
455 | Getting URIs
456 | Token 1 URI: ipfs://metadata/1.json
457 | Token totalTokens URI: ipfs://metadata/5.json
458 | Nesting token with ID 5 into token with ID 1
459 | Token's id 5 owner is 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
460 | Token's id 5 rmrk owner is [
461 | '0x5FbDB2315678afecb367f032d93F642f64180aa3',
462 | BigNumber { value: "1" },
463 | true
464 | ]
465 | ````
466 |
467 | This concludes our work on the [`SimpleNestableMultiAsset.sol`](./SimpleNestableMultiAsset.sol). We can now move on
468 | to examining the [`AdvancedNestableMultiAsset`](./AdvancedNestableMultiAsset.sol).
469 |
470 | ## AdvancedNestableMultiAsset
471 |
472 | The `AdvancedNestableMultiAsset` smart contract allows for more flexibility when using the nestable and multi asset
473 | legos together. It implements the minimum required implementation in order to be compatible with RMRK nestable and multi
474 | asset, but leaves more business logic implementation freedom to the developer. It uses the
475 | [`RMRKNestableMultiAsset.sol`](https://github.com/rmrk-team/evm/blob/dev/contracts/RMRK/nestable/RMRKNestableMultiAsset.sol)
476 | import to gain access to the joined Nestable and Multi asset legos:
477 |
478 | ````solidity
479 | import "@rmrk-team/evm-contracts/contracts/RMRK/nestable/RMRKNestableMultiAsset.sol";
480 | ````
481 |
482 | We only need `name` and `symbol` of the NFT in order to properly initialize it after the `AdvancedNestableMultiAsset`
483 | inherits it:
484 |
485 | ````solidity
486 | contract AdvancedNestableMultiAsset is RMRKNestableMultiAsset {
487 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
488 | constructor(
489 | string memory name,
490 | string memory symbol
491 | )
492 | RMRKNestableMultiAsset(name, symbol)
493 | {
494 | // Custom optional: constructor logic
495 | }
496 | }
497 | ````
498 |
499 | This is all that is required to get you started with implementing the joined Nestable and Multi asset RMRK legos.
500 |
501 |
502 | The minimal AdvancedNestableMultiAsset.sol should look like this:
503 |
504 | ````solidity
505 | // SPDX-License-Identifier: Apache-2.0
506 |
507 | pragma solidity ^0.8.18;
508 |
509 | import "@rmrk-team/evm-contracts/contracts/RMRK/nestable/RMRKNestableMultiAsset.sol";
510 |
511 | contract AdvancedNestableMultiAsset is RMRKNestableMultiAsset {
512 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
513 | constructor(
514 | string memory name,
515 | string memory symbol
516 | )
517 | RMRKNestableMultiAsset(name, symbol)
518 | {
519 | // Custom optional: constructor logic
520 | }
521 | }
522 | ````
523 |
524 |
525 |
526 | Using `RMRKNestableMultiAsset` requires custom implementation of minting logic. Available internal functions to use when writing it are:
527 |
528 | - `_mint(address to, uint256 tokenId)`
529 | - `_safeMint(address to, uint256 tokenId)`
530 | - `_safeMint(address to, uint256 tokenId, bytes memory data)`
531 | - `_nestMint(address to, uint256 tokenId, uint256 destinationId)`
532 |
533 | The latter is used to nest mint the NFT directly to the parent NFT. If you intend to support it at the minting stage,
534 | you should implement it in your smart contract.
535 |
536 | In addition to the minting functions, you should also implement the burning, transfer and asset management functions if they apply to your use case:
537 |
538 | - `_burn(uint256 tokenId)`
539 | - `transferFrom(address from, address to, uint256 tokenId)`
540 | - `nestTransfer(address from, address to, uint256 tokenId, uint256 destinationId)`
541 | - `_addAssetEntry(uint64 id, string memory metadataURI)`
542 | - `_addAssetToToken(uint256 tokenId, uint64 assetId, uint64 replacesAssetWithId)`
543 |
544 | Any additional function supporting your NFT use case and utility can also be added. Remember to thoroughly test your
545 | smart contracts with extensive test suites and define strict access control rules for the functions that you implement.
546 |
547 | Happy multiassetful nesting! 🐣🫧🐣
--------------------------------------------------------------------------------
/contracts/NestableMultiAsset/SimpleNestableMultiAsset.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.18;
3 |
4 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKNestableMultiAssetImpl.sol";
5 |
6 | contract SimpleNestableMultiAsset is RMRKNestableMultiAssetImpl {
7 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
8 | constructor(InitData memory data)
9 | RMRKNestableMultiAssetImpl(
10 | "SimpleNestableMultiAsset",
11 | "SNMA",
12 | "ipfs://meta",
13 | "ipfs://tokenMeta",
14 | data
15 | )
16 | {}
17 | }
18 |
--------------------------------------------------------------------------------
/contracts/README.md:
--------------------------------------------------------------------------------
1 | # RMRK EVM implementation examples
2 |
3 | This section contains the examples of using RMRK legos to build your own smart contracts. Every example uses the
4 | `@rmrk-team/evm-contracts` dependency in order to implement the examples. Adding it allows you to easily include them in
5 | your own smart contracts.
6 |
7 | The examples included are:
8 |
9 | 1. [Nestable](./Nestable/README.md) examines the Nestable RMRK lego.
10 | 2. [MultiAsset](./MultiAsset/README.md) examines the MultiAsset RMRK lego.
11 | 3. [NestableMultiAsset](./NestableMultiAsset/) examines the Nestable and MultiAsset RMRK legos operating together.
12 | 4. [MegredEquippable](./MergedEquippable/README.md) examines the Merged equippable RMRK lego composite.
13 | 5. [SplitEquippable](./SplitEquippable/README.md) examines the Split equippable RMRK lego composite.
14 |
15 | **NOTE: This is a living collection of examples, which will get updated as the RMRK EVM implementation evolves.**
--------------------------------------------------------------------------------
/contracts/SimpleCatalog.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.18;
3 |
4 | import "@rmrk-team/evm-contracts/contracts/implementations/RMRKCatalogImpl.sol";
5 |
6 | contract SimpleCatalog is RMRKCatalogImpl {
7 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
8 | constructor(string memory symbol, string memory type_)
9 | RMRKCatalogImpl(symbol, type_)
10 | {}
11 | }
12 |
--------------------------------------------------------------------------------
/contracts/SplitEquippable/AdvancedExternalEquip.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | pragma solidity ^0.8.18;
4 |
5 | import "@rmrk-team/evm-contracts/contracts/RMRK/equippable/RMRKExternalEquip.sol";
6 |
7 | /* import "hardhat/console.sol"; */
8 |
9 | contract AdvancedExternalEquip is RMRKExternalEquip {
10 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
11 | constructor(address nestableAddress) RMRKExternalEquip(nestableAddress) {
12 | // Custom optional: constructor logic
13 | }
14 |
15 | // Custom optional: external gated function to set nestableAddress
16 | // Available internal functions:
17 | // _setNestableAddress(address nestableAddress)
18 |
19 | // Custom expected: external, optionally gated, function to add assets.
20 | // Available internal functions:
21 | // _addAssetEntry(uint64 id, uint64 equippableGroupId, address catalogAddress, string memory metadataURI, uint64[] calldata partIds)
22 |
23 | // Custom expected: external, optionally gated, function to add assets to tokens.
24 | // Available internal functions:
25 | // _addAssetToToken(uint256 tokenId, uint64 assetId, uint64 replacesAssetWithId)
26 |
27 | // Custom expected: external, optionally gated, function to add set valid parent reference Id.
28 | // Available internal functions:
29 | // _setValidParentForEquippableGroup(uint64 equippableGroupId, address parentAddress, uint64 slotPartId)
30 | }
31 |
--------------------------------------------------------------------------------
/contracts/SplitEquippable/AdvancedNestableExternalEquip.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | pragma solidity ^0.8.18;
4 |
5 | import "@rmrk-team/evm-contracts/contracts/RMRK/equippable/RMRKNestableExternalEquip.sol";
6 |
7 | contract AdvancedNestableExternalEquip is RMRKNestableExternalEquip {
8 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
9 | constructor(string memory name, string memory symbol)
10 | RMRKNestableExternalEquip(name, symbol)
11 | {
12 | // Custom optional: constructor logic
13 | }
14 |
15 | // Custom expected: external, optionally gated, functions to mint.
16 | // Available internal functions:
17 | // _mint(address to, uint256 tokenId)
18 | // _safeMint(address to, uint256 tokenId)
19 | // _safeMint(address to, uint256 tokenId, bytes memory data)
20 |
21 | // Custom expected: external, optionally gated, functions to nest mint.
22 | // Available internal functions:
23 | // _nestMint(address to, uint256 tokenId, uint256 destinationId)
24 |
25 | // Custom expected: external gated function to burn.
26 | // Available internal functions:
27 | // _burn(uint256 tokenId)
28 |
29 | // Custom optional: external gated function to set equippableAddress
30 | // Available internal functions:
31 | // _setEquippableAddress(address equippable)
32 |
33 | // Custom optional: utility functions to transfer and nest transfer from caller
34 | // Available public functions:
35 | // transferFrom(address from, address to, uint256 tokenId)
36 | // nestTransfer(address from, address to, uint256 tokenId, uint256 destinationId)
37 | }
38 |
--------------------------------------------------------------------------------
/contracts/SplitEquippable/README.md:
--------------------------------------------------------------------------------
1 | # SplitEquippable
2 |
3 | 
4 |
5 | The `ExternalEquippable` composite of RMRK legos uses the [`Nestable`](../Nestable/README.md),
6 | [`MultiAsset`](../MultiAsset/README.md), [`Equippable`](../MergedEquippable/README.md#equippable) and
7 | [`Catalog`](../MergedEquippable/README.md#catalog) RMRK legos. Unlike [`MergedEquippable`](../MergedEquippable/README.md) RMRK
8 | lego composite, the external equippable splits `Nestable` apart from `MultiAsset` and `Equippable` in order to provide
9 | more space for custom business logic implementation.
10 |
11 | ## Abstract
12 |
13 | In this tutorial we will examine the SplitEquippable composite of RMRK blocks:
14 |
15 | - [`SimpleNestableExternalEquip`](./SimpleNestableExternalEquip.sol), [`SimpleExternalEquip`](./SimpleExternalEquip.sol)
16 | and [SimpleCatalog](../SimpleCatalog.sol) work together to showcase the minimal implementation of SplitEquippable RMRK lego
17 | composite.
18 | - [`AdvancedNestableExternalEquip`](./AdvancedNestableExternalEquip.sol),
19 | [`AdvancedExternalEquip`](./AdvancedExternalEquip.sol) and [`AdvancedCatalog`](../AdvancedCatalog.sol) work together to
20 | showcase a more customizable implementation of the SplitEquippable RMRK lego composite.
21 |
22 | Let's first examine the simple, minimal, implementation and then move on to the advanced one.
23 |
24 | ## Simple SplitEquippable
25 |
26 | The simple `SplitEquippable` consists of three smart contracts. The [`SimpleCatalog`](../MergedEquippable/README.md#catalog)
27 | is already examined in the `MergedEquippable` documentation. Let's first examine the `SimpleExternalEquip` and then move
28 | on to the `SimpleNestableExternalEquip`.
29 |
30 | **NOTE: As the `SimpleCatalog` smart contract is used by both `MergedEquippable` as well as `SplitEquippable` it resides
31 | in the root `contracts/` directory.**
32 |
33 | ### SimpleExternalEquip
34 |
35 | The `SimpleExternalEquip` example uses the
36 | [`RMRKExternalEquipImpl`](https://github.com/rmrk-team/evm/blob/dev/contracts/implementations/RMRKExternalEquipImpl.sol).
37 | It is used by importing it using the `import` statement below the `pragma` definition:
38 |
39 | ````solidity
40 | import "@rmrk-team/evm-contracts/contracts/implementations/RMRKExternalEquipImpl.sol";
41 | ````
42 |
43 | Once the `RMRKExternalEquipImpl.sol` is imported into our file, we can set the inheritance of our smart contract:
44 |
45 | ````solidity
46 | contract SimpleExternalEquip is RMRKExternalEquipImpl {
47 |
48 | }
49 | ````
50 |
51 | The `RMRKExternalEquipImpl` implements all of the required functionality of the `Equippable` and `MultiAsset` RMRK
52 | legos. It implements asset and equippable management.
53 |
54 | **WARNING: The `RMRKExternalEquipImpl` only has minimal access control implemented. If you intend to use it, make sure to
55 | define your own, otherwise your smart contracts are at risk of unexpected behaviour.**
56 |
57 | The constructor to initialize the `RMRKExternalEquipImpl` accepts the following arguments:
58 |
59 | - `nestableAddress`: `address` type of argument specifying the address of the deployed `SimpleNestableExternalEquip` smart
60 | contract
61 |
62 | In order to properly initiate the inherited smart contract, our smart contract needs to accept the before mentioned argument in the `constructor` and pass it to the `RMRKExternalEquipImpl`:
63 |
64 | ````solidity
65 | constructor(
66 | address nestableAddress
67 | ) RMRKExternalEquipImpl(nestableAddress) {}
68 | ````
69 |
70 |
71 | The SimpleExternalEquip.sol should look like this:
72 |
73 | ````solidity
74 | // SPDX-License-Identifier: UNLICENSED
75 | pragma solidity ^0.8.18;
76 |
77 | import "@rmrk-team/evm-contracts/contracts/implementations/RMRKExternalEquipImpl.sol";
78 |
79 | contract SimpleExternalEquip is RMRKExternalEquipImpl {
80 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
81 | constructor(
82 | address nestableAddress
83 | ) RMRKExternalEquipImpl(nestableAddress) {}
84 | }
85 | ````
86 |
87 |
88 |
89 | #### RMRKExternalEquipImpl
90 |
91 | Let's take a moment to examine the core of this implementation, the `RMRKExternalEquipImpl`.
92 |
93 | It uses the `RMRKExternalEquip` and `OwnableLock` smart contracts from `RMRK` stack as well as `Strings` utility from
94 | `OpenZeppelin`. To dive deeper into their operation, please refer to their respective documentation.
95 |
96 | The following functions are available:
97 |
98 | ##### `addAssetToToken`
99 |
100 | The `addAssetToToken` is used to add a new asset to the token and accepts three arguments:
101 |
102 | - `tokenId`: `uint256` type of argument specifying the ID of the token we are adding asset to
103 | - `assetId`: `uint64` type of argument specifying the ID of the asset we are adding to the token
104 | - `replacesAssetWithId`: `uint64` type of argument specifying the ID of the asset we are overwriting with the desired
105 | asset
106 |
107 | ##### `addAssetEntry`
108 |
109 | The `addAssetEntry` is used to add an asset entry:
110 |
111 | - `metadataURI`: `string` type of argument specifying the metadata URI of a new asset
112 | - `equippableGroupId`: `uint64` type of argument specifying the ID of the group this asset belongs to. This ID
113 | can then be referenced in the `setValidParentRefId` in order to allow every asset with this equippable
114 | reference ID to be equipped into an NFT
115 | - `catalogAddress`: `address` type of argument specifying the address of the Catalog smart contract
116 | - `metadataURI`: `string` type of argument specifying the URI of the asset
117 | - `partIds`: `uint64[]` type of argument specifying the fixed and slot parts IDs for this asset
118 |
119 | ##### `setValidParentForEquippableGroup`
120 |
121 | The `setValidParentForEquippableGroup` is used to declare which assets are equippable into the parent address at the
122 | given slot and accepts three arguments:
123 |
124 | - `equippableGroupId`: `uint64` type of argument specifying the group of assets that can be equipped
125 | - `parentAddress`: `address` type of argument specifying the address into which the asset is equippable
126 | - `slotPartId`: `uint64` type of argument specifying the ID of the part it can be equipped to
127 |
128 | #### `totalAssets`
129 |
130 | The `totalAssets` is used to retrieve a total number of assets defined in the collection.
131 |
132 | ### SimpleNestableExternalEquip
133 |
134 | The `SimpleNestableExternalEquip` example uses the
135 | [`RMRKNestableExternalEquipImpl`](https://github.com/rmrk-team/evm/blob/dev/contracts/implementations/RMRKNestableExternalEquipImpl.sol).
136 | It is used by importing it using the `import` statement below the `pragma` definition:
137 |
138 | ````solidity
139 | import "@rmrk-team/evm-contracts/contracts/implementations/RMRKNestableExternalEquipImpl.sol";
140 | ````
141 |
142 | Once the `RMRKNestableExternalEquipImpl.sol` is imported into our file, we can set the inheritance of our smart contract:
143 |
144 | ````solidity
145 | contract SimpleNestableExternalEquip is RMRKNestableExternalEquipImpl {
146 |
147 | }
148 | ````
149 |
150 | The `RMRKNestableExternalEquipImpl` implements all of the functionality of the `Nestable` RMRK lego block. It implements
151 | minting and burning of the NFTs as well as setting the equippable address.
152 |
153 | **WARNING: The `RMRKNestableExternalEquipImpl` only has minimal access control implemented. If you intend to use it, make
154 | sure to define your own, otherwise your smart contracts are at risk of unexpected behaviour.**
155 |
156 | The `constructor` to initialize the `RMRKNestableExternalEquipImpl` accepts the following arguments:
157 |
158 | - `equippableAddress`: `address` type of argument specifying the address of the `SimpleExternalEquip` smart contract
159 | - `name`: `string` type of argument specifying the name of the collection
160 | - `symbol`: `string` type of argument specifying the symbol of the collection
161 | - `collectionMetadata`: `string` type of argument specifying the metadata URI of the whole collection
162 | - `tokenURI`: `string` type of argument specifying the base URI of the token metadata
163 | - `data`: struct type of argument providing a number of initialization values, used to avoid initialization transaction
164 | being reverted due to passing too many parameters
165 |
166 | **NOTE: The `InitData` struct is used to pass the initialization parameters to the implementation smart contract. This
167 | is done so that the execution of the deploy transaction doesn't revert because we are trying to pass too many
168 | arguments.**
169 |
170 | **The `InitData` struct contains the following fields:**
171 |
172 | ````solidity
173 | [
174 | erc20TokenAddress,
175 | tokenUriIsEnumerable,
176 | royaltyRecipient,
177 | royaltyPercentageBps, // Expressed in basis points
178 | maxSupply,
179 | pricePerMint
180 | ]
181 | ````
182 |
183 | **NOTE: Basis points are the smallest supported denomination of percent. In our case this is one hundreth of a percent.
184 | This means that 1 basis point equals 0.01% and 10000 basis points equal 100%. So for example, if you want to set royalty
185 | percentage to 5%, the `royaltyPercentageBps` value should be 500.**
186 |
187 | In order to properly initialize the inherited smart contract, our smart contract needs to accept the arguments,
188 | mentioned above, in the `constructor` and pass them to the `RMRKNestableExternalEquipImpl`:
189 |
190 | ````solidity
191 | constructor(
192 | address equippableAddress,
193 | string memory name,
194 | string memory symbol,
195 | string memory collectionMetadata,
196 | string memory tokenURI,
197 | InitData memory data
198 | )
199 | RMRKNestableExternalEquipImpl(
200 | equippableAddress,
201 | name,
202 | symbol,
203 | collectionMetadata,
204 | tokenURI,
205 | data
206 | )
207 | {}
208 | ````
209 |
210 |
211 | The SimpleNestableExternalEquip.sol should look like this:
212 |
213 | ````solidity
214 | // SPDX-License-Identifier: UNLICENSED
215 | pragma solidity ^0.8.18;
216 |
217 | import "@rmrk-team/evm-contracts/contracts/implementations/RMRKNestableExternalEquipImpl.sol";
218 |
219 | contract SimpleNestableExternalEquip is RMRKNestableExternalEquipImpl {
220 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
221 | constructor(
222 | address equippableAddress,
223 | string memory name,
224 | string memory symbol,
225 | string memory collectionMetadata,
226 | string memory tokenURI,
227 | InitData memory data
228 | )
229 | RMRKNestableExternalEquipImpl(
230 | equippableAddress,
231 | name,
232 | symbol,
233 | collectionMetadata,
234 | tokenURI,
235 | data
236 | )
237 | {}
238 | }
239 | ````
240 |
241 |
242 |
243 | #### RMRKNestableExternalEquipImpl
244 |
245 | Let's take a moment to examine the core of this implementation, the `RMRKNestableExternalEquipImpl`.
246 |
247 | It uses the `RMRKNestableExternalEquip`, `RMRKRoyalties`, `RMRKCollectionMetadata` and `RMRKMintingUtils` smart contracts
248 | from RMRK stack. To dive deeper into their operation, please refer to their respective documentation.
249 |
250 | Two errors are defined:
251 |
252 | ````solidity
253 | error RMRKMintUnderpriced();
254 | error RMRKMintZero();
255 | ````
256 |
257 | `RMRKMintUnderpriced()` is used when not enough value is used when attempting to mint a token and `RMRKMintZero()` is
258 | used when attempting to mint 0 tokens.
259 |
260 | **WARNING: The `RMRKMultiAssetImpl` only has minimal access control implemented. If you intend to use it, make sure
261 | to define your own, otherwise your smart contracts are at risk of unexpected behaviour.**
262 |
263 | Let's examine the available methods:
264 |
265 | ##### `mint`
266 |
267 | The `mint` function is used to mint parent NFTs and accepts two arguments:
268 |
269 | - `to`: `address` type of argument that specifies who should receive the newly minted tokens
270 | - `numToMint`: `uint256` type of argument that specifies how many tokens should be minted
271 |
272 | There are a few constraints to this function:
273 |
274 | - after minting, the total number of tokens should not exceed the maximum allowed supply
275 | - attempting to mint 0 tokens is not allowed as it makes no sense to pay for the gas without any effect
276 | - value should accompany transaction equal to a price per mint multiplied by the `numToMint`
277 |
278 | ##### `nestMint`
279 |
280 | The `nestMint` function is used to mint child NFTs to be owned by the parent NFT and accepts three arguments:
281 |
282 | - `to`: `address` type of argument specifying the address of the smart contract to which the parent NFT belongs to
283 | - `numToMint`: `uint256` type of argument specifying the amount of tokens to be minted
284 | - `destinationId`: `uint256` type of argument specifying the ID of the parent NFT to which to mint the child NFT
285 |
286 | The constraints of `nestMint` are similar to the ones set out for `mint` function.
287 |
288 | ##### `setEquippableAddress`
289 |
290 | The `setEquippableAddress` function is used to set the address of a deployed `SimpleExternalEquip` smart contract and
291 | accepts one argument:
292 |
293 | - `equippable`: `address` type of argument specifying the address of a deployed `SimpleExternalEquip` smart contract
294 |
295 | #### `tokenURI`
296 |
297 | The `tokenURI` is used to retrieve the metadata URI of the desired token and accepts one argument:
298 |
299 | - `tokenId`: `uint256` type of argument representing the token ID of which we are retrieving the URI
300 |
301 | #### `updateRoyaltyRecipient`
302 |
303 | The `updateRoyaltyRecipient` function is used to update the royalty recipient and accepts one argument:
304 |
305 | - `newRoyaltyRecipient`: `address` type of argument specifying the address of the new beneficiary recipient
306 |
307 | ### Deploy script
308 |
309 | The deploy script for the simple `SplitEquippable` resides in the
310 | [`deploySplitEquippable.ts`](../../scripts/deploySplitEquippable.ts).
311 |
312 | The deploy script uses the `ethers`, `SimpleCatalog`, `SimpleEquippable`, `SimpleNestableExternalEquip`,
313 | `RMRKEquipRenderUtils` and `ContractTransaction` imports. We will also define the `pricePerMint` constant, which will be
314 | used to set the minting price of the tokens. The empty deploy script should look like this:
315 |
316 | ````typescript
317 | import { ethers } from "hardhat";
318 | import {
319 | SimpleCatalog,
320 | SimpleExternalEquip,
321 | SimpleNestableExternalEquip,
322 | RMRKEquipRenderUtils,
323 | } from "../typechain-types";
324 | import { ContractTransaction } from "ethers";
325 |
326 | const pricePerMint = ethers.utils.parseEther("0.0001");
327 |
328 | async function main() {
329 |
330 | }
331 |
332 | main().catch((error) => {
333 | console.error(error);
334 | process.exitCode = 1;
335 | });
336 | ````
337 |
338 | Since we will expand upon this deploy script in the [user journey](#user-journey), we will add a `deployContracts`
339 | function. In it we will deploy one `SimpleExternalEquip` and one `SimpleExternalEquip` smart contract per example (we
340 | will use `Kanaria` and `Gem` examples). In addition to that, we will also deploy the `SimpleCatalog` and the
341 | `RMRKEquipRenderUtils` which we will use to piece together the final product of the user journey. Once the smart
342 | contracts are deployed, we will output their addresses. The function is defined below the `main` function definition:
343 |
344 | ````typescript
345 | async function deployContracts(): Promise<
346 | [
347 | SimpleNestableExternalEquip,
348 | SimpleExternalEquip,
349 | SimpleNestableExternalEquip,
350 | SimpleExternalEquip,
351 | SimpleCatalog,
352 | RMRKEquipRenderUtils
353 | ]
354 | > {
355 | const [beneficiary] = await ethers.getSigners();
356 | const equipFactory = await ethers.getContractFactory("SimpleExternalEquip");
357 | const nestableFactory = await ethers.getContractFactory(
358 | "SimpleNestableExternalEquip"
359 | );
360 | const catalogFactory = await ethers.getContractFactory("SimpleCatalog");
361 | const viewsFactory = await ethers.getContractFactory("RMRKEquipRenderUtils");
362 |
363 | const nestableKanaria: SimpleNestableExternalEquip =
364 | await nestableFactory.deploy(
365 | ethers.constants.AddressZero,
366 | "Kanaria",
367 | "KAN",
368 | "ipfs://collectionMeta",
369 | "ipfs://tokenMeta",
370 | {
371 | erc20TokenAddress: ethers.constants.AddressZero,
372 | tokenUriIsEnumerable: true,
373 | royaltyRecipient: await beneficiary.getAddress(),
374 | royaltyPercentageBps: 10,
375 | maxSupply: 1000,
376 | pricePerMint: pricePerMint
377 | }
378 | );
379 | const nestableGem: SimpleNestableExternalEquip = await nestableFactory.deploy(
380 | ethers.constants.AddressZero,
381 | "Gem",
382 | "GM",
383 | "ipfs://collectionMeta",
384 | "ipfs://tokenMeta",
385 | {
386 | erc20TokenAddress: ethers.constants.AddressZero,
387 | tokenUriIsEnumerable: true,
388 | royaltyRecipient: await beneficiary.getAddress(),
389 | royaltyPercentageBps: 10,
390 | maxSupply: 3000,
391 | pricePerMint: pricePerMint
392 | }
393 | );
394 |
395 | const kanariaEquip: SimpleExternalEquip = await equipFactory.deploy(
396 | nestableKanaria.address
397 | );
398 | const gemEquip: SimpleExternalEquip = await equipFactory.deploy(
399 | nestableGem.address
400 | );
401 | const catalog: SimpleCatalog = await catalogFactory.deploy("KB", "svg");
402 | const views: RMRKEquipRenderUtils = await viewsFactory.deploy();
403 |
404 | await nestableKanaria.deployed();
405 | await kanariaEquip.deployed();
406 | await nestableGem.deployed();
407 | await gemEquip.deployed();
408 | await catalog.deployed();
409 |
410 | const allTx = [
411 | await nestableKanaria.setEquippableAddress(kanariaEquip.address),
412 | await nestableGem.setEquippableAddress(gemEquip.address),
413 | ];
414 | await Promise.all(allTx.map((tx) => tx.wait()));
415 | console.log(
416 | `Sample contracts deployed to ${nestableKanaria.address} (Kanaria Nestable) | ${kanariaEquip.address} (Kanaria Equip), ${nestableGem.address} (Gem Nestable) | ${gemEquip.address} (Gem Equip) and ${catalog.address} (Catalog)`
417 | );
418 |
419 | return [nestableKanaria, kanariaEquip, nestableGem, gemEquip, catalog, views];
420 | }
421 | ````
422 |
423 | In order for the `deployContracts` to be called when running the deploy script, we have to add it to the `main`
424 | function:
425 |
426 | ````typescript
427 | const [kanaria, gem, catalog, views] = await deployContracts();
428 | ````
429 |
430 | A custom script added to [`package.json`](../../package.json) allows us to easily run the script:
431 |
432 | ````json
433 | "scripts": {
434 | "deploy-split-equippable": "hardhat run scripts/deploySplitEquippable.ts"
435 | }
436 | ````
437 |
438 | Using the script with `npm run deploy-split-equippable` should return the following output:
439 |
440 | ````shell
441 | npm run deploy-split-equippable
442 |
443 | > @rmrk-team/evm-contract-samples@0.1.0 deploy-split-equippable
444 | > hardhat run scripts/deploySplitEquippable.ts
445 |
446 | Compiled 47 Solidity files successfully
447 | Sample contracts deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3 (Kanaria Nestable) | 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 (Kanaria Equip), 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 (Gem Nestable) | 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 (Gem Equip) and 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 (Catalog)
448 | ````
449 |
450 | ### User journey
451 |
452 | With the deploy script ready, we can examine how the journey of a user using split equippable would look like.
453 |
454 | The catalog of the user jourey script is the same as the deploy script, as we need to deploy the smart contract in order
455 | to interact with it:
456 |
457 | ````typescript
458 | import { ethers } from "hardhat";
459 | import {
460 | SimpleCatalog,
461 | SimpleExternalEquip,
462 | SimpleNestableExternalEquip,
463 | RMRKEquipRenderUtils,
464 | } from "../typechain-types";
465 | import { ContractTransaction } from "ethers";
466 |
467 | const pricePerMint = ethers.utils.parseEther("0.0001");
468 |
469 | async function main() {
470 | const [nestableKanaria, kanariaEquip, nestableGem, gemEquip, catalog, views] =
471 | await deployContracts();
472 | }
473 |
474 | async function deployContracts(): Promise<
475 | [
476 | SimpleNestableExternalEquip,
477 | SimpleExternalEquip,
478 | SimpleNestableExternalEquip,
479 | SimpleExternalEquip,
480 | SimpleCatalog,
481 | RMRKEquipRenderUtils
482 | ]
483 | > {
484 | const [beneficiary] = await ethers.getSigners();
485 | const equipFactory = await ethers.getContractFactory("SimpleExternalEquip");
486 | const nestableFactory = await ethers.getContractFactory(
487 | "SimpleNestableExternalEquip"
488 | );
489 | const catalogFactory = await ethers.getContractFactory("SimpleCatalog");
490 | const viewsFactory = await ethers.getContractFactory("RMRKEquipRenderUtils");
491 |
492 | const nestableKanaria: SimpleNestableExternalEquip =
493 | await nestableFactory.deploy(
494 | ethers.constants.AddressZero,
495 | "Kanaria",
496 | "KAN",
497 | "ipfs://collectionMeta",
498 | "ipfs://tokenMeta",
499 | {
500 | erc20TokenAddress: ethers.constants.AddressZero,
501 | tokenUriIsEnumerable: true,
502 | royaltyRecipient: await beneficiary.getAddress(),
503 | royaltyPercentageBps: 10,
504 | maxSupply: 1000,
505 | pricePerMint: pricePerMint
506 | }
507 | );
508 | const nestableGem: SimpleNestableExternalEquip = await nestableFactory.deploy(
509 | ethers.constants.AddressZero,
510 | "Gem",
511 | "GM",
512 | "ipfs://collectionMeta",
513 | "ipfs://tokenMeta",
514 | {
515 | erc20TokenAddress: ethers.constants.AddressZero,
516 | tokenUriIsEnumerable: true,
517 | royaltyRecipient: await beneficiary.getAddress(),
518 | royaltyPercentageBps: 10,
519 | maxSupply: 3000,
520 | pricePerMint: pricePerMint
521 | }
522 | );
523 |
524 | const kanariaEquip: SimpleExternalEquip = await equipFactory.deploy(
525 | nestableKanaria.address
526 | );
527 | const gemEquip: SimpleExternalEquip = await equipFactory.deploy(
528 | nestableGem.address
529 | );
530 | const catalog: SimpleCatalog = await catalogFactory.deploy("KB", "svg");
531 | const views: RMRKEquipRenderUtils = await viewsFactory.deploy();
532 |
533 | await nestableKanaria.deployed();
534 | await kanariaEquip.deployed();
535 | await nestableGem.deployed();
536 | await gemEquip.deployed();
537 | await catalog.deployed();
538 |
539 | const allTx = [
540 | await nestableKanaria.setEquippableAddress(kanariaEquip.address),
541 | await nestableGem.setEquippableAddress(gemEquip.address),
542 | ];
543 | await Promise.all(allTx.map((tx) => tx.wait()));
544 | console.log(
545 | `Sample contracts deployed to ${nestableKanaria.address} (Kanaria Nestable) | ${kanariaEquip.address} (Kanaria Equip), ${nestableGem.address} (Gem Nestable) | ${gemEquip.address} (Gem Equip) and ${catalog.address} (Catalog)`
546 | );
547 |
548 | return [nestableKanaria, kanariaEquip, nestableGem, gemEquip, catalog, views];
549 | }
550 |
551 | main().catch((error) => {
552 | console.error(error);
553 | process.exitCode = 1;
554 | });
555 | ````
556 |
557 | Once the smart contracts are deployed, we can setup the Catalog. We will set it up have two fixed part options for
558 | background, head, body and wings. Additionally we will add three slot options for gems. All of these will be added
559 | sing the [`addPartList`](#addpartlist) method. The call together with encapsulating `setupCatalog` function should look
560 | like this:
561 |
562 | ````typescript
563 | async function setupCatalog(catalog: SimpleCatalog, gemAddress: string): Promise {
564 | // Setup catalog with 2 fixed part options for background, head, body and wings.
565 | // Also 3 slot options for gems
566 | const tx = await catalog.addPartList([
567 | {
568 | // Background option 1
569 | partId: 1,
570 | part: {
571 | itemType: 2, // Fixed
572 | z: 0,
573 | equippable: [],
574 | metadataURI: "ipfs://backgrounds/1.svg",
575 | },
576 | },
577 | {
578 | // Background option 2
579 | partId: 2,
580 | part: {
581 | itemType: 2, // Fixed
582 | z: 0,
583 | equippable: [],
584 | metadataURI: "ipfs://backgrounds/2.svg",
585 | },
586 | },
587 | {
588 | // Head option 1
589 | partId: 3,
590 | part: {
591 | itemType: 2, // Fixed
592 | z: 3,
593 | equippable: [],
594 | metadataURI: "ipfs://heads/1.svg",
595 | },
596 | },
597 | {
598 | // Head option 2
599 | partId: 4,
600 | part: {
601 | itemType: 2, // Fixed
602 | z: 3,
603 | equippable: [],
604 | metadataURI: "ipfs://heads/2.svg",
605 | },
606 | },
607 | {
608 | // Body option 1
609 | partId: 5,
610 | part: {
611 | itemType: 2, // Fixed
612 | z: 2,
613 | equippable: [],
614 | metadataURI: "ipfs://body/1.svg",
615 | },
616 | },
617 | {
618 | // Body option 2
619 | partId: 6,
620 | part: {
621 | itemType: 2, // Fixed
622 | z: 2,
623 | equippable: [],
624 | metadataURI: "ipfs://body/2.svg",
625 | },
626 | },
627 | {
628 | // Wings option 1
629 | partId: 7,
630 | part: {
631 | itemType: 2, // Fixed
632 | z: 1,
633 | equippable: [],
634 | metadataURI: "ipfs://wings/1.svg",
635 | },
636 | },
637 | {
638 | // Wings option 2
639 | partId: 8,
640 | part: {
641 | itemType: 2, // Fixed
642 | z: 1,
643 | equippable: [],
644 | metadataURI: "ipfs://wings/2.svg",
645 | },
646 | },
647 | {
648 | // Gems slot 1
649 | partId: 9,
650 | part: {
651 | itemType: 1, // Slot
652 | z: 4,
653 | equippable: [gemAddress], // Only gems tokens can be equipped here
654 | metadataURI: "",
655 | },
656 | },
657 | {
658 | // Gems slot 2
659 | partId: 10,
660 | part: {
661 | itemType: 1, // Slot
662 | z: 4,
663 | equippable: [gemAddress], // Only gems tokens can be equipped here
664 | metadataURI: "",
665 | },
666 | },
667 | {
668 | // Gems slot 3
669 | partId: 11,
670 | part: {
671 | itemType: 1, // Slot
672 | z: 4,
673 | equippable: [gemAddress], // Only gems tokens can be equipped here
674 | metadataURI: "",
675 | },
676 | },
677 | ]);
678 | await tx.wait();
679 | console.log("Catalog is set");
680 | }
681 | ````
682 |
683 | Notice how the `z` value of the background is `0` and that of the head is `3`. Also note how the `itemType` value of
684 | the `Slot` type of fixed items is `2` and that of equippable items is `1`. Additionally the `metadataURI` is usually
685 | left blank for the equippables, but has to be set for the fixed items. The `equippable` values have to be set to the
686 | gem smart contracts for the equippable items.
687 |
688 | In order for the `setupCatalog` to be called, we have to add it to the `main` function:
689 |
690 | ````typescript
691 | await setupCatalog(catalog, gemEquip.address);
692 | ````
693 |
694 | **NOTE: The address of the `SimpleExternalEquip` part of `Gem` should be passed to the `setupCatalog`.**
695 |
696 | With the Catalog set up, the tokens should now be minted. Both `Kanaria` and `Gem` tokens will be minted in the
697 | `mintTokens`. To define how many tokens should be minted, `totalBirds` constant will be added below the `import`
698 | statements:
699 |
700 | ````typescript
701 | const totalBirds = 5;
702 | ````
703 |
704 | The `mintToken` function should accept two arguments (`SimpleNestableExternalEquip` of `Kanaria` and `Gem`). We will
705 | prepare a batch of transactions to mint the tokens and send them. Once the tokens are minted, we will output the total
706 | number of tokens minted. While the `Kanaria` tokens will be minted to the `tokenOwner` address, the `Gem` tokens will be
707 | minted using the [`nestMint`](#nestMint) method in order to be minted directly to the Kanaria tokens. We will
708 | mint three `Gem` tokens to each `Kanaria`. Since all of the nested tokens need to be approved, we will also build a
709 | batch of transaction to accept a single nest-minted `Gem` for each `Kanaria`:
710 |
711 | ````typescript
712 | async function mintTokens(
713 | kanaria: SimpleNestableExternalEquip,
714 | gem: SimpleNestableExternalEquip
715 | ): Promise {
716 | const [ , tokenOwner] = await ethers.getSigners();
717 |
718 | // Mint some kanarias
719 | let tx = await kanaria.mint(tokenOwner.address, totalBirds, {
720 | value: pricePerMint.mul(totalBirds),
721 | });
722 | await tx.wait();
723 | console.log(`Minted ${totalBirds} kanarias`);
724 |
725 | // Mint 3 gems into each nestableKanaria
726 | let allTx: ContractTransaction[] = [];
727 | for (let i = 1; i <= totalBirds; i++) {
728 | let tx = await gem.nestMint(kanaria.address, 3, i, {
729 | value: pricePerMint.mul(3),
730 | });
731 | allTx.push(tx);
732 | }
733 | await Promise.all(allTx.map((tx) => tx.wait()));
734 | console.log(`Minted 3 gems into each nestableKanaria`);
735 |
736 | // Accept 3 gems for each kanaria
737 | console.log("Accepting Gems");
738 | for (let tokenId = 1; tokenId <= totalBirds; tokenId++) {
739 | allTx = [
740 | await kanaria.connect(tokenOwner).acceptChild(
741 | tokenId,
742 | 2,
743 | gem.address,
744 | 3 * tokenId,
745 | ),
746 | await kanaria.connect(tokenOwner).acceptChild(tokenId, 1, gem.address, 3 * tokenId - 1),
747 | await kanaria.connect(tokenOwner).acceptChild(tokenId, 0, gem.address, 3 * tokenId - 2),
748 | ];
749 | }
750 | await Promise.all(allTx.map((tx) => tx.wait()));
751 | console.log(`Accepted gems for each kanaria`);
752 | }
753 | ````
754 |
755 | **NOTE: We assign the `tokenOwner` the second available signer, so that the assets are not automatically accepted when added
756 | to the token. This happens when an account adding an asset to a token is also the owner of said token.**
757 |
758 | In order for the `mintTokens` to be called, we have to add it to the `main` function:
759 |
760 | ````typescript
761 | await mintTokens(nestableKanaria, nestableGem);
762 | ````
763 |
764 | Having minted both `Kanaria`s and `Gem`s, we can now add assets to them. The assets are added to the
765 | `SimpleExternalEquip` parts of them. We will add assets to the `Kanaria` using the `addKanariaAssets` function.
766 | It accepts `Kanaria` and address of the `Catalog` smart contract. Assets will be added using the
767 | [`addAssetEntry`](#addassetentry) method. We will add a default asset, which doesn't need a `catalogAddress`
768 | value. The composed asset needs to have the `catalogAddress`. We also specify the fixed parts IDs for background, head,
769 | body and wings. Additionally we allow the gems to be equipped in the slot parts IDs. With the asset entires added,
770 | we can add them to a token and then accept them as well:
771 |
772 | ````typescript
773 | async function addKanariaAssets(
774 | const [ , tokenOwner] = await ethers.getSigners();
775 | kanaria: SimpleExternalEquip,
776 | catalogAddress: string
777 | ): Promise {
778 | const assetDefaultId = 1;
779 | const assetComposedId = 2;
780 | let allTx: ContractTransaction[] = [];
781 | let allTx: ContractTransaction[] = [];
782 | let tx = await kanaria.addEquippableAssetEntry(
783 | 0, // Only used for assets meant to equip into others
784 | ethers.constants.AddressZero, // catalog is not needed here
785 | "ipfs://default.png",
786 | []
787 | );
788 | allTx.push(tx);
789 |
790 | tx = await kanaria.addEquippableAssetEntry(
791 | 0, // Only used for assets meant to equip into others
792 | catalogAddress, // Since we're using parts, we must define the catalog
793 | "ipfs://meta1.json",
794 | [1, 3, 5, 7, 9, 10, 11], // We're using first background, head, body and wings and state that this can receive the 3 slot parts for gems
795 | );
796 | allTx.push(tx);
797 | // Wait for both assets to be added
798 | await Promise.all(allTx.map((tx) => tx.wait()));
799 | console.log("Added 2 asset entries");
800 |
801 | // Add assets to token
802 | const tokenId = 1;
803 | allTx = [
804 | await kanaria.addAssetToToken(tokenId, assetDefaultId, 0),
805 | await kanaria.addAssetToToken(tokenId, assetComposedId, 0),
806 | ];
807 | await Promise.all(allTx.map((tx) => tx.wait()));
808 | console.log("Added assets to token 1");
809 |
810 | // Accept both assets:
811 | tx = await kanaria.connect(tokenOwner).acceptAsset(tokenId, 0, assetDefaultId);
812 | await tx.wait();
813 | tx = await kanaria.connect(tokenOwner).acceptAsset(tokenId, 0, assetComposedId);
814 | await tx.wait();
815 | console.log("Assets accepted");
816 | }
817 | ````
818 |
819 | Adding assets to `Gem`s is done in the `addGemAssets`. It accepts `SimpleExternalEquip` part of `Gem`, address of
820 | the `SimpleExternalEquip` of `Kanaria` smart contract and the address of the `Catalog` smart contract. We will add 4
821 | assets for each gem; one full version and three that match each slot. Reference IDs are specified for easier
822 | reference from the child's perspective. The assets will be added one by one. Note how the full versions of gems
823 | don't have the `equippableRefId`.
824 |
825 | Having added the asset entries, we can now add the valid parent reference IDs using the
826 | `setValidParentForEquippableGroup`. For example if we want to add a valid reference for the left gem, we need to pass
827 | the value of equippable reference ID of the left gem, parent smart contract address (in our case this is
828 | `SimpleExternalEquip` of `Kanaria` smart contract) and ID of the slot which was defined in `Catalog` (this is ID number 9
829 | in the `Catalog` for the left gem).
830 |
831 | Last thing to do is to add assets to the tokens using [`addAssetToToken`](#addassettotoken). Asset of type
832 | A will be added to the gems 1 and 2, and the type B of the asset is added to gem 3. All of these should be accepted
833 | using `acceptAsset`:
834 |
835 | ````typescript
836 | async function addGemAssets(
837 | gem: SimpleExternalEquip,
838 | kanariaAddress: string,
839 | catalogAddress: string
840 | ): Promise {
841 | const [ , tokenOwner] = await ethers.getSigners();
842 | // We'll add 4 assets for each nestableGem, a full version and 3 versions matching each slot.
843 | // We will have only 2 types of gems -> 4x2: 8 assets.
844 | // This is not composed by others, so fixed and slot parts are never used.
845 | const gemVersions = 4;
846 |
847 | // These refIds are used from the child's perspective, to group assets that can be equipped into a parent
848 | // With it, we avoid the need to do set it asset by asset
849 | const equippableRefIdLeftGem = 1;
850 | const equippableRefIdMidGem = 2;
851 | const equippableRefIdRightGem = 3;
852 |
853 | // We can do a for loop, but this makes it clearer.
854 | let allTx = [
855 | await gem.addEquippableAssetEntry(
856 | // Full version for first type of gem, no need of refId or catalog
857 | 0,
858 | catalogAddress,
859 | `ipfs://gems/typeA/full.svg`,
860 | []
861 | ),
862 | await gem.addEquippableAssetEntry(
863 | // Equipped into left slot for first type of gem
864 | equippableRefIdLeftGem,
865 | catalogAddress,
866 | `ipfs://gems/typeA/left.svg`,
867 | []
868 | ),
869 | await gem.addEquippableAssetEntry(
870 | // Equipped into mid slot for first type of gem
871 | equippableRefIdMidGem,
872 | catalogAddress,
873 | `ipfs://gems/typeA/mid.svg`,
874 | []
875 | ),
876 | await gem.addEquippableAssetEntry(
877 | // Equipped into left slot for first type of gem
878 | equippableRefIdRightGem,
879 | catalogAddress,
880 | `ipfs://gems/typeA/right.svg`,
881 | []
882 | ),
883 | await gem.addEquippableAssetEntry(
884 | // Full version for second type of gem, no need of refId or catalog
885 | 0,
886 | ethers.constants.AddressZero,
887 | `ipfs://gems/typeB/full.svg`,
888 | []
889 | ),
890 | await gem.addEquippableAssetEntry(
891 | // Equipped into left slot for second type of gem
892 | equippableRefIdLeftGem,
893 | catalogAddress,
894 | `ipfs://gems/typeB/left.svg`,
895 | []
896 | ),
897 | await gem.addEquippableAssetEntry(
898 | // Equipped into mid slot for second type of gem
899 | equippableRefIdMidGem,
900 | catalogAddress,
901 | `ipfs://gems/typeB/mid.svg`,
902 | []
903 | ),
904 | await gem.addEquippableAssetEntry(
905 | // Equipped into right slot for second type of gem
906 | equippableRefIdRightGem,
907 | catalogAddress,
908 | `ipfs://gems/typeB/right.svg`,
909 | []
910 | ),
911 | ];
912 |
913 | await Promise.all(allTx.map((tx) => tx.wait()));
914 | console.log(
915 | "Added 8 nestableGem assets. 2 Types of gems with full, left, mid and right versions."
916 | );
917 |
918 | // 9, 10 and 11 are the slot part ids for the gems, defined on the catalog.
919 | // e.g. Any asset on nestableGem, which sets its equippableGroupId to equippableRefIdLeftGem
920 | // will be considered a valid equip into any nestableKanaria on slot 9 (left nestableGem).
921 | allTx = [
922 | await gem.setValidParentForEquippableGroup(equippableRefIdLeftGem, kanariaAddress, 9),
923 | await gem.setValidParentForEquippableGroup(equippableRefIdMidGem, kanariaAddress, 10),
924 | await gem.setValidParentForEquippableGroup(equippableRefIdRightGem, kanariaAddress, 11),
925 | ];
926 | await Promise.all(allTx.map((tx) => tx.wait()));
927 |
928 | // We add assets of type A to nestableGem 1 and 2, and type Bto nestableGem 3. Both are nested into the first nestableKanaria
929 | // This means gems 1 and 2 will have the same asset, which is totally valid.
930 | allTx = [
931 | await gem.addAssetToToken(1, 1, 0),
932 | await gem.addAssetToToken(1, 2, 0),
933 | await gem.addAssetToToken(1, 3, 0),
934 | await gem.addAssetToToken(1, 4, 0),
935 | await gem.addAssetToToken(2, 1, 0),
936 | await gem.addAssetToToken(2, 2, 0),
937 | await gem.addAssetToToken(2, 3, 0),
938 | await gem.addAssetToToken(2, 4, 0),
939 | await gem.addAssetToToken(3, 5, 0),
940 | await gem.addAssetToToken(3, 6, 0),
941 | await gem.addAssetToToken(3, 7, 0),
942 | await gem.addAssetToToken(3, 8, 0),
943 | ];
944 | await Promise.all(allTx.map((tx) => tx.wait()));
945 | console.log("Added 4 assets to each of 3 gems.");
946 |
947 | // We accept each asset for all gems
948 | allTx = [
949 | await gem.connect(tokenOwner).acceptAsset(1, 3, 4),
950 | await gem.connect(tokenOwner).acceptAsset(1, 2, 3),
951 | await gem.connect(tokenOwner).acceptAsset(1, 1, 2),
952 | await gem.connect(tokenOwner).acceptAsset(1, 0, 1),
953 | await gem.connect(tokenOwner).acceptAsset(2, 3, 4),
954 | await gem.connect(tokenOwner).acceptAsset(2, 2, 3),
955 | await gem.connect(tokenOwner).acceptAsset(2, 1, 2),
956 | await gem.connect(tokenOwner).acceptAsset(2, 0, 1),
957 | await gem.connect(tokenOwner).acceptAsset(3, 3, 8),
958 | await gem.connect(tokenOwner).acceptAsset(3, 2, 7),
959 | await gem.connect(tokenOwner).acceptAsset(3, 1, 6),
960 | await gem.connect(tokenOwner).acceptAsset(3, 0, 5),
961 | ];
962 | await Promise.all(allTx.map((tx) => tx.wait()));
963 | console.log("Accepted 4 assets to each of 3 gems.");
964 | }
965 | ````
966 |
967 | In order for the `addKanariaAssets` and `addGemAssets` to be called, we have to add them to the `main` function:
968 |
969 | ````typescript
970 | await addKanariaAssets(kanariaEquip, catalog.address);
971 | await addGemAssets(gemEquip, kanariaEquip.address, catalog.address);
972 | ````
973 |
974 | With `Kanaria`s and `Gem`s ready, we can equip the gems to Kanarias using the `equipGems` function. We will build a
975 | batch of `equip` transactions calling the `SimpleExternalEquip` of the `Kanaria` and send it all at once:
976 |
977 | ````typescript
978 | async function equipGems(kanariaEquip: SimpleExternalEquip): Promise {
979 | const [ , tokenOwner] = await ethers.getSigners();
980 | const allTx = [
981 | await kanariaEquip.connect(tokenOwner).equip({
982 | tokenId: 1, // Kanaria 1
983 | childIndex: 2, // Gem 1 is on position 2
984 | assetId: 2, // Asset for the kanaria which is composable
985 | slotPartId: 9, // left gem slot
986 | childAssetId: 2, // Asset id for child meant for the left gem
987 | }),
988 | await kanariaEquip.connect(tokenOwner).equip({
989 | tokenId: 1, // Kanaria 1
990 | childIndex: 1, // Gem 2 is on position 1
991 | assetId: 2, // Asset for the kanaria which is composable
992 | slotPartId: 10, // mid gem slot
993 | childAssetId: 3, // Asset id for child meant for the mid gem
994 | }),
995 | await kanariaEquip.connect(tokenOwner).equip({
996 | tokenId: 1, // Kanaria 1
997 | childIndex: 0, // Gem 3 is on position 0
998 | assetId: 2, // Asset for the kanaria which is composable
999 | slotPartId: 11, // right gem slot
1000 | childAssetId: 8, // Asset id for child meant for the right gem
1001 | }),
1002 | ];
1003 | await Promise.all(allTx.map((tx) => tx.wait()));
1004 | console.log("Equipped 3 gems into first nestableKanaria");
1005 | }
1006 | ````
1007 |
1008 | In order for the `equipGems` to be called, we have to add it to the `main` function:
1009 |
1010 | ````typescript
1011 | await equipGems(kanariaEquip);
1012 | ````
1013 |
1014 | Last thing to do is to compose the equippables with the `composeEquippables` function. It composes the whole NFT along
1015 | with the nested and equipped parts:
1016 |
1017 | ````typescript
1018 | async function composeEquippables(
1019 | views: RMRKEquipRenderUtils,
1020 | kanariaAddress: string
1021 | ): Promise {
1022 | const tokenId = 1;
1023 | const assetId = 2;
1024 | console.log(
1025 | "Composed: ",
1026 | await views.composeEquippables(kanariaAddress, tokenId, assetId)
1027 | );
1028 | }
1029 | ````
1030 |
1031 | In order for the `composeEquippables` to be called, we have to add it to the `main` function:
1032 |
1033 | ````typescript
1034 | await composeEquippables(views, kanariaEquip.address);
1035 | ````
1036 |
1037 | With the user journey script concluded, we can add a custom helper to the [`package.json`](../../package.json) to make
1038 | running it easier:
1039 |
1040 | ````json
1041 | "user-journey-split-equippable": "hardhat run scripts/splitEquippableUserJourney.ts"
1042 | ````
1043 |
1044 | Running it using `npm run user-journey-split-equippable` should return the following output:
1045 |
1046 | ````shell
1047 | npm run user-journey-split-equippable
1048 |
1049 | > @rmrk-team/evm-contract-samples@0.1.0 user-journey-split-equippable
1050 | > hardhat run scripts/splitEquippableUserJourney.ts
1051 |
1052 | Sample contracts deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3 | 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0, 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 | 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 and 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
1053 | Catalog is set
1054 | Minted 5 kanarias
1055 | Minted 3 gems into each nestableKanaria
1056 | Accepted 1 nestableGem for each nestableKanaria
1057 | Accepted 1 nestableGem for each nestableKanaria
1058 | Accepted 1 nestableGem for each nestableKanaria
1059 | Added 2 asset entries
1060 | Added assets to token 1
1061 | Assets accepted
1062 | Added 8 nestableGem assets. 2 Types of gems with full, left, mid and right versions.
1063 | Added 4 assets to each of 3 gems.
1064 | Accepted 4 assets to each of 3 gems.
1065 | Equipped 3 gems into first nestableKanaria
1066 | Composed: [
1067 | [
1068 | BigNumber { value: "2" },
1069 | BigNumber { value: "0" },
1070 | '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9',
1071 | 'ipfs://meta1.json',
1072 | id: BigNumber { value: "2" },
1073 | equippableGroupId: BigNumber { value: "0" },
1074 | catalogAddress: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9',
1075 | metadataURI: 'ipfs://meta1.json'
1076 | ],
1077 | [
1078 | [
1079 | BigNumber { value: "1" },
1080 | 0,
1081 | 'ipfs://backgrounds/1.svg',
1082 | partId: BigNumber { value: "1" },
1083 | z: 0,
1084 | metadataURI: 'ipfs://backgrounds/1.svg'
1085 | ],
1086 | [
1087 | BigNumber { value: "3" },
1088 | 3,
1089 | 'ipfs://heads/1.svg',
1090 | partId: BigNumber { value: "3" },
1091 | z: 3,
1092 | metadataURI: 'ipfs://heads/1.svg'
1093 | ],
1094 | [
1095 | BigNumber { value: "5" },
1096 | 2,
1097 | 'ipfs://body/1.svg',
1098 | partId: BigNumber { value: "5" },
1099 | z: 2,
1100 | metadataURI: 'ipfs://body/1.svg'
1101 | ],
1102 | [
1103 | BigNumber { value: "7" },
1104 | 1,
1105 | 'ipfs://wings/1.svg',
1106 | partId: BigNumber { value: "7" },
1107 | z: 1,
1108 | metadataURI: 'ipfs://wings/1.svg'
1109 | ]
1110 | ],
1111 | [
1112 | [
1113 | BigNumber { value: "9" },
1114 | BigNumber { value: "2" },
1115 | 4,
1116 | BigNumber { value: "1" },
1117 | '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1118 | '',
1119 | partId: BigNumber { value: "9" },
1120 | childAssetId: BigNumber { value: "2" },
1121 | z: 4,
1122 | childTokenId: BigNumber { value: "1" },
1123 | childAddress: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1124 | metadataURI: ''
1125 | ],
1126 | [
1127 | BigNumber { value: "10" },
1128 | BigNumber { value: "3" },
1129 | 4,
1130 | BigNumber { value: "2" },
1131 | '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1132 | '',
1133 | partId: BigNumber { value: "10" },
1134 | childAssetId: BigNumber { value: "3" },
1135 | z: 4,
1136 | childTokenId: BigNumber { value: "2" },
1137 | childAddress: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1138 | metadataURI: ''
1139 | ],
1140 | [
1141 | BigNumber { value: "11" },
1142 | BigNumber { value: "8" },
1143 | 4,
1144 | BigNumber { value: "3" },
1145 | '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1146 | '',
1147 | partId: BigNumber { value: "11" },
1148 | childAssetId: BigNumber { value: "8" },
1149 | z: 4,
1150 | childTokenId: BigNumber { value: "3" },
1151 | childAddress: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1152 | metadataURI: ''
1153 | ]
1154 | ],
1155 | asset: [
1156 | BigNumber { value: "2" },
1157 | BigNumber { value: "0" },
1158 | '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9',
1159 | 'ipfs://meta1.json',
1160 | id: BigNumber { value: "2" },
1161 | equippableGroupId: BigNumber { value: "0" },
1162 | catalogAddress: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9',
1163 | metadataURI: 'ipfs://meta1.json'
1164 | ],
1165 | fixedParts: [
1166 | [
1167 | BigNumber { value: "1" },
1168 | 0,
1169 | 'ipfs://backgrounds/1.svg',
1170 | partId: BigNumber { value: "1" },
1171 | z: 0,
1172 | metadataURI: 'ipfs://backgrounds/1.svg'
1173 | ],
1174 | [
1175 | BigNumber { value: "3" },
1176 | 3,
1177 | 'ipfs://heads/1.svg',
1178 | partId: BigNumber { value: "3" },
1179 | z: 3,
1180 | metadataURI: 'ipfs://heads/1.svg'
1181 | ],
1182 | [
1183 | BigNumber { value: "5" },
1184 | 2,
1185 | 'ipfs://body/1.svg',
1186 | partId: BigNumber { value: "5" },
1187 | z: 2,
1188 | metadataURI: 'ipfs://body/1.svg'
1189 | ],
1190 | [
1191 | BigNumber { value: "7" },
1192 | 1,
1193 | 'ipfs://wings/1.svg',
1194 | partId: BigNumber { value: "7" },
1195 | z: 1,
1196 | metadataURI: 'ipfs://wings/1.svg'
1197 | ]
1198 | ],
1199 | slotParts: [
1200 | [
1201 | BigNumber { value: "9" },
1202 | BigNumber { value: "2" },
1203 | 4,
1204 | BigNumber { value: "1" },
1205 | '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1206 | '',
1207 | partId: BigNumber { value: "9" },
1208 | childAssetId: BigNumber { value: "2" },
1209 | z: 4,
1210 | childTokenId: BigNumber { value: "1" },
1211 | childAddress: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1212 | metadataURI: ''
1213 | ],
1214 | [
1215 | BigNumber { value: "10" },
1216 | BigNumber { value: "3" },
1217 | 4,
1218 | BigNumber { value: "2" },
1219 | '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1220 | '',
1221 | partId: BigNumber { value: "10" },
1222 | childAssetId: BigNumber { value: "3" },
1223 | z: 4,
1224 | childTokenId: BigNumber { value: "2" },
1225 | childAddress: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1226 | metadataURI: ''
1227 | ],
1228 | [
1229 | BigNumber { value: "11" },
1230 | BigNumber { value: "8" },
1231 | 4,
1232 | BigNumber { value: "3" },
1233 | '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1234 | '',
1235 | partId: BigNumber { value: "11" },
1236 | childAssetId: BigNumber { value: "8" },
1237 | z: 4,
1238 | childTokenId: BigNumber { value: "3" },
1239 | childAddress: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
1240 | metadataURI: ''
1241 | ]
1242 | ]
1243 | ]
1244 | ````
1245 |
1246 | This concludes our work on the simple Split equippable RMRK lego composite and we can now move on to examining the
1247 | advanced implementation.
1248 |
1249 | ## Advanced SplitEquippable
1250 |
1251 | The advanced `SplitEquippable` consists of three smart contracts. The
1252 | [`AdvancedCatalog`](../MergedEquippable/README.md#advancedbase) is already examined in the `MergedEquippable`
1253 | documentation. Let's first examine the `AdvancedExternalEquip` and then move on to the `AdvancedNestableExternalEquip`.
1254 |
1255 | **NOTE: As the `AdvancedCatalog` smart contract is used by both `MergedEquippable` as well as `SplitEquippable` it resides
1256 | in the root `contracts/` directory.**
1257 |
1258 | ### AdvancedExternalEquip
1259 |
1260 | The [`AdvancedExternalEquip.sol`](./AdvancedExternalEquip.sol) smart contract represents the minimum required
1261 | implementation in order for the smart contract to be compatible with the `MultiAsset` and `Equippable` part of the
1262 | `ExternalEquip` RMRK lego composite. It uses the
1263 | [`RMRKExternalEquip`](https://github.com/rmrk-team/evm/blob/dev/contracts/RMRK/equippable/RMRKExternalEquip.sol) import
1264 | to gain access to the `MultiAsset` and `Equippable` part of the External equippable RMRK lego composite:
1265 |
1266 | ````solidity
1267 | import "@rmrk-team/evm-contracts/contracts/RMRK/equippable/RMRKExternalEquip.sol";
1268 | ````
1269 |
1270 | We only need the `nestableAddress`, which is the address of the deployed `AdvancedNestableExternalEquip` smart contract,
1271 | in order to properly initialize it after the `AdvancedExternalEquip` inherits it:
1272 |
1273 | ````solidity
1274 | contract AdvancedExternalEquip is RMRKExternalEquip {
1275 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
1276 | constructor(
1277 | address nestableAddress
1278 | )
1279 | RMRKExternalEquip(nestableAddress)
1280 | {
1281 | // Custom optional: constructor logic
1282 | }
1283 | }
1284 | ````
1285 |
1286 | **NOTE: Passing `0x0` as the value of `nestableAddress` allows us to initialize the smart contract without having the
1287 | address of the deployed `AdvancedExternalEquip` and allows us to add it at a later point in time.**
1288 |
1289 | This is all that is required to get you started with implementing the `MultiAsset` and `Equippable` parts of the
1290 | external equippable RMRK lego composite.
1291 |
1292 |
1293 | The minimal AdvancedExternalEquip.sol should look like this:
1294 |
1295 | ````solidity
1296 | // SPDX-License-Identifier: Apache-2.0
1297 |
1298 | pragma solidity ^0.8.18;
1299 |
1300 | import "@rmrk-team/evm-contracts/contracts/RMRK/equippable/RMRKExternalEquip.sol";
1301 |
1302 | /* import "hardhat/console.sol"; */
1303 |
1304 | contract AdvancedExternalEquip is RMRKExternalEquip {
1305 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
1306 | constructor(
1307 | address nestableAddress
1308 | )
1309 | RMRKExternalEquip(nestableAddress)
1310 | {
1311 | // Custom optional: constructor logic
1312 | }
1313 | }
1314 | ````
1315 |
1316 |
1317 |
1318 | Using `RMRKExternalEquip` requires custom implementation of asset management logic. Available internal functions when writing it are:
1319 |
1320 | - `_setNestableAddress(address nestableAddress)`
1321 | - `_addAssetEntry(ExtendedAsset calldata asset, uint64[] calldata fixedPartIds, uint64[] calldata slotPartIds)`
1322 | - `_addAssetToToken(uint256 tokenId, uint64 assetId, uint64 replacesAssetWithId)`
1323 | - `_setValidParentForEquippableGroup(uint64 equippableGroupId, address parentAddress, uint64 slotPartId)`
1324 |
1325 | ### AdvancedNestableExternalEquip
1326 |
1327 | The [`AdvancedNestableExternalEquip`](./AdvancedNestableExternalEquip.sol) smart contracts represents the minimum required
1328 | implementation in order for the smart contract to be compatible with the `Nestable` part of the `ExternalEquip` RMRK lego
1329 | composite. It uses the
1330 | [`RMRKNestableExternalEquip`](https://github.com/rmrk-team/evm/blob/dev/contracts/RMRK/equippable/RMRKNestableExternalEquip.sol)
1331 | import to gain access to the `Nestable` part of the External equippable RMRK lego composite:
1332 |
1333 | ````solidity
1334 | import "@rmrk-team/evm-contracts/contracts/RMRK/equippable/RMRKNestableExternalEquip.sol";
1335 | ````
1336 |
1337 | We only need the `name` and `symbol` of the NFT collection in order to properly initialize it after the
1338 | `AdvancedNestableExternalEquip` inherits it:
1339 |
1340 | ````solidity
1341 | contract AdvancedNestableExternalEquip is RMRKNestableExternalEquip {
1342 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
1343 | constructor(
1344 | string memory name,
1345 | string memory symbol
1346 | )
1347 | RMRKNestableExternalEquip(name, symbol)
1348 | {
1349 | // Custom optional: constructor logic
1350 | }
1351 | }
1352 | ````
1353 |
1354 | This is all that is required to get you started with implementing the `Nestable` part of the external equippable RMRK lego composite.
1355 |
1356 |
1357 | The minimal AdvancedNestableExternalEquip.sol should look like this:
1358 |
1359 | ````solidity
1360 | // SPDX-License-Identifier: Apache-2.0
1361 |
1362 | pragma solidity ^0.8.18;
1363 |
1364 | import "@rmrk-team/evm-contracts/contracts/RMRK/equippable/RMRKNestableExternalEquip.sol";
1365 |
1366 | contract AdvancedNestableExternalEquip is RMRKNestableExternalEquip {
1367 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
1368 | constructor(
1369 | string memory name,
1370 | string memory symbol
1371 | )
1372 | RMRKNestableExternalEquip(name, symbol)
1373 | {
1374 | // Custom optional: constructor logic
1375 | }
1376 | }
1377 | ````
1378 |
1379 |
1380 |
1381 | Using `RMRKNestableExternalEquip`requires custom implementation of minting logic. Available internal functions to use when writing it are:
1382 |
1383 | - `_mint(address to, uint256 tokenId)`
1384 | - `_safeMint(address to, uint256 tokenId)`
1385 | - `_safeMint(address to, uint256 tokenId, bytes memory data)`
1386 | - `_nestMint(address to, uint256 tokenId, uint256 destinationId)`
1387 |
1388 | The latter is used to nest mint the NFT directly to the parent NFT. If you intend to support it at the minting stage,
1389 | you should implement it in your smart contract.
1390 |
1391 | In addition to the minting functions, you should also implement the burning and transfer functions if they apply to your
1392 | use case:
1393 |
1394 | - `_burn(uint256 tokenId)`
1395 | - `transferFrom(address from, address to, uint256 tokenId)`
1396 | - `nestTransfer(address from, address to, uint256 tokenId, uint256 destinationId)`
1397 |
1398 | It is also important to implement the function for setting the address of the deployed `AdvancedExternalEquip`:
1399 |
1400 | - `_setEquippableAddress(address equippable)`
1401 |
1402 | Any additional function supporting your NFT use case and utility can also be added. Remember to thoroughly test your
1403 | smart contracts with extensive test suites and define strict access control rules for the functions that you implement.
1404 |
1405 | Happy equipping! 🛠
--------------------------------------------------------------------------------
/contracts/SplitEquippable/SimpleExternalEquip.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.18;
3 |
4 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKExternalEquipImpl.sol";
5 |
6 | contract SimpleExternalEquip is RMRKExternalEquipImpl {
7 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
8 | constructor(address nestableAddress)
9 | RMRKExternalEquipImpl(nestableAddress)
10 | {}
11 | }
12 |
--------------------------------------------------------------------------------
/contracts/SplitEquippable/SimpleNestableExternalEquip.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.18;
3 |
4 | import "@rmrk-team/evm-contracts/contracts/implementations/nativeTokenPay/RMRKNestableExternalEquipImpl.sol";
5 |
6 | contract SimpleNestableExternalEquip is RMRKNestableExternalEquipImpl {
7 | // NOTE: Additional custom arguments can be added to the constructor based on your needs.
8 | constructor(
9 | address equippableAddress,
10 | string memory name,
11 | string memory symbol,
12 | string memory collectionMetadata,
13 | string memory tokenURI,
14 | InitData memory data
15 | )
16 | RMRKNestableExternalEquipImpl(
17 | equippableAddress,
18 | name,
19 | symbol,
20 | collectionMetadata,
21 | tokenURI,
22 | data
23 | )
24 | {}
25 | }
26 |
--------------------------------------------------------------------------------
/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import { HardhatUserConfig } from "hardhat/config";
2 | import "@nomicfoundation/hardhat-toolbox";
3 | import "@typechain/hardhat";
4 | import "hardhat-gas-reporter";
5 | import "solidity-coverage";
6 | import "hardhat-contract-sizer";
7 |
8 | const config: HardhatUserConfig = {
9 | solidity: {
10 | version: "0.8.18",
11 | settings: {
12 | optimizer: {
13 | enabled: true,
14 | runs: 200,
15 | },
16 | },
17 | },
18 | networks: {
19 | goerli: {
20 | url: process.env.GOERLI_URL || "",
21 | accounts:
22 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
23 | },
24 | moonbaseAlpha: {
25 | url: "https://rpc.testnet.moonbeam.network",
26 | accounts:
27 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
28 | },
29 | moonriver: {
30 | url: "https://rpc.api.moonriver.moonbeam.network",
31 | chainId: 1285, // (hex: 0x505),
32 | accounts:
33 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
34 | },
35 | },
36 | etherscan: {
37 | apiKey: {
38 | moonriver: process.env.MOONRIVER_MOONSCAN_APIKEY || "", // Moonriver Moonscan API Key
39 | moonbaseAlpha: process.env.MOONBEAM_MOONSCAN_APIKEY || "", // Moonbeam Moonscan API Key
40 | goerli: process.env.ETHERSCAN_API_KEY || "", // Goerli Etherscan API Key
41 | },
42 | },
43 | };
44 |
45 | export default config;
46 |
--------------------------------------------------------------------------------
/img/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmrk-team/evm-sample-contracts/36a5efb995dcbfa38fe71f2fb0fa5db20df5671c/img/4.jpg
--------------------------------------------------------------------------------
/img/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmrk-team/evm-sample-contracts/36a5efb995dcbfa38fe71f2fb0fa5db20df5671c/img/5.jpg
--------------------------------------------------------------------------------
/img/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmrk-team/evm-sample-contracts/36a5efb995dcbfa38fe71f2fb0fa5db20df5671c/img/6.jpg
--------------------------------------------------------------------------------
/img/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmrk-team/evm-sample-contracts/36a5efb995dcbfa38fe71f2fb0fa5db20df5671c/img/7.jpg
--------------------------------------------------------------------------------
/img/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmrk-team/evm-sample-contracts/36a5efb995dcbfa38fe71f2fb0fa5db20df5671c/img/8.jpg
--------------------------------------------------------------------------------
/img/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmrk-team/evm-sample-contracts/36a5efb995dcbfa38fe71f2fb0fa5db20df5671c/img/9.jpg
--------------------------------------------------------------------------------
/img/RMRKLegoInfo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmrk-team/evm-sample-contracts/36a5efb995dcbfa38fe71f2fb0fa5db20df5671c/img/RMRKLegoInfo.png
--------------------------------------------------------------------------------
/img/RMRKLegoInfographics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmrk-team/evm-sample-contracts/36a5efb995dcbfa38fe71f2fb0fa5db20df5671c/img/RMRKLegoInfographics.png
--------------------------------------------------------------------------------
/img/RMRKLegos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmrk-team/evm-sample-contracts/36a5efb995dcbfa38fe71f2fb0fa5db20df5671c/img/RMRKLegos.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@rmrk-team/evm-contract-samples",
3 | "version": "0.1.0",
4 | "description": "Set of sample contracts created using RMRK standard package: @rmrk-team/evm-contracts",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1",
7 | "deploy-equippable": "hardhat run scripts/deployEquippable.ts",
8 | "deploy-multi-asset": "hardhat run scripts/deployMultiAsset.ts",
9 | "deploy-nestable": "hardhat run scripts/deployNestable.ts",
10 | "deploy-nestable-multi-asset": "hardhat run scripts/deployNestableMultiAsset.ts",
11 | "deploy-split-equippable": "hardhat run scripts/deploySplitEquippable.ts",
12 | "user-journey-nestable": "hardhat run scripts/nestableUserJourney.ts",
13 | "user-journey-multi-asset": "hardhat run scripts/multiAssetUserJourney.ts",
14 | "user-journey-nestable-multi-asset": "hardhat run scripts/nestableMultiAssetUserJourney.ts",
15 | "user-journey-merged-equippable": "hardhat run scripts/mergedEquippableUserJourney.ts",
16 | "user-journey-split-equippable": "hardhat run scripts/splitEquippableUserJourney.ts"
17 | },
18 | "keywords": [],
19 | "author": "@rmrk-team",
20 | "license": "Apache-2.0",
21 | "dependencies": {
22 | "@rmrk-team/evm-contracts": "^0.26.0"
23 | },
24 | "devDependencies": {
25 | "@ethersproject/providers": "^5.4.7",
26 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.1",
27 | "@nomicfoundation/hardhat-network-helpers": "^1.0.7",
28 | "@nomicfoundation/hardhat-toolbox": "^2.0.2",
29 | "@nomiclabs/hardhat-ethers": "^2.2.1",
30 | "@nomiclabs/hardhat-etherscan": "^3.1.3",
31 | "@typechain/ethers-v5": "^10.1.1",
32 | "@typechain/hardhat": "^6.1.4",
33 | "@types/chai": "^4.3.4",
34 | "@types/mocha": "^10.0.1",
35 | "chai": "^4.3.6",
36 | "hardhat": "^2.12.3",
37 | "hardhat-contract-sizer": "^2.6.1",
38 | "hardhat-gas-reporter": "^1.0.9",
39 | "solidity-coverage": "^0.8.2",
40 | "ts-node": "^10.9.1",
41 | "typechain": "^8.1.1",
42 | "typescript": "^4.9.3"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/scripts/deployEquippable.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import {
3 | SimpleCatalog,
4 | SimpleEquippable,
5 | RMRKEquipRenderUtils,
6 | } from "../typechain-types";
7 | import { ContractTransaction } from "ethers";
8 |
9 | const pricePerMint = ethers.utils.parseEther("0.0001");
10 |
11 | async function main() {
12 | const [kanaria, gem, base, views] = await deployContracts();
13 | }
14 |
15 | async function deployContracts(): Promise<
16 | [SimpleEquippable, SimpleEquippable, SimpleCatalog, RMRKEquipRenderUtils]
17 | > {
18 | console.log("Deploying smart contracts");
19 |
20 | const [beneficiary] = await ethers.getSigners();
21 | const contractFactory = await ethers.getContractFactory("SimpleEquippable");
22 | const catalogFactory = await ethers.getContractFactory("SimpleCatalog");
23 | const viewsFactory = await ethers.getContractFactory("RMRKEquipRenderUtils");
24 |
25 | const kanaria: SimpleEquippable = await contractFactory.deploy(
26 | "Kanaria",
27 | "KAN",
28 | "ipfs://collectionMeta",
29 | "ipfs://tokenMeta",
30 | {
31 | erc20TokenAddress: ethers.constants.AddressZero,
32 | tokenUriIsEnumerable: true,
33 | royaltyRecipient: await beneficiary.getAddress(),
34 | royaltyPercentageBps: 10,
35 | maxSupply: 1000,
36 | pricePerMint: pricePerMint
37 | }
38 | );
39 | const gem: SimpleEquippable = await contractFactory.deploy(
40 | "Gem",
41 | "GM",
42 | "ipfs://collectionMeta",
43 | "ipfs://tokenMeta",
44 | {
45 | erc20TokenAddress: ethers.constants.AddressZero,
46 | tokenUriIsEnumerable: true,
47 | royaltyRecipient: await beneficiary.getAddress(),
48 | royaltyPercentageBps: 10,
49 | maxSupply: 3000,
50 | pricePerMint: pricePerMint
51 | }
52 | );
53 | const base: SimpleCatalog = await catalogFactory.deploy("KB", "svg");
54 | const views: RMRKEquipRenderUtils = await viewsFactory.deploy();
55 |
56 | await kanaria.deployed();
57 | await gem.deployed();
58 | await base.deployed();
59 | await views.deployed();
60 | console.log(
61 | `Sample contracts deployed to ${kanaria.address} (Kanaria), ${gem.address} (Gem) and ${base.address} (Catalog)`
62 | );
63 |
64 | return [kanaria, gem, base, views];
65 | }
66 |
67 | main().catch((error) => {
68 | console.error(error);
69 | process.exitCode = 1;
70 | });
71 |
--------------------------------------------------------------------------------
/scripts/deployMultiAsset.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import { SimpleMultiAsset } from "../typechain-types";
3 |
4 | async function main() {
5 | const pricePerMint = ethers.utils.parseEther("0.0001");
6 |
7 | const contractFactory = await ethers.getContractFactory("SimpleMultiAsset");
8 | const token: SimpleMultiAsset = await contractFactory.deploy(
9 | {
10 | erc20TokenAddress: ethers.constants.AddressZero,
11 | tokenUriIsEnumerable: true,
12 | royaltyRecipient: ethers.constants.AddressZero,
13 | royaltyPercentageBps: 0,
14 | maxSupply: 1000,
15 | pricePerMint: pricePerMint
16 | }
17 | );
18 |
19 | await token.deployed();
20 | console.log(`Sample contract deployed to ${token.address}`);
21 | }
22 |
23 | main().catch((error) => {
24 | console.error(error);
25 | process.exitCode = 1;
26 | });
27 |
--------------------------------------------------------------------------------
/scripts/deployNestable.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import { SimpleNestable } from "../typechain-types";
3 | import { ContractTransaction } from "ethers";
4 |
5 | async function main() {
6 | const pricePerMint = ethers.utils.parseEther("0.0001");
7 | const totalTokens = 5;
8 | const [owner] = await ethers.getSigners();
9 |
10 | const contractFactory = await ethers.getContractFactory("SimpleNestable");
11 | const parent: SimpleNestable = await contractFactory.deploy(
12 | "Kanaria",
13 | "KAN",
14 | "ipfs://collectionMeta",
15 | "ipfs://tokenMeta",
16 | {
17 | erc20TokenAddress: ethers.constants.AddressZero,
18 | tokenUriIsEnumerable: true,
19 | royaltyRecipient: await owner.getAddress(),
20 | royaltyPercentageBps: 10,
21 | maxSupply: 1000,
22 | pricePerMint: pricePerMint
23 | }
24 | );
25 | const child: SimpleNestable = await contractFactory.deploy(
26 | "Chunky",
27 | "CHN",
28 | "ipfs://collectionMeta",
29 | "ipfs://tokenMeta",
30 | {
31 | erc20TokenAddress: ethers.constants.AddressZero,
32 | tokenUriIsEnumerable: true,
33 | royaltyRecipient: await owner.getAddress(),
34 | royaltyPercentageBps: 10,
35 | maxSupply: 1000,
36 | pricePerMint: pricePerMint
37 | }
38 | );
39 |
40 | await parent.deployed();
41 | await child.deployed();
42 | console.log(
43 | `Sample contracts deployed to ${parent.address} and ${child.address}`
44 | );
45 | }
46 |
47 | // main2();
48 | main().catch((error) => {
49 | console.error(error);
50 | process.exitCode = 1;
51 | });
52 |
--------------------------------------------------------------------------------
/scripts/deployNestableMultiAsset.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import { SimpleNestableMultiAsset } from "../typechain-types";
3 | import { ContractTransaction } from "ethers";
4 |
5 | async function main() {
6 | const pricePerMint = ethers.utils.parseEther("0.0000000001");
7 | const totalTokens = 5;
8 | const [owner] = await ethers.getSigners();
9 |
10 | const contractFactory = await ethers.getContractFactory(
11 | "SimpleNestableMultiAsset"
12 | );
13 | const token: SimpleNestableMultiAsset = await contractFactory.deploy(
14 | {
15 | erc20TokenAddress: ethers.constants.AddressZero,
16 | tokenUriIsEnumerable: true,
17 | royaltyRecipient: await owner.getAddress(),
18 | royaltyPercentageBps: 10,
19 | maxSupply: 1000,
20 | pricePerMint: pricePerMint
21 | }
22 | );
23 |
24 | await token.deployed();
25 | console.log(`Sample contract deployed to ${token.address}`);
26 | }
27 |
28 | main().catch((error) => {
29 | console.error(error);
30 | process.exitCode = 1;
31 | });
32 |
--------------------------------------------------------------------------------
/scripts/deploySplitEquippable.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import {
3 | SimpleCatalog,
4 | SimpleExternalEquip,
5 | SimpleNestableExternalEquip,
6 | RMRKEquipRenderUtils,
7 | } from "../typechain-types";
8 | import { ContractTransaction } from "ethers";
9 |
10 | const pricePerMint = ethers.utils.parseEther("0.0001");
11 |
12 | async function main() {
13 | const [nestableKanaria, kanariaEquip, nestableGem, gemEquip, base, views] =
14 | await deployContracts();
15 | }
16 |
17 | async function deployContracts(): Promise<
18 | [
19 | SimpleNestableExternalEquip,
20 | SimpleExternalEquip,
21 | SimpleNestableExternalEquip,
22 | SimpleExternalEquip,
23 | SimpleCatalog,
24 | RMRKEquipRenderUtils
25 | ]
26 | > {
27 | const [beneficiary] = await ethers.getSigners();
28 | const equipFactory = await ethers.getContractFactory("SimpleExternalEquip");
29 | const nestableFactory = await ethers.getContractFactory(
30 | "SimpleNestableExternalEquip"
31 | );
32 | const catalogFactory = await ethers.getContractFactory("SimpleCatalog");
33 | const viewsFactory = await ethers.getContractFactory("RMRKEquipRenderUtils");
34 |
35 | const nestableKanaria: SimpleNestableExternalEquip =
36 | await nestableFactory.deploy(
37 | ethers.constants.AddressZero,
38 | "Kanaria",
39 | "KAN",
40 | "ipfs://collectionMeta",
41 | "ipfs://tokenMeta",
42 | {
43 | erc20TokenAddress: ethers.constants.AddressZero,
44 | tokenUriIsEnumerable: true,
45 | royaltyRecipient: await beneficiary.getAddress(),
46 | royaltyPercentageBps: 10,
47 | maxSupply: 1000,
48 | pricePerMint: pricePerMint
49 | }
50 | );
51 | const nestableGem: SimpleNestableExternalEquip = await nestableFactory.deploy(
52 | ethers.constants.AddressZero,
53 | "Gem",
54 | "GM",
55 | "ipfs://collectionMeta",
56 | "ipfs://tokenMeta",
57 | {
58 | erc20TokenAddress: ethers.constants.AddressZero,
59 | tokenUriIsEnumerable: true,
60 | royaltyRecipient: await beneficiary.getAddress(),
61 | royaltyPercentageBps: 10,
62 | maxSupply: 3000,
63 | pricePerMint: pricePerMint
64 | }
65 | );
66 |
67 | const kanariaEquip: SimpleExternalEquip = await equipFactory.deploy(
68 | nestableKanaria.address
69 | );
70 | const gemEquip: SimpleExternalEquip = await equipFactory.deploy(
71 | nestableGem.address
72 | );
73 | const base: SimpleCatalog = await catalogFactory.deploy("KB", "svg");
74 | const views: RMRKEquipRenderUtils = await viewsFactory.deploy();
75 |
76 | await nestableKanaria.deployed();
77 | await kanariaEquip.deployed();
78 | await nestableGem.deployed();
79 | await gemEquip.deployed();
80 | await base.deployed();
81 | await views.deployed();
82 |
83 | const allTx = [
84 | await nestableKanaria.setEquippableAddress(kanariaEquip.address),
85 | await nestableGem.setEquippableAddress(gemEquip.address),
86 | ];
87 | await Promise.all(allTx.map((tx) => tx.wait()));
88 | console.log(
89 | `Sample contracts deployed to ${nestableKanaria.address} (Kanaria Nestable) | ${kanariaEquip.address} (Kanaria Equip), ${nestableGem.address} (Gem Nestable) | ${gemEquip.address} (Gem Equip) and ${base.address} (Catalog)`
90 | );
91 |
92 | return [nestableKanaria, kanariaEquip, nestableGem, gemEquip, base, views];
93 | }
94 |
95 | main().catch((error) => {
96 | console.error(error);
97 | process.exitCode = 1;
98 | });
99 |
--------------------------------------------------------------------------------
/scripts/mergedEquippableUserJourney.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import {
3 | SimpleCatalog,
4 | SimpleEquippable,
5 | RMRKEquipRenderUtils,
6 | } from "../typechain-types";
7 | import { ContractTransaction } from "ethers";
8 |
9 | const pricePerMint = ethers.utils.parseEther("0.0001");
10 | const totalBirds = 5;
11 | const deployedKanariaAddress = "";
12 | const deployedGemAddress = "";
13 | const deployedCatalogAddress = "";
14 | const deployedViewsAddress = "";
15 |
16 | async function main() {
17 | const [kanaria, gem, base, views] = await deployContracts();
18 | // const [kanaria, gem, base, views] = await retrieveContracts();
19 |
20 | // Notice that most of these steps will happen at different points in time
21 | // Here we do all in one go to demonstrate how to use it.
22 | await setupCatalog(base, gem.address);
23 | await mintTokens(kanaria, gem);
24 | await addKanariaAssets(kanaria, base.address);
25 | await addGemAssets(gem, kanaria.address, base.address);
26 | await equipGems(kanaria);
27 | await composeEquippables(views, kanaria.address);
28 | }
29 |
30 | async function retrieveContracts(): Promise<
31 | [SimpleEquippable, SimpleEquippable, SimpleCatalog, RMRKEquipRenderUtils]
32 | > {
33 | const contractFactory = await ethers.getContractFactory("SimpleEquippable");
34 | const catalogFactory = await ethers.getContractFactory("SimpleCatalog");
35 | const viewsFactory = await ethers.getContractFactory("RMRKEquipRenderUtils");
36 |
37 | const kanaria: SimpleEquippable = contractFactory.attach(
38 | deployedKanariaAddress
39 | );
40 | const gem: SimpleEquippable = contractFactory.attach(deployedGemAddress);
41 | const base: SimpleCatalog = catalogFactory.attach(deployedCatalogAddress);
42 | const views: RMRKEquipRenderUtils = await viewsFactory.attach(
43 | deployedViewsAddress
44 | );
45 |
46 | return [kanaria, gem, base, views];
47 | }
48 |
49 | async function deployContracts(): Promise<
50 | [SimpleEquippable, SimpleEquippable, SimpleCatalog, RMRKEquipRenderUtils]
51 | > {
52 | const [beneficiary] = await ethers.getSigners();
53 | const contractFactory = await ethers.getContractFactory("SimpleEquippable");
54 | const catalogFactory = await ethers.getContractFactory("SimpleCatalog");
55 | const viewsFactory = await ethers.getContractFactory("RMRKEquipRenderUtils");
56 |
57 | const kanaria: SimpleEquippable = await contractFactory.deploy(
58 | "Kanaria",
59 | "KAN",
60 | "ipfs://collectionMeta",
61 | "ipfs://tokenMeta",
62 | {
63 | erc20TokenAddress: ethers.constants.AddressZero,
64 | tokenUriIsEnumerable: true,
65 | royaltyRecipient: await beneficiary.getAddress(),
66 | royaltyPercentageBps: 10,
67 | maxSupply: 1000,
68 | pricePerMint: pricePerMint
69 | }
70 | );
71 | const gem: SimpleEquippable = await contractFactory.deploy(
72 | "Gem",
73 | "GM",
74 | "ipfs://collectionMeta",
75 | "ipfs://tokenMeta",
76 | {
77 | erc20TokenAddress: ethers.constants.AddressZero,
78 | tokenUriIsEnumerable: true,
79 | royaltyRecipient: await beneficiary.getAddress(),
80 | royaltyPercentageBps: 10,
81 | maxSupply: 3000,
82 | pricePerMint: pricePerMint
83 | }
84 | );
85 | const base: SimpleCatalog = await catalogFactory.deploy("KB", "svg");
86 | const views: RMRKEquipRenderUtils = await viewsFactory.deploy();
87 |
88 | await kanaria.deployed();
89 | await gem.deployed();
90 | await base.deployed();
91 | await views.deployed();
92 | console.log(
93 | `Sample contracts deployed to ${kanaria.address}, ${gem.address} and ${base.address}`
94 | );
95 |
96 | return [kanaria, gem, base, views];
97 | }
98 |
99 | async function setupCatalog(base: SimpleCatalog, gemAddress: string): Promise {
100 | console.log("Setting up Catalog");
101 | // Setup base with 2 fixed part options for background, head, body and wings.
102 | // Also 3 slot options for gems
103 | const tx = await base.addPartList([
104 | {
105 | // Background option 1
106 | partId: 1,
107 | part: {
108 | itemType: 2, // Fixed
109 | z: 0,
110 | equippable: [],
111 | metadataURI: "ipfs://backgrounds/1.json",
112 | },
113 | },
114 | {
115 | // Background option 2
116 | partId: 2,
117 | part: {
118 | itemType: 2, // Fixed
119 | z: 0,
120 | equippable: [],
121 | metadataURI: "ipfs://backgrounds/2.json",
122 | },
123 | },
124 | {
125 | // Head option 1
126 | partId: 3,
127 | part: {
128 | itemType: 2, // Fixed
129 | z: 3,
130 | equippable: [],
131 | metadataURI: "ipfs://heads/1.json",
132 | },
133 | },
134 | {
135 | // Head option 2
136 | partId: 4,
137 | part: {
138 | itemType: 2, // Fixed
139 | z: 3,
140 | equippable: [],
141 | metadataURI: "ipfs://heads/2.json",
142 | },
143 | },
144 | {
145 | // Body option 1
146 | partId: 5,
147 | part: {
148 | itemType: 2, // Fixed
149 | z: 2,
150 | equippable: [],
151 | metadataURI: "ipfs://body/1.json",
152 | },
153 | },
154 | {
155 | // Body option 2
156 | partId: 6,
157 | part: {
158 | itemType: 2, // Fixed
159 | z: 2,
160 | equippable: [],
161 | metadataURI: "ipfs://body/2.json",
162 | },
163 | },
164 | {
165 | // Wings option 1
166 | partId: 7,
167 | part: {
168 | itemType: 2, // Fixed
169 | z: 1,
170 | equippable: [],
171 | metadataURI: "ipfs://wings/1.json",
172 | },
173 | },
174 | {
175 | // Wings option 2
176 | partId: 8,
177 | part: {
178 | itemType: 2, // Fixed
179 | z: 1,
180 | equippable: [],
181 | metadataURI: "ipfs://wings/2.json",
182 | },
183 | },
184 | {
185 | // Gems slot 1
186 | partId: 9,
187 | part: {
188 | itemType: 1, // Slot
189 | z: 4,
190 | equippable: [gemAddress], // Only gems tokens can be equipped here
191 | metadataURI: "",
192 | },
193 | },
194 | {
195 | // Gems slot 2
196 | partId: 10,
197 | part: {
198 | itemType: 1, // Slot
199 | z: 4,
200 | equippable: [gemAddress], // Only gems tokens can be equipped here
201 | metadataURI: "",
202 | },
203 | },
204 | {
205 | // Gems slot 3
206 | partId: 11,
207 | part: {
208 | itemType: 1, // Slot
209 | z: 4,
210 | equippable: [gemAddress], // Only gems tokens can be equipped here
211 | metadataURI: "",
212 | },
213 | },
214 | ]);
215 | await tx.wait();
216 | console.log("Catalog is set");
217 | }
218 |
219 | async function mintTokens(
220 | kanaria: SimpleEquippable,
221 | gem: SimpleEquippable
222 | ): Promise {
223 | console.log("Minting tokens");
224 | const [ , tokenOwner] = await ethers.getSigners();
225 |
226 | // Mint some kanarias
227 | console.log("Minting Kanaria tokens");
228 | let tx = await kanaria.mint(tokenOwner.address, totalBirds, {
229 | value: pricePerMint.mul(totalBirds),
230 | });
231 | await tx.wait();
232 | console.log(`Minted ${totalBirds} kanarias`);
233 |
234 | // Mint 3 gems into each kanaria
235 | console.log("Nest-minting Gem tokens");
236 | let allTx: ContractTransaction[] = [];
237 | for (let i = 1; i <= totalBirds; i++) {
238 | let tx = await gem.nestMint(kanaria.address, 3, i, {
239 | value: pricePerMint.mul(3),
240 | });
241 | allTx.push(tx);
242 | }
243 | await Promise.all(allTx.map((tx) => tx.wait()));
244 | console.log(`Minted 3 gems into each kanaria`);
245 |
246 | // Accept 3 gems for each kanaria
247 | console.log("Accepting Gems");
248 | for (let tokenId = 1; tokenId <= totalBirds; tokenId++) {
249 | allTx = [
250 | await kanaria.connect(tokenOwner).acceptChild(tokenId, 2, gem.address, 3 * tokenId),
251 | await kanaria.connect(tokenOwner).acceptChild(tokenId, 1, gem.address, 3 * tokenId - 1),
252 | await kanaria.connect(tokenOwner).acceptChild(tokenId, 0, gem.address, 3 * tokenId - 2),
253 | ];
254 | }
255 | await Promise.all(allTx.map((tx) => tx.wait()));
256 | console.log(`Accepted gems for each kanaria`);
257 | }
258 |
259 | async function addKanariaAssets(
260 | kanaria: SimpleEquippable,
261 | catalogAddress: string
262 | ): Promise {
263 | console.log("Adding Kanaria assets");
264 | const [ , tokenOwner] = await ethers.getSigners();
265 | const assetDefaultId = 1;
266 | const assetComposedId = 2;
267 | let allTx: ContractTransaction[] = [];
268 | let tx = await kanaria.addEquippableAssetEntry(
269 | 0, // Only used for assets meant to equip into others
270 | ethers.constants.AddressZero, // base is not needed here
271 | "ipfs://default.png",
272 | []
273 | );
274 | allTx.push(tx);
275 |
276 | tx = await kanaria.addEquippableAssetEntry(
277 | 0, // Only used for assets meant to equip into others
278 | catalogAddress, // Since we're using parts, we must define the base
279 | "ipfs://meta1.json",
280 | [1, 3, 5, 7, 9, 10, 11] // We're using first background, head, body and wings and state that this can receive the 3 slot parts for gems
281 | );
282 | allTx.push(tx);
283 | // Wait for both assets to be added
284 | await Promise.all(allTx.map((tx) => tx.wait()));
285 | console.log("Added 2 asset entries");
286 |
287 | // Add assets to token
288 | const tokenId = 1;
289 | allTx = [
290 | await kanaria.addAssetToToken(tokenId, assetDefaultId, 0),
291 | await kanaria.addAssetToToken(tokenId, assetComposedId, 0),
292 | ];
293 | await Promise.all(allTx.map((tx) => tx.wait()));
294 | console.log("Added assets to token 1");
295 |
296 | // Accept both assets:
297 | tx = await kanaria.connect(tokenOwner).acceptAsset(tokenId, 0, assetDefaultId);
298 | await tx.wait();
299 | tx = await kanaria.connect(tokenOwner).acceptAsset(tokenId, 0, assetComposedId);
300 | await tx.wait();
301 | console.log("Assets accepted");
302 | }
303 |
304 | async function addGemAssets(
305 | gem: SimpleEquippable,
306 | kanariaAddress: string,
307 | catalogAddress: string
308 | ): Promise {
309 | console.log("Adding Gem assets");
310 | const [ , tokenOwner] = await ethers.getSigners();
311 | // We'll add 4 assets for each gem, a full version and 3 versions matching each slot.
312 | // We will have only 2 types of gems -> 4x2: 8 assets.
313 | // This is not composed by others, so fixed and slot parts are never used.
314 | const gemVersions = 4;
315 |
316 | // These refIds are used from the child's perspective, to group assets that can be equipped into a parent
317 | // With it, we avoid the need to do set it asset by asset
318 | const equippableRefIdLeftGem = 1;
319 | const equippableRefIdMidGem = 2;
320 | const equippableRefIdRightGem = 3;
321 |
322 | // We can do a for loop, but this makes it clearer.
323 | console.log("Adding asset entries");
324 | let allTx = [
325 | await gem.addEquippableAssetEntry(
326 | // Full version for first type of gem, no need of refId or base
327 | 0,
328 | catalogAddress,
329 | `ipfs://gems/typeA/full.json`,
330 | []
331 | ),
332 | await gem.addEquippableAssetEntry(
333 | // Equipped into left slot for first type of gem
334 | equippableRefIdLeftGem,
335 | catalogAddress,
336 | `ipfs://gems/typeA/left.json`,
337 | []
338 | ),
339 | await gem.addEquippableAssetEntry(
340 | // Equipped into mid slot for first type of gem
341 | equippableRefIdMidGem,
342 | catalogAddress,
343 | `ipfs://gems/typeA/mid.json`,
344 | []
345 | ),
346 | await gem.addEquippableAssetEntry(
347 | // Equipped into left slot for first type of gem
348 | equippableRefIdRightGem,
349 | catalogAddress,
350 | `ipfs://gems/typeA/right.json`,
351 | []
352 | ),
353 | await gem.addEquippableAssetEntry(
354 | // Full version for second type of gem, no need of refId or base
355 | 0,
356 | ethers.constants.AddressZero,
357 | `ipfs://gems/typeB/full.json`,
358 | []
359 | ),
360 | await gem.addEquippableAssetEntry(
361 | // Equipped into left slot for second type of gem
362 | equippableRefIdLeftGem,
363 | catalogAddress,
364 | `ipfs://gems/typeB/left.json`,
365 | []
366 | ),
367 | await gem.addEquippableAssetEntry(
368 | // Equipped into mid slot for second type of gem
369 | equippableRefIdMidGem,
370 | catalogAddress,
371 | `ipfs://gems/typeB/mid.json`,
372 | []
373 | ),
374 | await gem.addEquippableAssetEntry(
375 | // Equipped into right slot for second type of gem
376 | equippableRefIdRightGem,
377 | catalogAddress,
378 | `ipfs://gems/typeB/right.json`,
379 | []
380 | ),
381 | ];
382 |
383 | await Promise.all(allTx.map((tx) => tx.wait()));
384 | console.log(
385 | "Added 8 gem assets. 2 Types of gems with full, left, mid and right versions."
386 | );
387 |
388 | // 9, 10 and 11 are the slot part ids for the gems, defined on the base.
389 | // e.g. Any asset on gem, which sets its equippableGroupId to equippableRefIdLeftGem
390 | // will be considered a valid equip into any kanaria on slot 9 (left gem).
391 | console.log("Setting valid parent reference IDs");
392 | allTx = [
393 | await gem.setValidParentForEquippableGroup(
394 | equippableRefIdLeftGem,
395 | kanariaAddress,
396 | 9
397 | ),
398 | await gem.setValidParentForEquippableGroup(
399 | equippableRefIdMidGem,
400 | kanariaAddress,
401 | 10
402 | ),
403 | await gem.setValidParentForEquippableGroup(
404 | equippableRefIdRightGem,
405 | kanariaAddress,
406 | 11
407 | ),
408 | ];
409 | await Promise.all(allTx.map((tx) => tx.wait()));
410 |
411 | // We add assets of type A to gem 1 and 2, and type Bto gem 3. Both are nested into the first kanaria
412 | // This means gems 1 and 2 will have the same asset, which is totally valid.
413 | console.log("Add assets to tokens");
414 | allTx = [
415 | await gem.addAssetToToken(1, 1, 0),
416 | await gem.addAssetToToken(1, 2, 0),
417 | await gem.addAssetToToken(1, 3, 0),
418 | await gem.addAssetToToken(1, 4, 0),
419 | await gem.addAssetToToken(2, 1, 0),
420 | await gem.addAssetToToken(2, 2, 0),
421 | await gem.addAssetToToken(2, 3, 0),
422 | await gem.addAssetToToken(2, 4, 0),
423 | await gem.addAssetToToken(3, 5, 0),
424 | await gem.addAssetToToken(3, 6, 0),
425 | await gem.addAssetToToken(3, 7, 0),
426 | await gem.addAssetToToken(3, 8, 0),
427 | ];
428 | await Promise.all(allTx.map((tx) => tx.wait()));
429 | console.log("Added 4 assets to each of 3 gems.");
430 |
431 | // We accept each asset for all gems
432 | allTx = [
433 | await gem.connect(tokenOwner).acceptAsset(1, 3, 4),
434 | await gem.connect(tokenOwner).acceptAsset(1, 2, 3),
435 | await gem.connect(tokenOwner).acceptAsset(1, 1, 2),
436 | await gem.connect(tokenOwner).acceptAsset(1, 0, 1),
437 | await gem.connect(tokenOwner).acceptAsset(2, 3, 4),
438 | await gem.connect(tokenOwner).acceptAsset(2, 2, 3),
439 | await gem.connect(tokenOwner).acceptAsset(2, 1, 2),
440 | await gem.connect(tokenOwner).acceptAsset(2, 0, 1),
441 | await gem.connect(tokenOwner).acceptAsset(3, 3, 8),
442 | await gem.connect(tokenOwner).acceptAsset(3, 2, 7),
443 | await gem.connect(tokenOwner).acceptAsset(3, 1, 6),
444 | await gem.connect(tokenOwner).acceptAsset(3, 0, 5),
445 | ];
446 | await Promise.all(allTx.map((tx) => tx.wait()));
447 | console.log("Accepted 4 assets to each of 3 gems.");
448 | }
449 |
450 | async function equipGems(kanaria: SimpleEquippable): Promise {
451 | console.log("Equipping gems");
452 | const [ , tokenOwner] = await ethers.getSigners();
453 | const allTx = [
454 | await kanaria.connect(tokenOwner).equip({
455 | tokenId: 1, // Kanaria 1
456 | childIndex: 2, // Gem 1 is on position 2
457 | assetId: 2, // Asset for the kanaria which is composable
458 | slotPartId: 9, // left gem slot
459 | childAssetId: 2, // Asset id for child meant for the left gem
460 | }),
461 | await kanaria.connect(tokenOwner).equip({
462 | tokenId: 1, // Kanaria 1
463 | childIndex: 1, // Gem 2 is on position 1
464 | assetId: 2, // Asset for the kanaria which is composable
465 | slotPartId: 10, // mid gem slot
466 | childAssetId: 3, // Asset id for child meant for the mid gem
467 | }),
468 | await kanaria.connect(tokenOwner).equip({
469 | tokenId: 1, // Kanaria 1
470 | childIndex: 0, // Gem 3 is on position 0
471 | assetId: 2, // Asset for the kanaria which is composable
472 | slotPartId: 11, // right gem slot
473 | childAssetId: 8, // Asset id for child meant for the right gem
474 | }),
475 | ];
476 | await Promise.all(allTx.map((tx) => tx.wait()));
477 | console.log("Equipped 3 gems into first kanaria");
478 | }
479 |
480 | async function composeEquippables(
481 | views: RMRKEquipRenderUtils,
482 | kanariaAddress: string
483 | ): Promise {
484 | console.log("Composing equippables");
485 | const tokenId = 1;
486 | const assetId = 2;
487 | console.log(
488 | "Composed: ",
489 | await views.composeEquippables(kanariaAddress, tokenId, assetId)
490 | );
491 | }
492 |
493 | main().catch((error) => {
494 | console.error(error);
495 | process.exitCode = 1;
496 | });
497 |
--------------------------------------------------------------------------------
/scripts/multiAssetUserJourney.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import { SimpleMultiAsset } from "../typechain-types";
3 | import { ContractTransaction } from "ethers";
4 |
5 | async function main() {
6 | const pricePerMint = ethers.utils.parseEther("0.0001");
7 | const totalTokens = 5;
8 | const [ , tokenOwner] = await ethers.getSigners();
9 |
10 | const contractFactory = await ethers.getContractFactory("SimpleMultiAsset");
11 | const token: SimpleMultiAsset = await contractFactory.deploy(
12 | {
13 | erc20TokenAddress: ethers.constants.AddressZero,
14 | tokenUriIsEnumerable: true,
15 | royaltyRecipient: ethers.constants.AddressZero,
16 | royaltyPercentageBps: 0,
17 | maxSupply: 1000,
18 | pricePerMint: pricePerMint
19 | }
20 | );
21 |
22 | await token.deployed();
23 | console.log(`Sample contract deployed to ${token.address}`);
24 |
25 | // Mint tokens 1 to totalTokens
26 | console.log("Minting tokens");
27 | let tx = await token.mint(tokenOwner.address, totalTokens, {
28 | value: pricePerMint.mul(totalTokens),
29 | });
30 | await tx.wait();
31 | console.log(`Minted ${totalTokens} tokens`);
32 | const totalSupply = await token.totalSupply();
33 | console.log("Total tokens: %s", totalSupply);
34 |
35 | // Add entries and add to tokens
36 | console.log("Adding assets");
37 | let allTx: ContractTransaction[] = [];
38 | for (let i = 1; i <= totalTokens; i++) {
39 | let tx = await token.addAssetEntry(`ipfs://metadata/${i}.json`);
40 | allTx.push(tx);
41 | }
42 | console.log(`Added ${totalTokens} assets`);
43 |
44 | console.log("Awaiting for all tx to finish...");
45 | await Promise.all(allTx.map((tx) => tx.wait()));
46 |
47 | console.log("Adding assets to tokens");
48 | allTx = [];
49 | for (let i = 1; i <= totalTokens; i++) {
50 | // We give each token a asset id with the same number. This is just a coincidence, not a restriction.
51 | let tx = await token.addAssetToToken(i, i, 0);
52 | allTx.push(tx);
53 | console.log(`Added asset ${i} to token ${i}.`);
54 | }
55 | console.log("Awaiting for all tx to finish...");
56 | await Promise.all(allTx.map((tx) => tx.wait()));
57 |
58 | console.log("Accepting token assets");
59 | allTx = [];
60 | for (let i = 1; i <= totalTokens; i++) {
61 | // Accept pending asset for each token (on index 0)
62 | let tx = await token.connect(tokenOwner).acceptAsset(i, 0, i);
63 | allTx.push(tx);
64 | console.log(`Accepted first pending asset for token ${i}.`);
65 | }
66 | console.log("Awaiting for all tx to finish...");
67 | await Promise.all(allTx.map((tx) => tx.wait()));
68 |
69 | // Few sample queries:
70 | console.log("Getting URIs");
71 | const uriToken1 = await token.tokenURI(1);
72 | const uriFinalToken = await token.tokenURI(totalTokens);
73 |
74 | console.log("Token 1 URI: ", uriToken1);
75 | console.log("Token totalTokens URI: ", uriFinalToken);
76 | }
77 |
78 | main().catch((error) => {
79 | console.error(error);
80 | process.exitCode = 1;
81 | });
82 |
--------------------------------------------------------------------------------
/scripts/nestableMultiAssetUserJourney.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import { SimpleNestableMultiAsset } from "../typechain-types";
3 | import { ContractTransaction } from "ethers";
4 |
5 | async function main() {
6 | const pricePerMint = ethers.utils.parseEther("0.0000000001");
7 | const totalTokens = 5;
8 | const [ owner, tokenOwner] = await ethers.getSigners();
9 |
10 | const contractFactory = await ethers.getContractFactory(
11 | "SimpleNestableMultiAsset"
12 | );
13 | const token: SimpleNestableMultiAsset = await contractFactory.deploy(
14 | {
15 | erc20TokenAddress: ethers.constants.AddressZero,
16 | tokenUriIsEnumerable: true,
17 | royaltyRecipient: await owner.getAddress(),
18 | royaltyPercentageBps: 10,
19 | maxSupply: 1000,
20 | pricePerMint: pricePerMint
21 | }
22 | );
23 |
24 | await token.deployed();
25 | console.log(`Sample contract deployed to ${token.address}`);
26 |
27 | // Mint tokens 1 to totalTokens
28 | console.log("Minting NFTs");
29 | let tx = await token.mint(tokenOwner.address, totalTokens, {
30 | value: pricePerMint.mul(totalTokens),
31 | });
32 | await tx.wait();
33 | console.log(`Minted ${totalTokens} tokens`);
34 | const totalSupply = await token.totalSupply();
35 | console.log("Total tokens: %s", totalSupply);
36 |
37 | // Add entries and add to tokens
38 | console.log("Adding assets");
39 | let allTx: ContractTransaction[] = [];
40 | for (let i = 1; i <= totalTokens; i++) {
41 | let tx = await token.addAssetEntry(`ipfs://metadata/${i}.json`);
42 | allTx.push(tx);
43 | }
44 | console.log(`Added ${totalTokens} assets`);
45 |
46 | console.log("Awaiting for all tx to finish...");
47 | await Promise.all(allTx.map((tx) => tx.wait()));
48 |
49 | console.log("Adding assets to tokens");
50 | allTx = [];
51 | for (let i = 1; i <= totalTokens; i++) {
52 | // We give each token a asset id with the same number. This is just a coincidence, not a restriction.
53 | let tx = await token.addAssetToToken(i, i, 0);
54 | allTx.push(tx);
55 | console.log(`Added asset ${i} to token ${i}.`);
56 | }
57 | console.log("Awaiting for all tx to finish...");
58 | await Promise.all(allTx.map((tx) => tx.wait()));
59 |
60 | console.log("Accepting assets to tokens");
61 | allTx = [];
62 | for (let i = 1; i <= totalTokens; i++) {
63 | // Accept pending asset for each token (on index 0)
64 | let tx = await token.connect(tokenOwner).acceptAsset(i, 0, i);
65 | allTx.push(tx);
66 | console.log(`Accepted first pending asset for token ${i}.`);
67 | }
68 | console.log("Awaiting for all tx to finish...");
69 | await Promise.all(allTx.map((tx) => tx.wait()));
70 |
71 | // Few sample queries:
72 | console.log("Getting URIs");
73 | const uriToken1 = await token.tokenURI(1);
74 | const uriToken5 = await token.tokenURI(totalTokens);
75 |
76 | console.log("Token 1 URI: ", uriToken1);
77 | console.log("Token totalTokens URI: ", uriToken5);
78 |
79 | // Transfer token 5 into token 1
80 | console.log("Nesting token with ID 5 into token with ID 1");
81 | await token.connect(tokenOwner).nestTransferFrom(tokenOwner.address, token.address, 5, 1, "0x");
82 | const parentId = await token.ownerOf(5);
83 | const rmrkParent = await token.directOwnerOf(5);
84 | console.log("Token's id 5 owner is ", parentId);
85 | console.log("Token's id 5 rmrk owner is ", rmrkParent);
86 | }
87 |
88 | main().catch((error) => {
89 | console.error(error);
90 | process.exitCode = 1;
91 | });
92 |
--------------------------------------------------------------------------------
/scripts/nestableUserJourney.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import { SimpleNestable } from "../typechain-types";
3 | import { ContractTransaction } from "ethers";
4 |
5 | async function main() {
6 | const pricePerMint = ethers.utils.parseEther("0.0001");
7 | const totalTokens = 5;
8 | const [owner] = await ethers.getSigners();
9 |
10 | const contractFactory = await ethers.getContractFactory("SimpleNestable");
11 | const parent: SimpleNestable = await contractFactory.deploy(
12 | "Kanaria",
13 | "KAN",
14 | "ipfs://collectionMeta",
15 | "ipfs://tokenMeta",
16 | {
17 | erc20TokenAddress: ethers.constants.AddressZero,
18 | tokenUriIsEnumerable: true,
19 | royaltyRecipient: await owner.getAddress(),
20 | royaltyPercentageBps: 10,
21 | maxSupply: 1000,
22 | pricePerMint: pricePerMint
23 | }
24 | );
25 | const child: SimpleNestable = await contractFactory.deploy(
26 | "Chunky",
27 | "CHN",
28 | "ipfs://collectionMeta",
29 | "ipfs://tokenMeta",
30 | {
31 | erc20TokenAddress: ethers.constants.AddressZero,
32 | tokenUriIsEnumerable: true,
33 | royaltyRecipient: await owner.getAddress(),
34 | royaltyPercentageBps: 10,
35 | maxSupply: 1000,
36 | pricePerMint: pricePerMint
37 | }
38 | );
39 |
40 | await parent.deployed();
41 | await child.deployed();
42 | console.log(
43 | `Sample contracts deployed to ${parent.address} and ${child.address}`
44 | );
45 |
46 | // Mint tokens 1 to totalTokens
47 | console.log("Minting parent NFTs");
48 | let tx = await parent.mint(owner.address, totalTokens, {
49 | value: pricePerMint.mul(totalTokens),
50 | });
51 | await tx.wait();
52 | console.log(`Minted ${totalTokens} tokens`);
53 | let totalSupply = await parent.totalSupply();
54 | console.log("Total parent tokens: %s", totalSupply);
55 |
56 | // Mint 2 children on each parent
57 | console.log("Minting child NFTs");
58 | let allTx: ContractTransaction[] = [];
59 | for (let i = 1; i <= totalTokens; i++) {
60 | let tx = await child.nestMint(parent.address, 2, i, {
61 | value: pricePerMint.mul(2),
62 | });
63 | allTx.push(tx);
64 | }
65 | console.log("Added 2 chunkies per kanaria");
66 | console.log("Awaiting for all tx to finish...");
67 | await Promise.all(allTx.map((tx) => tx.wait()));
68 |
69 | totalSupply = await child.totalSupply();
70 | console.log("Total child tokens: %s", totalSupply);
71 |
72 | console.log("Inspecting child NFT with the ID of 1");
73 | let parentId = await child.ownerOf(1);
74 | let rmrkParent = await child.directOwnerOf(1);
75 | console.log("Chunky's id 1 owner is ", parentId);
76 | console.log("Chunky's id 1 rmrk owner is ", rmrkParent);
77 | console.log("Parent address: ", parent.address);
78 |
79 | // Accept the first child for kanaria id 1:
80 | console.log("Accepting the fist child NFT for the parent NFT with ID 1");
81 | tx = await parent.acceptChild(1, 0, child.address, 1);
82 | await tx.wait();
83 |
84 | // Show accepted and pending children
85 | console.log(
86 | "Exaimning accepted and pending children of parent NFT with ID 1"
87 | );
88 | console.log("Children: ", await parent.childrenOf(1));
89 | console.log("Pending: ", await parent.pendingChildrenOf(1));
90 |
91 | // Send 1st child to owner:
92 | console.log("Removing the nested NFT from the parent token with the ID of 1");
93 | tx = await parent.transferChild(1, owner.address, 0, 0, child.address, 1, false, "0x");
94 | await tx.wait();
95 |
96 | parentId = await child.ownerOf(1);
97 | rmrkParent = await child.directOwnerOf(1);
98 | console.log("Chunky's id 1 parent is ", parentId);
99 | console.log("Chunky's id 1 rmrk owner is ", rmrkParent);
100 | }
101 |
102 | // main2();
103 | main().catch((error) => {
104 | console.error(error);
105 | process.exitCode = 1;
106 | });
107 |
--------------------------------------------------------------------------------
/scripts/splitEquippableUserJourney.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 | import { BigNumber } from "ethers";
3 | import {
4 | SimpleCatalog,
5 | SimpleExternalEquip,
6 | SimpleNestableExternalEquip,
7 | RMRKEquipRenderUtils,
8 | } from "../typechain-types";
9 | import { ContractTransaction } from "ethers";
10 |
11 | const pricePerMint = ethers.utils.parseEther("0.0001");
12 | const totalBirds = 5;
13 |
14 | async function main() {
15 | const [nestableKanaria, kanariaEquip, nestableGem, gemEquip, base, views] =
16 | await deployContracts();
17 |
18 | // Notice that most of these steps will happen at different points in time
19 | // Here we do all in one go to demonstrate how to use it.
20 | await setupCatalog(base, gemEquip.address);
21 | await mintTokens(nestableKanaria, nestableGem);
22 | await addKanariaAssets(kanariaEquip, base.address);
23 | await addGemAssets(gemEquip, kanariaEquip.address, base.address);
24 | await equipGems(kanariaEquip);
25 | await composeEquippables(views, kanariaEquip.address);
26 | }
27 |
28 | async function deployContracts(): Promise<
29 | [
30 | SimpleNestableExternalEquip,
31 | SimpleExternalEquip,
32 | SimpleNestableExternalEquip,
33 | SimpleExternalEquip,
34 | SimpleCatalog,
35 | RMRKEquipRenderUtils
36 | ]
37 | > {
38 | const [beneficiary] = await ethers.getSigners();
39 | const equipFactory = await ethers.getContractFactory("SimpleExternalEquip");
40 | const nestableFactory = await ethers.getContractFactory(
41 | "SimpleNestableExternalEquip"
42 | );
43 | const catalogFactory = await ethers.getContractFactory("SimpleCatalog");
44 | const viewsFactory = await ethers.getContractFactory("RMRKEquipRenderUtils");
45 |
46 | const nestableKanaria: SimpleNestableExternalEquip =
47 | await nestableFactory.deploy(
48 | ethers.constants.AddressZero,
49 | "Kanaria",
50 | "KAN",
51 | "ipfs://collectionMeta",
52 | "ipfs://tokenMeta",
53 | {
54 | erc20TokenAddress: ethers.constants.AddressZero,
55 | tokenUriIsEnumerable: true,
56 | royaltyRecipient: await beneficiary.getAddress(),
57 | royaltyPercentageBps: 10,
58 | maxSupply: 1000,
59 | pricePerMint: pricePerMint
60 | }
61 | );
62 | const nestableGem: SimpleNestableExternalEquip = await nestableFactory.deploy(
63 | ethers.constants.AddressZero,
64 | "Gem",
65 | "GM",
66 | "ipfs://collectionMeta",
67 | "ipfs://tokenMeta",
68 | {
69 | erc20TokenAddress: ethers.constants.AddressZero,
70 | tokenUriIsEnumerable: true,
71 | royaltyRecipient: await beneficiary.getAddress(),
72 | royaltyPercentageBps: 10,
73 | maxSupply: 3000,
74 | pricePerMint: pricePerMint
75 | }
76 | );
77 |
78 | const kanariaEquip: SimpleExternalEquip = await equipFactory.deploy(
79 | nestableKanaria.address
80 | );
81 | const gemEquip: SimpleExternalEquip = await equipFactory.deploy(
82 | nestableGem.address
83 | );
84 | const base: SimpleCatalog = await catalogFactory.deploy("KB", "svg");
85 | const views: RMRKEquipRenderUtils = await viewsFactory.deploy();
86 |
87 | await nestableKanaria.deployed();
88 | await kanariaEquip.deployed();
89 | await nestableGem.deployed();
90 | await gemEquip.deployed();
91 | await base.deployed();
92 | await views.deployed();
93 |
94 | const allTx = [
95 | await nestableKanaria.setEquippableAddress(kanariaEquip.address),
96 | await nestableGem.setEquippableAddress(gemEquip.address),
97 | ];
98 | await Promise.all(allTx.map((tx) => tx.wait()));
99 | console.log(
100 | `Sample contracts deployed to ${nestableKanaria.address} | ${kanariaEquip.address}, ${nestableGem.address} | ${gemEquip.address} and ${base.address}`
101 | );
102 |
103 | return [nestableKanaria, kanariaEquip, nestableGem, gemEquip, base, views];
104 | }
105 |
106 | async function setupCatalog(base: SimpleCatalog, gemAddress: string): Promise {
107 | // Setup base with 2 fixed part options for background, head, body and wings.
108 | // Also 3 slot options for gems
109 | const tx = await base.addPartList([
110 | {
111 | // Background option 1
112 | partId: 1,
113 | part: {
114 | itemType: 2, // Fixed
115 | z: 0,
116 | equippable: [],
117 | metadataURI: "ipfs://backgrounds/1.svg",
118 | },
119 | },
120 | {
121 | // Background option 2
122 | partId: 2,
123 | part: {
124 | itemType: 2, // Fixed
125 | z: 0,
126 | equippable: [],
127 | metadataURI: "ipfs://backgrounds/2.svg",
128 | },
129 | },
130 | {
131 | // Head option 1
132 | partId: 3,
133 | part: {
134 | itemType: 2, // Fixed
135 | z: 3,
136 | equippable: [],
137 | metadataURI: "ipfs://heads/1.svg",
138 | },
139 | },
140 | {
141 | // Head option 2
142 | partId: 4,
143 | part: {
144 | itemType: 2, // Fixed
145 | z: 3,
146 | equippable: [],
147 | metadataURI: "ipfs://heads/2.svg",
148 | },
149 | },
150 | {
151 | // Body option 1
152 | partId: 5,
153 | part: {
154 | itemType: 2, // Fixed
155 | z: 2,
156 | equippable: [],
157 | metadataURI: "ipfs://body/1.svg",
158 | },
159 | },
160 | {
161 | // Body option 2
162 | partId: 6,
163 | part: {
164 | itemType: 2, // Fixed
165 | z: 2,
166 | equippable: [],
167 | metadataURI: "ipfs://body/2.svg",
168 | },
169 | },
170 | {
171 | // Wings option 1
172 | partId: 7,
173 | part: {
174 | itemType: 2, // Fixed
175 | z: 1,
176 | equippable: [],
177 | metadataURI: "ipfs://wings/1.svg",
178 | },
179 | },
180 | {
181 | // Wings option 2
182 | partId: 8,
183 | part: {
184 | itemType: 2, // Fixed
185 | z: 1,
186 | equippable: [],
187 | metadataURI: "ipfs://wings/2.svg",
188 | },
189 | },
190 | {
191 | // Gems slot 1
192 | partId: 9,
193 | part: {
194 | itemType: 1, // Slot
195 | z: 4,
196 | equippable: [gemAddress], // Only gems tokens can be equipped here
197 | metadataURI: "",
198 | },
199 | },
200 | {
201 | // Gems slot 2
202 | partId: 10,
203 | part: {
204 | itemType: 1, // Slot
205 | z: 4,
206 | equippable: [gemAddress], // Only gems tokens can be equipped here
207 | metadataURI: "",
208 | },
209 | },
210 | {
211 | // Gems slot 3
212 | partId: 11,
213 | part: {
214 | itemType: 1, // Slot
215 | z: 4,
216 | equippable: [gemAddress], // Only gems tokens can be equipped here
217 | metadataURI: "",
218 | },
219 | },
220 | ]);
221 | await tx.wait();
222 | console.log("Catalog is set");
223 | }
224 |
225 | async function mintTokens(
226 | kanaria: SimpleNestableExternalEquip,
227 | gem: SimpleNestableExternalEquip
228 | ): Promise {
229 | const [ , tokenOwner] = await ethers.getSigners();
230 |
231 | // Mint some kanarias
232 | let tx = await kanaria.mint(tokenOwner.address, totalBirds, {
233 | value: pricePerMint.mul(totalBirds),
234 | });
235 | await tx.wait();
236 | console.log(`Minted ${totalBirds} kanarias`);
237 |
238 | // Mint 3 gems into each nestableKanaria
239 | let allTx: ContractTransaction[] = [];
240 | for (let i = 1; i <= totalBirds; i++) {
241 | let tx = await gem.nestMint(kanaria.address, 3, i, {
242 | value: pricePerMint.mul(3),
243 | });
244 | allTx.push(tx);
245 | }
246 | await Promise.all(allTx.map((tx) => tx.wait()));
247 | console.log(`Minted 3 gems into each nestableKanaria`);
248 |
249 | // Accept 3 gems for each kanaria
250 | console.log("Accepting Gems");
251 |
252 | for (let tokenId = 1; tokenId <= totalBirds; tokenId++) {
253 | allTx = [
254 | await kanaria.connect(tokenOwner).acceptChild(
255 | tokenId,
256 | 2,
257 | gem.address,
258 | 3 * tokenId,
259 | ),
260 | await kanaria.connect(tokenOwner).acceptChild(tokenId, 1, gem.address, 3 * tokenId - 1),
261 | await kanaria.connect(tokenOwner).acceptChild(tokenId, 0, gem.address, 3 * tokenId - 2),
262 | ];
263 | }
264 | await Promise.all(allTx.map((tx) => tx.wait()));
265 | console.log(`Accepted gems for each kanaria`);
266 | }
267 |
268 | async function addKanariaAssets(
269 | kanaria: SimpleExternalEquip,
270 | catalogAddress: string
271 | ): Promise {
272 | const [ , tokenOwner] = await ethers.getSigners();
273 | const assetDefaultId = 1;
274 | const assetComposedId = 2;
275 | let allTx: ContractTransaction[] = [];
276 | let tx = await kanaria.addEquippableAssetEntry(
277 | 0, // Only used for assets meant to equip into others
278 | ethers.constants.AddressZero, // base is not needed here
279 | "ipfs://default.png",
280 | []
281 | );
282 | allTx.push(tx);
283 |
284 | tx = await kanaria.addEquippableAssetEntry(
285 | 0, // Only used for assets meant to equip into others
286 | catalogAddress, // Since we're using parts, we must define the base
287 | "ipfs://meta1.json",
288 | [1, 3, 5, 7, 9, 10, 11], // We're using first background, head, body and wings and state that this can receive the 3 slot parts for gems
289 | );
290 | allTx.push(tx);
291 | // Wait for both assets to be added
292 | await Promise.all(allTx.map((tx) => tx.wait()));
293 | console.log("Added 2 asset entries");
294 |
295 | // Add assets to token
296 | const tokenId = 1;
297 | allTx = [
298 | await kanaria.addAssetToToken(tokenId, assetDefaultId, 0),
299 | await kanaria.addAssetToToken(tokenId, assetComposedId, 0),
300 | ];
301 | await Promise.all(allTx.map((tx) => tx.wait()));
302 | console.log("Added assets to token 1");
303 |
304 | // Accept both assets:
305 | tx = await kanaria.connect(tokenOwner).acceptAsset(tokenId, 0, assetDefaultId);
306 | await tx.wait();
307 | tx = await kanaria.connect(tokenOwner).acceptAsset(tokenId, 0, assetComposedId);
308 | await tx.wait();
309 | console.log("Assets accepted");
310 | }
311 |
312 | async function addGemAssets(
313 | gem: SimpleExternalEquip,
314 | kanariaAddress: string,
315 | catalogAddress: string
316 | ): Promise {
317 | const [ , tokenOwner] = await ethers.getSigners();
318 | // We'll add 4 assets for each nestableGem, a full version and 3 versions matching each slot.
319 | // We will have only 2 types of gems -> 4x2: 8 assets.
320 | // This is not composed by others, so fixed and slot parts are never used.
321 | const gemVersions = 4;
322 |
323 | // These refIds are used from the child's perspective, to group assets that can be equipped into a parent
324 | // With it, we avoid the need to do set it asset by asset
325 | const equippableRefIdLeftGem = 1;
326 | const equippableRefIdMidGem = 2;
327 | const equippableRefIdRightGem = 3;
328 |
329 | // We can do a for loop, but this makes it clearer.
330 | let allTx = [
331 | await gem.addEquippableAssetEntry(
332 | // Full version for first type of gem, no need of refId or base
333 | 0,
334 | catalogAddress,
335 | `ipfs://gems/typeA/full.svg`,
336 | []
337 | ),
338 | await gem.addEquippableAssetEntry(
339 | // Equipped into left slot for first type of gem
340 | equippableRefIdLeftGem,
341 | catalogAddress,
342 | `ipfs://gems/typeA/left.svg`,
343 | []
344 | ),
345 | await gem.addEquippableAssetEntry(
346 | // Equipped into mid slot for first type of gem
347 | equippableRefIdMidGem,
348 | catalogAddress,
349 | `ipfs://gems/typeA/mid.svg`,
350 | []
351 | ),
352 | await gem.addEquippableAssetEntry(
353 | // Equipped into left slot for first type of gem
354 | equippableRefIdRightGem,
355 | catalogAddress,
356 | `ipfs://gems/typeA/right.svg`,
357 | []
358 | ),
359 | await gem.addEquippableAssetEntry(
360 | // Full version for second type of gem, no need of refId or base
361 | 0,
362 | ethers.constants.AddressZero,
363 | `ipfs://gems/typeB/full.svg`,
364 | []
365 | ),
366 | await gem.addEquippableAssetEntry(
367 | // Equipped into left slot for second type of gem
368 | equippableRefIdLeftGem,
369 | catalogAddress,
370 | `ipfs://gems/typeB/left.svg`,
371 | []
372 | ),
373 | await gem.addEquippableAssetEntry(
374 | // Equipped into mid slot for second type of gem
375 | equippableRefIdMidGem,
376 | catalogAddress,
377 | `ipfs://gems/typeB/mid.svg`,
378 | []
379 | ),
380 | await gem.addEquippableAssetEntry(
381 | // Equipped into right slot for second type of gem
382 | equippableRefIdRightGem,
383 | catalogAddress,
384 | `ipfs://gems/typeB/right.svg`,
385 | []
386 | ),
387 | ];
388 |
389 | await Promise.all(allTx.map((tx) => tx.wait()));
390 | console.log(
391 | "Added 8 nestableGem assets. 2 Types of gems with full, left, mid and right versions."
392 | );
393 |
394 | // 9, 10 and 11 are the slot part ids for the gems, defined on the base.
395 | // e.g. Any asset on nestableGem, which sets its equippableGroupId to equippableRefIdLeftGem
396 | // will be considered a valid equip into any nestableKanaria on slot 9 (left nestableGem).
397 | allTx = [
398 | await gem.setValidParentForEquippableGroup(
399 | equippableRefIdLeftGem,
400 | kanariaAddress,
401 | 9
402 | ),
403 | await gem.setValidParentForEquippableGroup(
404 | equippableRefIdMidGem,
405 | kanariaAddress,
406 | 10
407 | ),
408 | await gem.setValidParentForEquippableGroup(
409 | equippableRefIdRightGem,
410 | kanariaAddress,
411 | 11
412 | ),
413 | ];
414 | await Promise.all(allTx.map((tx) => tx.wait()));
415 |
416 | // We add assets of type A to nestableGem 1 and 2, and type Bto nestableGem 3. Both are nested into the first nestableKanaria
417 | // This means gems 1 and 2 will have the same asset, which is totally valid.
418 | allTx = [
419 | await gem.addAssetToToken(1, 1, 0),
420 | await gem.addAssetToToken(1, 2, 0),
421 | await gem.addAssetToToken(1, 3, 0),
422 | await gem.addAssetToToken(1, 4, 0),
423 | await gem.addAssetToToken(2, 1, 0),
424 | await gem.addAssetToToken(2, 2, 0),
425 | await gem.addAssetToToken(2, 3, 0),
426 | await gem.addAssetToToken(2, 4, 0),
427 | await gem.addAssetToToken(3, 5, 0),
428 | await gem.addAssetToToken(3, 6, 0),
429 | await gem.addAssetToToken(3, 7, 0),
430 | await gem.addAssetToToken(3, 8, 0),
431 | ];
432 | await Promise.all(allTx.map((tx) => tx.wait()));
433 | console.log("Added 4 assets to each of 3 gems.");
434 |
435 | // We accept each asset for all gems
436 | allTx = [
437 | await gem.connect(tokenOwner).acceptAsset(1, 3, 4),
438 | await gem.connect(tokenOwner).acceptAsset(1, 2, 3),
439 | await gem.connect(tokenOwner).acceptAsset(1, 1, 2),
440 | await gem.connect(tokenOwner).acceptAsset(1, 0, 1),
441 | await gem.connect(tokenOwner).acceptAsset(2, 3, 4),
442 | await gem.connect(tokenOwner).acceptAsset(2, 2, 3),
443 | await gem.connect(tokenOwner).acceptAsset(2, 1, 2),
444 | await gem.connect(tokenOwner).acceptAsset(2, 0, 1),
445 | await gem.connect(tokenOwner).acceptAsset(3, 3, 8),
446 | await gem.connect(tokenOwner).acceptAsset(3, 2, 7),
447 | await gem.connect(tokenOwner).acceptAsset(3, 1, 6),
448 | await gem.connect(tokenOwner).acceptAsset(3, 0, 5),
449 | ];
450 | await Promise.all(allTx.map((tx) => tx.wait()));
451 | console.log("Accepted 4 assets to each of 3 gems.");
452 | }
453 |
454 | async function equipGems(kanariaEquip: SimpleExternalEquip): Promise {
455 | const [ , tokenOwner] = await ethers.getSigners();
456 | const allTx = [
457 | await kanariaEquip.connect(tokenOwner).equip({
458 | tokenId: 1, // Kanaria 1
459 | childIndex: 2, // Gem 1 is on position 2
460 | assetId: 2, // Asset for the kanaria which is composable
461 | slotPartId: 9, // left gem slot
462 | childAssetId: 2, // Asset id for child meant for the left gem
463 | }),
464 | await kanariaEquip.connect(tokenOwner).equip({
465 | tokenId: 1, // Kanaria 1
466 | childIndex: 1, // Gem 2 is on position 1
467 | assetId: 2, // Asset for the kanaria which is composable
468 | slotPartId: 10, // mid gem slot
469 | childAssetId: 3, // Asset id for child meant for the mid gem
470 | }),
471 | await kanariaEquip.connect(tokenOwner).equip({
472 | tokenId: 1, // Kanaria 1
473 | childIndex: 0, // Gem 3 is on position 0
474 | assetId: 2, // Asset for the kanaria which is composable
475 | slotPartId: 11, // right gem slot
476 | childAssetId: 8, // Asset id for child meant for the right gem
477 | }),
478 | ];
479 | await Promise.all(allTx.map((tx) => tx.wait()));
480 | console.log("Equipped 3 gems into first nestableKanaria");
481 | }
482 |
483 | async function composeEquippables(
484 | views: RMRKEquipRenderUtils,
485 | kanariaAddress: string
486 | ): Promise {
487 | const tokenId = 1;
488 | const assetId = 2;
489 | console.log(
490 | "Composed: ",
491 | await views.composeEquippables(kanariaAddress, tokenId, assetId)
492 | );
493 | }
494 |
495 | main().catch((error) => {
496 | console.error(error);
497 | process.exitCode = 1;
498 | });
499 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "skipLibCheck": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------