├── .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 | ![RMRK Legos infographic](img/RMRKLegoInfographics.png) 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 | ![MultiAsset RMRK lego](../../img/4.jpg) 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 | ![Nestable RMRK lego](../../img/5.jpg) 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 | ![Nestable RMRK lego with MultiAsset RMRK lego](../../img/6.jpg) 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 | ![Split Equippable RMRK lego composite](../../img/9.jpg) 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 | --------------------------------------------------------------------------------