From 16dc1cf4f23cd54013250e70d21dc42a4567910d Mon Sep 17 00:00:00 2001 From: steven2308 Date: Thu, 5 Jan 2023 11:18:04 -0500 Subject: [PATCH 01/26] Enables contributor to nest mint on premint implementations. --- contracts/implementations/premint/RMRKEquippableImplPreMint.sol | 2 +- contracts/implementations/premint/RMRKNestableImplPreMint.sol | 2 +- .../premint/RMRKNestableMultiAssetImplPreMint.sol | 2 +- test/implementations/premint.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/implementations/premint/RMRKEquippableImplPreMint.sol b/contracts/implementations/premint/RMRKEquippableImplPreMint.sol index c897c130..7e8ec956 100644 --- a/contracts/implementations/premint/RMRKEquippableImplPreMint.sol +++ b/contracts/implementations/premint/RMRKEquippableImplPreMint.sol @@ -74,7 +74,7 @@ contract RMRKEquippableImplPreMint is RMRKAbstractEquippableImpl { address to, uint256 numToMint, uint256 destinationId - ) public virtual notLocked onlyOwner { + ) public virtual notLocked onlyOwnerOrContributor { (uint256 nextToken, uint256 totalSupplyOffset) = _preMint(numToMint); for (uint256 i = nextToken; i < totalSupplyOffset; ) { diff --git a/contracts/implementations/premint/RMRKNestableImplPreMint.sol b/contracts/implementations/premint/RMRKNestableImplPreMint.sol index 68765c14..d22d6941 100644 --- a/contracts/implementations/premint/RMRKNestableImplPreMint.sol +++ b/contracts/implementations/premint/RMRKNestableImplPreMint.sol @@ -74,7 +74,7 @@ contract RMRKNestableImplPreMint is RMRKAbstractNestableImpl { address to, uint256 numToMint, uint256 destinationId - ) public payable virtual notLocked onlyOwner { + ) public payable virtual notLocked onlyOwnerOrContributor { (uint256 nextToken, uint256 totalSupplyOffset) = _preMint(numToMint); for (uint256 i = nextToken; i < totalSupplyOffset; ) { diff --git a/contracts/implementations/premint/RMRKNestableMultiAssetImplPreMint.sol b/contracts/implementations/premint/RMRKNestableMultiAssetImplPreMint.sol index d12728b1..a327263c 100644 --- a/contracts/implementations/premint/RMRKNestableMultiAssetImplPreMint.sol +++ b/contracts/implementations/premint/RMRKNestableMultiAssetImplPreMint.sol @@ -76,7 +76,7 @@ contract RMRKNestableMultiAssetImplPreMint is address to, uint256 numToMint, uint256 destinationId - ) public payable virtual notLocked onlyOwner { + ) public payable virtual notLocked onlyOwnerOrContributor { (uint256 nextToken, uint256 totalSupplyOffset) = _preMint(numToMint); for (uint256 i = nextToken; i < totalSupplyOffset; ) { diff --git a/test/implementations/premint.ts b/test/implementations/premint.ts index 10d588f1..4e8f442c 100644 --- a/test/implementations/premint.ts +++ b/test/implementations/premint.ts @@ -131,7 +131,7 @@ async function shouldControlValidPreMinting(): Promise { const notOwner = addrs[0]; await expect( this.token.connect(notOwner).nestMint(this.token.address, 1, 1), - ).to.be.revertedWithCustomError(this.token, 'RMRKNotOwner'); + ).to.be.revertedWithCustomError(this.token, 'RMRKNotOwnerOrContributor'); }); it('can nest mint if owner', async function () { From f01574cb4de5a13e727b16ee4035054376eba6dd Mon Sep 17 00:00:00 2001 From: steven2308 Date: Wed, 4 Jan 2023 11:44:45 -0500 Subject: [PATCH 02/26] Adds equip and unequip to Equippables interface. --- contracts/RMRK/equippable/IRMRKEquippable.sol | 28 +++++++++++++++ contracts/RMRK/equippable/RMRKEquippable.sol | 18 ++-------- .../RMRK/equippable/RMRKExternalEquip.sol | 18 ++-------- docs/RMRK/equippable/IRMRKEquippable.md | 34 +++++++++++++++++++ docs/RMRK/equippable/IRMRKExternalEquip.md | 34 +++++++++++++++++++ test/interfaces.ts | 2 +- 6 files changed, 101 insertions(+), 33 deletions(-) diff --git a/contracts/RMRK/equippable/IRMRKEquippable.sol b/contracts/RMRK/equippable/IRMRKEquippable.sol index 74080986..cf86ece7 100644 --- a/contracts/RMRK/equippable/IRMRKEquippable.sol +++ b/contracts/RMRK/equippable/IRMRKEquippable.sol @@ -93,6 +93,34 @@ interface IRMRKEquippable is IRMRKMultiAsset { address parentAddress ); + /** + * @notice Used to equip a child into a token. + * @dev The `IntakeEquip` stuct contains the following data: + * [ + * tokenId, + * childIndex, + * assetId, + * slotPartId, + * childAssetId + * ] + * @param data An `IntakeEquip` struct specifying the equip data + */ + function equip(IntakeEquip memory data) external; + + /** + * @notice Used to unequip child from parent token. + * @dev This can only be called by the owner of the token or by an account that has been granted permission to + * manage the given token by the current owner. + * @param tokenId ID of the parent from which the child is being unequipped + * @param assetId ID of the parent's asset that contains the `Slot` into which the child is equipped + * @param slotPartId ID of the `Slot` from which to unequip the child + */ + function unequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) external; + /** * @notice Used to check whether the token has a given child equipped. * @dev This is used to prevent from transferring a child that is equipped. diff --git a/contracts/RMRK/equippable/RMRKEquippable.sol b/contracts/RMRK/equippable/RMRKEquippable.sol index e735c42b..5dc0f6fd 100644 --- a/contracts/RMRK/equippable/RMRKEquippable.sol +++ b/contracts/RMRK/equippable/RMRKEquippable.sol @@ -317,16 +317,7 @@ contract RMRKEquippable is } /** - * @notice Used to equip a child into a token. - * @dev The `IntakeEquip` stuct contains the following data: - * [ - * tokenId, - * childIndex, - * assetId, - * slotPartId, - * childAssetId - * ] - * @param data An `IntakeEquip` struct specifying the equip data + * @inheritdoc IRMRKEquippable */ function equip( IntakeEquip memory data @@ -424,12 +415,7 @@ contract RMRKEquippable is } /** - * @notice Used to unequip child from parent token. - * @dev This can only be called by the owner of the token or by an account that has been granted permission to - * manage the given token by the current owner. - * @param tokenId ID of the parent from which the child is being unequipped - * @param assetId ID of the parent's asset that contains the `Slot` into which the child is equipped - * @param slotPartId ID of the `Slot` from which to unequip the child + * @inheritdoc IRMRKEquippable */ function unequip( uint256 tokenId, diff --git a/contracts/RMRK/equippable/RMRKExternalEquip.sol b/contracts/RMRK/equippable/RMRKExternalEquip.sol index 6f263359..1ddc96cd 100644 --- a/contracts/RMRK/equippable/RMRKExternalEquip.sol +++ b/contracts/RMRK/equippable/RMRKExternalEquip.sol @@ -289,16 +289,7 @@ contract RMRKExternalEquip is // ------------------------------- EQUIPPING ------------------------------ /** - * @notice Used to equip a child into a token. - * @dev The `IntakeEquip` stuct contains the following data: - * [ - * tokenId, - * childIndex, - * assetId, - * slotPartId, - * childAssetId - * ] - * @param data An `IntakeEquip` struct specifying the equip data + * @inheritdoc IRMRKEquippable */ function equip( IntakeEquip memory data @@ -396,12 +387,7 @@ contract RMRKExternalEquip is } /** - * @notice Used to unequip child from parent token. - * @dev This can only be called by the owner of the token or by an account that has been granted permission to - * manage the given token by the current owner. - * @param tokenId ID of the parent from which the child is being unequipped - * @param assetId ID of the parent's asset that contains the `Slot` into which the child is equipped - * @param slotPartId ID of the `Slot` from which to unequip the child + * @inheritdoc IRMRKEquippable */ function unequip( uint256 tokenId, diff --git a/docs/RMRK/equippable/IRMRKEquippable.md b/docs/RMRK/equippable/IRMRKEquippable.md index 4f9f46be..cf1d5526 100644 --- a/docs/RMRK/equippable/IRMRKEquippable.md +++ b/docs/RMRK/equippable/IRMRKEquippable.md @@ -70,6 +70,22 @@ Used to verify whether a token can be equipped into a given parent's slot. |---|---|---| | _0 | bool | bool The boolean indicating whether the token with the given asset can be equipped into the desired slot | +### equip + +```solidity +function equip(IRMRKEquippable.IntakeEquip data) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| data | IRMRKEquippable.IntakeEquip | undefined | + ### getActiveAssetPriorities ```solidity @@ -392,6 +408,24 @@ function supportsInterface(bytes4 interfaceId) external view returns (bool) |---|---|---| | _0 | bool | undefined | +### unequip + +```solidity +function unequip(uint256 tokenId, uint64 assetId, uint64 slotPartId) external nonpayable +``` + +Used to unequip child from parent token. + +*This can only be called by the owner of the token or by an account that has been granted permission to manage the given token by the current owner.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the parent from which the child is being unequipped | +| assetId | uint64 | ID of the parent's asset that contains the `Slot` into which the child is equipped | +| slotPartId | uint64 | ID of the `Slot` from which to unequip the child | + ## Events diff --git a/docs/RMRK/equippable/IRMRKExternalEquip.md b/docs/RMRK/equippable/IRMRKExternalEquip.md index 26a9f5b8..76a3ed4d 100644 --- a/docs/RMRK/equippable/IRMRKExternalEquip.md +++ b/docs/RMRK/equippable/IRMRKExternalEquip.md @@ -70,6 +70,22 @@ Used to verify whether a token can be equipped into a given parent's slot. |---|---|---| | _0 | bool | bool The boolean indicating whether the token with the given asset can be equipped into the desired slot | +### equip + +```solidity +function equip(IRMRKEquippable.IntakeEquip data) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| data | IRMRKEquippable.IntakeEquip | undefined | + ### getActiveAssetPriorities ```solidity @@ -409,6 +425,24 @@ function supportsInterface(bytes4 interfaceId) external view returns (bool) |---|---|---| | _0 | bool | undefined | +### unequip + +```solidity +function unequip(uint256 tokenId, uint64 assetId, uint64 slotPartId) external nonpayable +``` + +Used to unequip child from parent token. + +*This can only be called by the owner of the token or by an account that has been granted permission to manage the given token by the current owner.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the parent from which the child is being unequipped | +| assetId | uint64 | ID of the parent's asset that contains the `Slot` into which the child is equipped | +| slotPartId | uint64 | ID of the `Slot` from which to unequip the child | + ## Events diff --git a/test/interfaces.ts b/test/interfaces.ts index c81755aa..9ddc0285 100644 --- a/test/interfaces.ts +++ b/test/interfaces.ts @@ -3,7 +3,7 @@ const IERC721 = '0x80ac58cd'; const IERC721Metadata = '0x5b5e139f'; const IOtherInterface = '0xffffffff'; const IRMRKCatalog = '0xd912401f'; -const IRMRKEquippable = '0xd239c420'; +const IRMRKEquippable = '0x28bc9ae4'; const IRMRKExternalEquip = '0x289dfee5'; const IRMRKMultiAsset = '0xd1526708'; const IRMRKNestable = '0x42b0e56f'; From 03e00239ec483995f8ddc2e4b6086cd4471cbff5 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Wed, 4 Jan 2023 11:45:08 -0500 Subject: [PATCH 03/26] Bumps version to 0.22.1. --- contracts/RMRK/core/RMRKCore.sol | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/RMRK/core/RMRKCore.sol b/contracts/RMRK/core/RMRKCore.sol index dba59e78..7a0ce7cd 100644 --- a/contracts/RMRK/core/RMRKCore.sol +++ b/contracts/RMRK/core/RMRKCore.sol @@ -12,7 +12,7 @@ import "./IRMRKCore.sol"; */ contract RMRKCore is IRMRKCore { /// @notice Version of the @rmrk-team/evm-contracts package - string public constant VERSION = "0.22.0"; + string public constant VERSION = "0.22.1"; /** * @notice Used to initialize the smart contract. diff --git a/package.json b/package.json index 4ae9ad57..386e7ee0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmrk-team/evm-contracts", - "version": "0.22.0", + "version": "0.22.1", "license": "Apache-2.0", "files": [ "contracts/RMRK/*", From b292fd230f699be03032a64e9255adf6adb95f4a Mon Sep 17 00:00:00 2001 From: steven2308 Date: Wed, 4 Jan 2023 13:09:32 -0500 Subject: [PATCH 04/26] Adds emotable extension. --- .../RMRK/extension/emotable/IRMRKEmotable.sol | 31 + .../RMRK/extension/emotable/RMRKEmotable.sol | 79 ++ .../emotable/RMRKMultiAssetEmotableMock.sol | 43 + docs/RMRK/extension/emotable/IRMRKEmotable.md | 78 ++ docs/RMRK/extension/emotable/RMRKEmotable.md | 100 ++ .../emotable/RMRKMultiAssetEmotableMock.md | 1005 +++++++++++++++++ test/extensions/emotable.ts | 89 ++ test/interfaces.ts | 2 + 8 files changed, 1427 insertions(+) create mode 100644 contracts/RMRK/extension/emotable/IRMRKEmotable.sol create mode 100644 contracts/RMRK/extension/emotable/RMRKEmotable.sol create mode 100644 contracts/mocks/extensions/emotable/RMRKMultiAssetEmotableMock.sol create mode 100644 docs/RMRK/extension/emotable/IRMRKEmotable.md create mode 100644 docs/RMRK/extension/emotable/RMRKEmotable.md create mode 100644 docs/mocks/extensions/emotable/RMRKMultiAssetEmotableMock.md create mode 100644 test/extensions/emotable.ts diff --git a/contracts/RMRK/extension/emotable/IRMRKEmotable.sol b/contracts/RMRK/extension/emotable/IRMRKEmotable.sol new file mode 100644 index 00000000..09d66b11 --- /dev/null +++ b/contracts/RMRK/extension/emotable/IRMRKEmotable.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.16; + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/** + * @title IRMRKEmotable + * @author RMRK team + * @notice Interface smart contract of the RMRK emotable module. + */ +interface IRMRKEmotable is IERC165 { + /** + * @notice Used to get the number of emotes for a specific emoji on a token. + * @param tokenId ID of the token to check for emoji count + * @param emoji Unicode identifier of the emoji + * @return Number of emotes with the emoji on the token + */ + function getEmoteCount( + uint256 tokenId, + bytes4 emoji + ) external view returns (uint256); + + /** + * @notice Used to emote or undo an emote on a token. + * @param tokenId ID of the token being emoted + * @param emoji Unicode identifier of the emoji + * @param state whether to turn emote or undo. True for emote, false for undo + */ + function emote(uint256 tokenId, bytes4 emoji, bool state) external; +} diff --git a/contracts/RMRK/extension/emotable/RMRKEmotable.sol b/contracts/RMRK/extension/emotable/RMRKEmotable.sol new file mode 100644 index 00000000..2797d239 --- /dev/null +++ b/contracts/RMRK/extension/emotable/RMRKEmotable.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.16; + +import "./IRMRKEmotable.sol"; + +/** + * @title RMRKEmotable + * @author RMRK team + * @notice Smart contract of the RMRK Emotable module. + */ +abstract contract RMRKEmotable is IRMRKEmotable { + // Used to avoid double emoting and control undoing + mapping(address => mapping(uint256 => mapping(bytes4 => uint256))) + private _emotesPerAddress; // Cheaper than using a bool + mapping(uint256 => mapping(bytes4 => uint256)) private _emotesPerToken; + + event Emoted( + address indexed emoter, + uint256 indexed tokenId, + bytes4 emoji, + bool on + ); + + /** + * @inheritdoc IRMRKEmotable + */ + function getEmoteCount( + uint256 tokenId, + bytes4 emoji + ) public view returns (uint256) { + return _emotesPerToken[tokenId][emoji]; + } + + /** + * @notice Used to emote or undo an emote on a token. + * @param tokenId ID of the token being emoted + * @param emoji Unicode identifier of the emoji + * @param state whether to turn emote or undo. True for emote, false for undo + */ + function _emote( + uint256 tokenId, + bytes4 emoji, + bool state + ) internal virtual { + bool currentVal = _emotesPerAddress[msg.sender][tokenId][emoji] == 1; + if (currentVal != state) { + _beforeEmote(tokenId, emoji, state); + if (state) { + _emotesPerToken[tokenId][emoji] += 1; + } else { + _emotesPerToken[tokenId][emoji] -= 1; + } + _emotesPerAddress[msg.sender][tokenId][emoji] = state ? 1 : 0; + emit Emoted(msg.sender, tokenId, emoji, state); + } + } + + /** + * @notice Hook that is called before emote is added or removed. + * @param tokenId ID of the token being emoted + * @param emoji Unicode identifier of the emoji + * @param state whether to turn emote or undo. True for emote, false for undo + */ + function _beforeEmote( + uint256 tokenId, + bytes4 emoji, + bool state + ) internal virtual {} + + /** + * @inheritdoc IERC165 + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual returns (bool) { + return interfaceId == type(IRMRKEmotable).interfaceId; + } +} diff --git a/contracts/mocks/extensions/emotable/RMRKMultiAssetEmotableMock.sol b/contracts/mocks/extensions/emotable/RMRKMultiAssetEmotableMock.sol new file mode 100644 index 00000000..f96e1297 --- /dev/null +++ b/contracts/mocks/extensions/emotable/RMRKMultiAssetEmotableMock.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.16; + +import "../../../RMRK/multiasset/RMRKMultiAsset.sol"; +import "../../../RMRK/extension/emotable/RMRKEmotable.sol"; + +contract RMRKMultiAssetEmotableMock is RMRKEmotable, RMRKMultiAsset { + constructor( + string memory name, + string memory symbol + ) RMRKMultiAsset(name, symbol) {} + + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } + + function supportsInterface( + bytes4 interfaceId + ) + public + view + virtual + override(RMRKEmotable, RMRKMultiAsset) + returns (bool) + { + return + RMRKEmotable.supportsInterface(interfaceId) || + super.supportsInterface(interfaceId); + } + + function emote(uint256 tokenId, bytes4 emoji, bool on) public { + _emote(tokenId, emoji, on); + } + + function _beforeEmote( + uint256 tokenId, + bytes4, + bool + ) internal view override { + _requireMinted(tokenId); + } +} diff --git a/docs/RMRK/extension/emotable/IRMRKEmotable.md b/docs/RMRK/extension/emotable/IRMRKEmotable.md new file mode 100644 index 00000000..46947bf3 --- /dev/null +++ b/docs/RMRK/extension/emotable/IRMRKEmotable.md @@ -0,0 +1,78 @@ +# IRMRKEmotable + +*RMRK team* + +> IRMRKEmotable + +Interface smart contract of the RMRK emotable module. + + + +## Methods + +### emote + +```solidity +function emote(uint256 tokenId, bytes4 emoji, bool state) external nonpayable +``` + +Used to emote or undo an emote on a token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token being emoted | +| emoji | bytes4 | Unicode identifier of the emoji | +| state | bool | whether to turn emote or undo. True for emote, false for undo | + +### getEmoteCount + +```solidity +function getEmoteCount(uint256 tokenId, bytes4 emoji) external view returns (uint256) +``` + +Used to get the number of emotes for a specific emoji on a token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token to check for emoji count | +| emoji | bytes4 | Unicode identifier of the emoji | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | Number of emotes with the emoji on the token | + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + +*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + + + + diff --git a/docs/RMRK/extension/emotable/RMRKEmotable.md b/docs/RMRK/extension/emotable/RMRKEmotable.md new file mode 100644 index 00000000..7bd55005 --- /dev/null +++ b/docs/RMRK/extension/emotable/RMRKEmotable.md @@ -0,0 +1,100 @@ +# RMRKEmotable + +*RMRK team* + +> RMRKEmotable + +Smart contract of the RMRK Emotable module. + + + +## Methods + +### emote + +```solidity +function emote(uint256 tokenId, bytes4 emoji, bool state) external nonpayable +``` + +Used to emote or undo an emote on a token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token being emoted | +| emoji | bytes4 | Unicode identifier of the emoji | +| state | bool | whether to turn emote or undo. True for emote, false for undo | + +### getEmoteCount + +```solidity +function getEmoteCount(uint256 tokenId, bytes4 emoji) external view returns (uint256) +``` + +Used to get the number of emotes for a specific emoji on a token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token to check for emoji count | +| emoji | bytes4 | Unicode identifier of the emoji | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | Number of emotes with the emoji on the token | + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + +*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + + + +## Events + +### Emoted + +```solidity +event Emoted(address indexed emoter, uint256 indexed tokenId, bytes4 emoji, bool on) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| emoter `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | +| emoji | bytes4 | undefined | +| on | bool | undefined | + + + diff --git a/docs/mocks/extensions/emotable/RMRKMultiAssetEmotableMock.md b/docs/mocks/extensions/emotable/RMRKMultiAssetEmotableMock.md new file mode 100644 index 00000000..3125d411 --- /dev/null +++ b/docs/mocks/extensions/emotable/RMRKMultiAssetEmotableMock.md @@ -0,0 +1,1005 @@ +# RMRKMultiAssetEmotableMock + + + + + + + + + +## Methods + +### VERSION + +```solidity +function VERSION() external view returns (string) +``` + +Version of the @rmrk-team/evm-contracts package + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | string | undefined | + +### acceptAsset + +```solidity +function acceptAsset(uint256 tokenId, uint256 index, uint64 assetId) external nonpayable +``` + +Accepts an asset at from the pending array of given token. + +*Migrates the asset from the token's pending asset array to the token's active asset array.Active assets cannot be removed by anyone, but can be replaced by a new asset.Requirements: - The caller must own the token or be approved to manage the token's assets - `tokenId` must exist. - `index` must be in range of the length of the pending asset array.Emits an {AssetAccepted} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token for which to accept the pending asset | +| index | uint256 | Index of the asset in the pending array to accept | +| assetId | uint64 | ID of the asset expected to be in the index | + +### approve + +```solidity +function approve(address to, uint256 tokenId) external nonpayable +``` + + + +*Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist. Emits an {Approval} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| to | address | undefined | +| tokenId | uint256 | undefined | + +### approveForAssets + +```solidity +function approveForAssets(address to, uint256 tokenId) external nonpayable +``` + +Used to grant permission to the user to manage token's assets. + +*This differs from transfer approvals, as approvals are not cleared when the approved party accepts or rejects an asset, or sets asset priorities. This approval is cleared on token transfer.Only a single account can be approved at a time, so approving the `0x0` address clears previous approvals.Requirements: - The caller must own the token or be an approved operator. - `tokenId` must exist.Emits an {ApprovalForAssets} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| to | address | Address of the account to grant the approval to | +| tokenId | uint256 | ID of the token for which the approval to manage the assets is granted | + +### balanceOf + +```solidity +function balanceOf(address owner) external view returns (uint256) +``` + + + +*Returns the number of tokens in ``owner``'s account.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| owner | address | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### emote + +```solidity +function emote(uint256 tokenId, bytes4 emoji, bool on) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | undefined | +| emoji | bytes4 | undefined | +| on | bool | undefined | + +### getActiveAssetPriorities + +```solidity +function getActiveAssetPriorities(uint256 tokenId) external view returns (uint16[]) +``` + +Used to retrieve the priorities of the active resoources of a given token. + +*Asset priorities are a non-sequential array of uint16 values with an array size equal to active asset priorites.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token for which to retrieve the priorities of the active assets | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint16[] | uint16[] An array of priorities of the active assets of the given token | + +### getActiveAssets + +```solidity +function getActiveAssets(uint256 tokenId) external view returns (uint64[]) +``` + +Used to retrieve IDs of the active assets of given token. + +*Asset data is stored by reference, in order to access the data corresponding to the ID, call `getAssetMetadata(tokenId, assetId)`.You can safely get 10k* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token to retrieve the IDs of the active assets | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint64[] | uint64[] An array of active asset IDs of the given token | + +### getApproved + +```solidity +function getApproved(uint256 tokenId) external view returns (address) +``` + + + +*Returns the account approved for `tokenId` token. Requirements: - `tokenId` must exist.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +### getApprovedForAssets + +```solidity +function getApprovedForAssets(uint256 tokenId) external view returns (address) +``` + +Used to retrieve the address of the account approved to manage assets of a given token. + +*Requirements: - `tokenId` must exist.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token for which to retrieve the approved address | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | address Address of the account that is approved to manage the specified token's assets | + +### getAssetMetadata + +```solidity +function getAssetMetadata(uint256 tokenId, uint64 assetId) external view returns (string) +``` + +Used to fetch the asset metadata of the specified token's active asset with the given index. + +*Assets are stored by reference mapping `_assets[assetId]`.Can be overriden to implement enumerate, fallback or other custom logic.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token from which to retrieve the asset metadata | +| assetId | uint64 | Asset Id, must be in the active assets array | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | string | string The metadata of the asset belonging to the specified index in the token's active assets array | + +### getAssetReplacements + +```solidity +function getAssetReplacements(uint256 tokenId, uint64 newAssetId) external view returns (uint64) +``` + +Used to retrieve the asset that will be replaced if a given asset from the token's pending array is accepted. + +*Asset data is stored by reference, in order to access the data corresponding to the ID, call `getAssetMetadata(tokenId, assetId)`.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token to check | +| newAssetId | uint64 | ID of the pending asset which will be accepted | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint64 | uint64 ID of the asset which will be replaced | + +### getEmoteCount + +```solidity +function getEmoteCount(uint256 tokenId, bytes4 emoji) external view returns (uint256) +``` + +Used to get the number of emotes for a specific emoji on a token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token to check for emoji count | +| emoji | bytes4 | Unicode identifier of the emoji | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | Number of emotes with the emoji on the token | + +### getPendingAssets + +```solidity +function getPendingAssets(uint256 tokenId) external view returns (uint64[]) +``` + +Used to retrieve IDs of the pending assets of given token. + +*Asset data is stored by reference, in order to access the data corresponding to the ID, call `getAssetMetadata(tokenId, assetId)`.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token to retrieve the IDs of the pending assets | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint64[] | uint64[] An array of pending asset IDs of the given token | + +### isApprovedForAll + +```solidity +function isApprovedForAll(address owner, address operator) external view returns (bool) +``` + + + +*Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll}* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| owner | address | undefined | +| operator | address | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + +### isApprovedForAllForAssets + +```solidity +function isApprovedForAllForAssets(address owner, address operator) external view returns (bool) +``` + +Used to check whether the address has been granted the operator role by a given address or not. + +*See {setApprovalForAllForAssets}.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| owner | address | Address of the account that we are checking for whether it has granted the operator role | +| operator | address | Address of the account that we are checking whether it has the operator role or not | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | bool The boolean value indicating wehter the account we are checking has been granted the operator role | + +### mint + +```solidity +function mint(address to, uint256 tokenId) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| to | address | undefined | +| tokenId | uint256 | undefined | + +### name + +```solidity +function name() external view returns (string) +``` + +Used to retrieve the collection name. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | string | string Name of the collection | + +### ownerOf + +```solidity +function ownerOf(uint256 tokenId) external view returns (address) +``` + + + +*Returns the owner of the `tokenId` token. Requirements: - `tokenId` must exist.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +### rejectAllAssets + +```solidity +function rejectAllAssets(uint256 tokenId, uint256 maxRejections) external nonpayable +``` + +Rejects all assets from the pending array of a given token. + +*Effecitvely deletes the pending array.Requirements: - The caller must own the token or be approved to manage the token's assets - `tokenId` must exist.Emits a {AssetRejected} event with assetId = 0.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token of which to clear the pending array. | +| maxRejections | uint256 | Maximum number of expected assets to reject, used to prevent from rejecting assets which arrive just before this operation. | + +### rejectAsset + +```solidity +function rejectAsset(uint256 tokenId, uint256 index, uint64 assetId) external nonpayable +``` + +Rejects an asset from the pending array of given token. + +*Removes the asset from the token's pending asset array.Requirements: - The caller must own the token or be approved to manage the token's assets - `tokenId` must exist. - `index` must be in range of the length of the pending asset array.Emits a {AssetRejected} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token that the asset is being rejected from | +| index | uint256 | Index of the asset in the pending array to be rejected | +| assetId | uint64 | ID of the asset expected to be in the index | + +### safeTransferFrom + +```solidity +function safeTransferFrom(address from, address to, uint256 tokenId) external nonpayable +``` + + + +*Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients are aware of the ERC721 protocol to prevent tokens from being forever locked. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| from | address | undefined | +| to | address | undefined | +| tokenId | uint256 | undefined | + +### safeTransferFrom + +```solidity +function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external nonpayable +``` + + + +*Safely transfers `tokenId` token from `from` to `to`. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must exist and be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. Emits a {Transfer} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| from | address | undefined | +| to | address | undefined | +| tokenId | uint256 | undefined | +| data | bytes | undefined | + +### setApprovalForAll + +```solidity +function setApprovalForAll(address operator, bool approved) external nonpayable +``` + + + +*Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the caller. Emits an {ApprovalForAll} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| operator | address | undefined | +| approved | bool | undefined | + +### setApprovalForAllForAssets + +```solidity +function setApprovalForAllForAssets(address operator, bool approved) external nonpayable +``` + +Used to add or remove an operator of assets for the caller. + +*Operators can call {acceptAsset}, {rejectAsset}, {rejectAllAssets} or {setPriority} for any token owned by the caller.Requirements: - The `operator` cannot be the caller.Emits an {ApprovalForAllForAssets} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| operator | address | Address of the account to which the operator role is granted or revoked from | +| approved | bool | The boolean value indicating whether the operator role is being granted (`true`) or revoked (`false`) | + +### setPriority + +```solidity +function setPriority(uint256 tokenId, uint16[] priorities) external nonpayable +``` + +Sets a new priority array for a given token. + +*The priority array is a non-sequential list of `uint16`s, where the lowest value is considered highest priority.Value `0` of a priority is a special case equivalent to unitialized.Requirements: - The caller must own the token or be approved to manage the token's assets - `tokenId` must exist. - The length of `priorities` must be equal the length of the active assets array.Emits a {AssetPrioritySet} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token to set the priorities for | +| priorities | uint16[] | An array of priorities of active assets. The succesion of items in the priorities array matches that of the succesion of items in the active array | + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + +### symbol + +```solidity +function symbol() external view returns (string) +``` + +Used to retrieve the collection symbol. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | string | string Symbol of the collection | + +### transferFrom + +```solidity +function transferFrom(address from, address to, uint256 tokenId) external nonpayable +``` + + + +*Transfers `tokenId` token from `from` to `to`. WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721 or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must understand this adds an external call which potentially creates a reentrancy vulnerability. Requirements: - `from` cannot be the zero address. - `to` cannot be the zero address. - `tokenId` token must be owned by `from`. - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| from | address | undefined | +| to | address | undefined | +| tokenId | uint256 | undefined | + + + +## Events + +### Approval + +```solidity +event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| owner `indexed` | address | undefined | +| approved `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | + +### ApprovalForAll + +```solidity +event ApprovalForAll(address indexed owner, address indexed operator, bool approved) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| owner `indexed` | address | undefined | +| operator `indexed` | address | undefined | +| approved | bool | undefined | + +### ApprovalForAllForAssets + +```solidity +event ApprovalForAllForAssets(address indexed owner, address indexed operator, bool approved) +``` + +Used to notify listeners that owner has granted approval to the user to manage assets of all of their tokens. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| owner `indexed` | address | undefined | +| operator `indexed` | address | undefined | +| approved | bool | undefined | + +### ApprovalForAssets + +```solidity +event ApprovalForAssets(address indexed owner, address indexed approved, uint256 indexed tokenId) +``` + +Used to notify listeners that owner has granted an approval to the user to manage the assets of a given token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| owner `indexed` | address | undefined | +| approved `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | + +### AssetAccepted + +```solidity +event AssetAccepted(uint256 indexed tokenId, uint64 indexed assetId, uint64 indexed replacesId) +``` + +Used to notify listeners that an asset object at `assetId` is accepted by the token and migrated from token's pending assets array to active assets array of the token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId `indexed` | uint256 | undefined | +| assetId `indexed` | uint64 | undefined | +| replacesId `indexed` | uint64 | undefined | + +### AssetAddedToToken + +```solidity +event AssetAddedToToken(uint256 indexed tokenId, uint64 indexed assetId, uint64 indexed replacesId) +``` + +Used to notify listeners that an asset object at `assetId` is added to token's pending asset array. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId `indexed` | uint256 | undefined | +| assetId `indexed` | uint64 | undefined | +| replacesId `indexed` | uint64 | undefined | + +### AssetPrioritySet + +```solidity +event AssetPrioritySet(uint256 indexed tokenId) +``` + +Used to notify listeners that token's prioritiy array is reordered. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId `indexed` | uint256 | undefined | + +### AssetRejected + +```solidity +event AssetRejected(uint256 indexed tokenId, uint64 indexed assetId) +``` + +Used to notify listeners that an asset object at `assetId` is rejected from token and is dropped from the pending assets array of the token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId `indexed` | uint256 | undefined | +| assetId `indexed` | uint64 | undefined | + +### AssetSet + +```solidity +event AssetSet(uint64 indexed assetId) +``` + +Used to notify listeners that an asset object is initialized at `assetId`. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| assetId `indexed` | uint64 | undefined | + +### Emoted + +```solidity +event Emoted(address indexed emoter, uint256 indexed tokenId, bytes4 emoji, bool on) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| emoter `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | +| emoji | bytes4 | undefined | +| on | bool | undefined | + +### Transfer + +```solidity +event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| from `indexed` | address | undefined | +| to `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | + + + +## Errors + +### ERC721AddressZeroIsNotaValidOwner + +```solidity +error ERC721AddressZeroIsNotaValidOwner() +``` + +Attempting to grant the token to 0x0 address + + + + +### ERC721ApprovalToCurrentOwner + +```solidity +error ERC721ApprovalToCurrentOwner() +``` + +Attempting to grant approval to the current owner of the token + + + + +### ERC721ApproveCallerIsNotOwnerNorApprovedForAll + +```solidity +error ERC721ApproveCallerIsNotOwnerNorApprovedForAll() +``` + +Attempting to grant approval when not being owner or approved for all should not be permitted + + + + +### ERC721ApproveToCaller + +```solidity +error ERC721ApproveToCaller() +``` + +Attempting to grant approval to self + + + + +### ERC721InvalidTokenId + +```solidity +error ERC721InvalidTokenId() +``` + +Attempting to use an invalid token ID + + + + +### ERC721MintToTheZeroAddress + +```solidity +error ERC721MintToTheZeroAddress() +``` + +Attempting to mint to 0x0 address + + + + +### ERC721NotApprovedOrOwner + +```solidity +error ERC721NotApprovedOrOwner() +``` + +Attempting to manage a token without being its owner or approved by the owner + + + + +### ERC721TokenAlreadyMinted + +```solidity +error ERC721TokenAlreadyMinted() +``` + +Attempting to mint an already minted token + + + + +### ERC721TransferFromIncorrectOwner + +```solidity +error ERC721TransferFromIncorrectOwner() +``` + +Attempting to transfer the token from an address that is not the owner + + + + +### ERC721TransferToNonReceiverImplementer + +```solidity +error ERC721TransferToNonReceiverImplementer() +``` + +Attempting to safe transfer to an address that is unable to receive the token + + + + +### ERC721TransferToTheZeroAddress + +```solidity +error ERC721TransferToTheZeroAddress() +``` + +Attempting to transfer the token to a 0x0 address + + + + +### RMRKApprovalForAssetsToCurrentOwner + +```solidity +error RMRKApprovalForAssetsToCurrentOwner() +``` + +Attempting to grant approval of assets to their current owner + + + + +### RMRKApproveForAssetsCallerIsNotOwnerNorApprovedForAll + +```solidity +error RMRKApproveForAssetsCallerIsNotOwnerNorApprovedForAll() +``` + +Attempting to grant approval of assets without being the caller or approved for all + + + + +### RMRKBadPriorityListLength + +```solidity +error RMRKBadPriorityListLength() +``` + +Attempting to set the priorities with an array of length that doesn't match the length of active assets array + + + + +### RMRKIdZeroForbidden + +```solidity +error RMRKIdZeroForbidden() +``` + +Attempting to use ID 0, which is not supported + +*The ID 0 in RMRK suite is reserved for empty values. Guarding against its use ensures the expected operation* + + +### RMRKIndexOutOfRange + +```solidity +error RMRKIndexOutOfRange() +``` + +Attempting to interact with an asset, using index greater than number of assets + + + + +### RMRKNotApprovedForAssetsOrOwner + +```solidity +error RMRKNotApprovedForAssetsOrOwner() +``` + +Attempting to manage an asset without owning it or having been granted permission by the owner to do so + + + + +### RMRKTokenDoesNotHaveAsset + +```solidity +error RMRKTokenDoesNotHaveAsset() +``` + +Attempting to compose a NFT of a token without active assets + + + + +### RMRKUnexpectedAssetId + +```solidity +error RMRKUnexpectedAssetId() +``` + +Attempting to accept or reject an asset which does not match the one at the specified index + + + + +### RMRKUnexpectedNumberOfAssets + +```solidity +error RMRKUnexpectedNumberOfAssets() +``` + +Attempting to reject all pending assets but more assets than expected are pending + + + + + diff --git a/test/extensions/emotable.ts b/test/extensions/emotable.ts new file mode 100644 index 00000000..72cd5c08 --- /dev/null +++ b/test/extensions/emotable.ts @@ -0,0 +1,89 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { bn } from '../utils'; +import { IERC165, IRMRKMultiAsset, IRMRKEmotable, IOtherInterface } from '../interfaces'; +import { RMRKMultiAssetEmotableMock } from '../../typechain-types'; + +// --------------- FIXTURES ----------------------- + +async function multiAssetEmotableFixture() { + const factory = await ethers.getContractFactory('RMRKMultiAssetEmotableMock'); + const token = await factory.deploy('Chunky', 'CHNK'); + await token.deployed(); + + return token; +} + +describe('RMRKMultiAssetEmotableMock', async function () { + let token: RMRKMultiAssetEmotableMock; + let owner: SignerWithAddress; + let addrs: SignerWithAddress[]; + const tokenId = bn(1); + const emoji1 = Buffer.from('😎'); + const emoji2 = Buffer.from('😁'); + + beforeEach(async function () { + [owner, ...addrs] = await ethers.getSigners(); + token = await loadFixture(multiAssetEmotableFixture); + }); + + it('can support IERC165', async function () { + expect(await token.supportsInterface(IERC165)).to.equal(true); + }); + + it('can support IRMRKMultiAsset', async function () { + expect(await token.supportsInterface(IRMRKMultiAsset)).to.equal(true); + }); + + it('can support IRMRKEmotable', async function () { + expect(await token.supportsInterface(IRMRKEmotable)).to.equal(true); + }); + + it('does not support other interfaces', async function () { + expect(await token.supportsInterface(IOtherInterface)).to.equal(false); + }); + + describe('With minted tokens', async function () { + beforeEach(async function () { + await token.mint(owner.address, tokenId); + }); + + it('can emote and undo emote', async function () { + await expect(token.emote(tokenId, emoji1, true)) + .to.emit(token, 'Emoted') + .withArgs(owner.address, tokenId.toNumber(), emoji1, true); + expect(await token.getEmoteCount(tokenId, emoji1)).to.equal(bn(1)); + + await expect(token.emote(tokenId, emoji1, false)) + .to.emit(token, 'Emoted') + .withArgs(owner.address, tokenId.toNumber(), emoji1, false); + expect(await token.getEmoteCount(tokenId, emoji1)).to.equal(bn(0)); + }); + + it('can be emoted from different accounts', async function () { + await token.connect(addrs[0]).emote(tokenId, emoji1, true); + await token.connect(addrs[1]).emote(tokenId, emoji1, true); + await token.connect(addrs[2]).emote(tokenId, emoji2, true); + expect(await token.getEmoteCount(tokenId, emoji1)).to.equal(bn(2)); + expect(await token.getEmoteCount(tokenId, emoji2)).to.equal(bn(1)); + }); + + it('does nothing if new state is the same as old state', async function () { + await token.emote(tokenId, emoji1, true); + await token.emote(tokenId, emoji1, true); + expect(await token.getEmoteCount(tokenId, emoji1)).to.equal(bn(1)); + + await token.emote(tokenId, emoji2, false); + expect(await token.getEmoteCount(tokenId, emoji2)).to.equal(bn(0)); + }); + + it('cannot emote not existing token', async function () { + await expect(token.emote(2, emoji1, true)).to.be.revertedWithCustomError( + token, + 'ERC721InvalidTokenId', + ); + }); + }); +}); diff --git a/test/interfaces.ts b/test/interfaces.ts index 9ddc0285..6b73701f 100644 --- a/test/interfaces.ts +++ b/test/interfaces.ts @@ -3,6 +3,7 @@ const IERC721 = '0x80ac58cd'; const IERC721Metadata = '0x5b5e139f'; const IOtherInterface = '0xffffffff'; const IRMRKCatalog = '0xd912401f'; +const IRMRKEmotable = '0xf8d6854d'; const IRMRKEquippable = '0x28bc9ae4'; const IRMRKExternalEquip = '0x289dfee5'; const IRMRKMultiAsset = '0xd1526708'; @@ -20,6 +21,7 @@ export { IERC721Metadata, IOtherInterface, IRMRKCatalog, + IRMRKEmotable, IRMRKEquippable, IRMRKExternalEquip, IRMRKMultiAsset, From 33785d8589d13a7e671a29c6a44a9b5240f537b9 Mon Sep 17 00:00:00 2001 From: Steven Pineda Date: Wed, 4 Jan 2023 21:09:22 -0500 Subject: [PATCH 05/26] Apply suggestions from code review Co-authored-by: Jan Turk --- contracts/RMRK/extension/emotable/IRMRKEmotable.sol | 2 +- contracts/RMRK/extension/emotable/RMRKEmotable.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/RMRK/extension/emotable/IRMRKEmotable.sol b/contracts/RMRK/extension/emotable/IRMRKEmotable.sol index 09d66b11..05a59434 100644 --- a/contracts/RMRK/extension/emotable/IRMRKEmotable.sol +++ b/contracts/RMRK/extension/emotable/IRMRKEmotable.sol @@ -25,7 +25,7 @@ interface IRMRKEmotable is IERC165 { * @notice Used to emote or undo an emote on a token. * @param tokenId ID of the token being emoted * @param emoji Unicode identifier of the emoji - * @param state whether to turn emote or undo. True for emote, false for undo + * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote */ function emote(uint256 tokenId, bytes4 emoji, bool state) external; } diff --git a/contracts/RMRK/extension/emotable/RMRKEmotable.sol b/contracts/RMRK/extension/emotable/RMRKEmotable.sol index 2797d239..18d0f36c 100644 --- a/contracts/RMRK/extension/emotable/RMRKEmotable.sol +++ b/contracts/RMRK/extension/emotable/RMRKEmotable.sol @@ -36,7 +36,7 @@ abstract contract RMRKEmotable is IRMRKEmotable { * @notice Used to emote or undo an emote on a token. * @param tokenId ID of the token being emoted * @param emoji Unicode identifier of the emoji - * @param state whether to turn emote or undo. True for emote, false for undo + * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote */ function _emote( uint256 tokenId, @@ -60,7 +60,7 @@ abstract contract RMRKEmotable is IRMRKEmotable { * @notice Hook that is called before emote is added or removed. * @param tokenId ID of the token being emoted * @param emoji Unicode identifier of the emoji - * @param state whether to turn emote or undo. True for emote, false for undo + * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote */ function _beforeEmote( uint256 tokenId, From d2d6223f86382f9f3950c4e398b9266bb1d2e207 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Thu, 5 Jan 2023 08:30:35 +0100 Subject: [PATCH 06/26] Split the can emote and undo emote example --- test/extensions/emotable.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/extensions/emotable.ts b/test/extensions/emotable.ts index 72cd5c08..b47b092e 100644 --- a/test/extensions/emotable.ts +++ b/test/extensions/emotable.ts @@ -50,11 +50,15 @@ describe('RMRKMultiAssetEmotableMock', async function () { await token.mint(owner.address, tokenId); }); - it('can emote and undo emote', async function () { + it('can emote', async function () { await expect(token.emote(tokenId, emoji1, true)) .to.emit(token, 'Emoted') .withArgs(owner.address, tokenId.toNumber(), emoji1, true); expect(await token.getEmoteCount(tokenId, emoji1)).to.equal(bn(1)); + }); + + it('can undo emote', async function () { + await token.emote(tokenId, emoji1, true); await expect(token.emote(tokenId, emoji1, false)) .to.emit(token, 'Emoted') From 5d757390944d7e8378574febe4229713e065c930 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Thu, 5 Jan 2023 16:15:26 -0500 Subject: [PATCH 07/26] Adds dev comment on emote. --- contracts/RMRK/extension/emotable/IRMRKEmotable.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/RMRK/extension/emotable/IRMRKEmotable.sol b/contracts/RMRK/extension/emotable/IRMRKEmotable.sol index 05a59434..ca70d0be 100644 --- a/contracts/RMRK/extension/emotable/IRMRKEmotable.sol +++ b/contracts/RMRK/extension/emotable/IRMRKEmotable.sol @@ -23,6 +23,7 @@ interface IRMRKEmotable is IERC165 { /** * @notice Used to emote or undo an emote on a token. + * @dev Does nothing if attempting to set a pre-existent state * @param tokenId ID of the token being emoted * @param emoji Unicode identifier of the emoji * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote From ee48b2e58f6edbc164323256fc1046207dee934a Mon Sep 17 00:00:00 2001 From: steven2308 Date: Fri, 6 Jan 2023 15:53:37 -0500 Subject: [PATCH 08/26] Implements Standalone emote tracker, which can track emotes on any collection. --- .../extension/emotable/IRMRKEmoteTracker.sol | 40 +++++++ .../RMRK/extension/emotable/RMRKEmotable.sol | 2 +- .../extension/emotable/RMRKEmoteTracker.sol | 96 ++++++++++++++++ contracts/mocks/ERC721Mock.sol | 4 + .../emotable/RMRKEmoteTrackerMock.sol | 16 +++ docs/RMRK/extension/emotable/IRMRKEmotable.md | 4 +- .../extension/emotable/IRMRKEmoteTracker.md | 80 ++++++++++++++ docs/RMRK/extension/emotable/RMRKEmotable.md | 4 +- .../extension/emotable/RMRKEmoteTracker.md | 103 ++++++++++++++++++ docs/mocks/ERC721Mock.md | 17 +++ .../emotable/RMRKEmoteTrackerMock.md | 103 ++++++++++++++++++ test/extensions/emotable.ts | 96 +++++++++++++++- test/interfaces.ts | 2 + 13 files changed, 560 insertions(+), 7 deletions(-) create mode 100644 contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol create mode 100644 contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol create mode 100644 contracts/mocks/extensions/emotable/RMRKEmoteTrackerMock.sol create mode 100644 docs/RMRK/extension/emotable/IRMRKEmoteTracker.md create mode 100644 docs/RMRK/extension/emotable/RMRKEmoteTracker.md create mode 100644 docs/mocks/extensions/emotable/RMRKEmoteTrackerMock.md diff --git a/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol b/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol new file mode 100644 index 00000000..22d78261 --- /dev/null +++ b/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.16; + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/** + * @title IRMRKEmoteTracker + * @author RMRK team + * @notice Interface smart contract of the RMRK emote tracker module. + */ +interface IRMRKEmoteTracker is IERC165 { + /** + * @notice Used to get the number of emotes for a specific emoji on a token. + * @param collection Address of the collection with the token to check for emoji count + * @param tokenId ID of the token to check for emoji count + * @param emoji Unicode identifier of the emoji + * @return Number of emotes with the emoji on the token + */ + function getEmoteCount( + address collection, + uint256 tokenId, + bytes4 emoji + ) external view returns (uint256); + + /** + * @notice Used to emote or undo an emote on a token. + * @dev Does nothing if attempting to set a pre-existent state + * @param collection Address of the collection with the token being emoted + * @param tokenId ID of the token being emoted + * @param emoji Unicode identifier of the emoji + * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote + */ + function emote( + address collection, + uint256 tokenId, + bytes4 emoji, + bool state + ) external; +} diff --git a/contracts/RMRK/extension/emotable/RMRKEmotable.sol b/contracts/RMRK/extension/emotable/RMRKEmotable.sol index 18d0f36c..36274fda 100644 --- a/contracts/RMRK/extension/emotable/RMRKEmotable.sol +++ b/contracts/RMRK/extension/emotable/RMRKEmotable.sol @@ -12,7 +12,7 @@ import "./IRMRKEmotable.sol"; abstract contract RMRKEmotable is IRMRKEmotable { // Used to avoid double emoting and control undoing mapping(address => mapping(uint256 => mapping(bytes4 => uint256))) - private _emotesPerAddress; // Cheaper than using a bool + private _emotesPerAddress; // Cheaper than using a bool mapping(uint256 => mapping(bytes4 => uint256)) private _emotesPerToken; event Emoted( diff --git a/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol b/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol new file mode 100644 index 00000000..af1320ab --- /dev/null +++ b/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.16; + +import "./IRMRKEmoteTracker.sol"; +import "hardhat/console.sol"; + +/** + * @title RMRKEmotable + * @author RMRK team + * @notice Smart contract of the RMRK Emotable module. + */ +abstract contract RMRKEmoteTracker is IRMRKEmoteTracker { + // Used to avoid double emoting and control undoing + // emoter address => collection => tokenId => emoji => state (1 for emoted, 0 for not.) + mapping(address => mapping(address => mapping(uint256 => mapping(bytes4 => uint256)))) + private _emotesPerAddress; // Cheaper than using a bool + // collection => tokenId => emoji => count + mapping(address => mapping(uint256 => mapping(bytes4 => uint256))) + private _emotesPerToken; + + event Emoted( + address indexed emoter, + address indexed collection, + uint256 indexed tokenId, + bytes4 emoji, + bool on + ); + + /** + * @inheritdoc IRMRKEmoteTracker + */ + function getEmoteCount( + address collection, + uint256 tokenId, + bytes4 emoji + ) public view returns (uint256) { + return _emotesPerToken[collection][tokenId][emoji]; + } + + /** + * @notice Used to emote or undo an emote on a token. + * @param collection Address of the collection with the token being emoted + * @param tokenId ID of the token being emoted + * @param emoji Unicode identifier of the emoji + * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote + */ + function _emote( + address collection, + uint256 tokenId, + bytes4 emoji, + bool state + ) internal virtual { + bool currentVal = _emotesPerAddress[msg.sender][collection][tokenId][ + emoji + ] == 1; + if (currentVal != state) { + _beforeEmote(collection, tokenId, emoji, state); + if (state) { + _emotesPerToken[collection][tokenId][emoji] += 1; + } else { + _emotesPerToken[collection][tokenId][emoji] -= 1; + } + _emotesPerAddress[msg.sender][collection][tokenId][emoji] = state + ? 1 + : 0; + emit Emoted(msg.sender, collection, tokenId, emoji, state); + } + } + + /** + * @notice Hook that is called before emote is added or removed. + * @param collection Address of the collection with the token being emoted + * @param tokenId ID of the token being emoted + * @param emoji Unicode identifier of the emoji + * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote + */ + function _beforeEmote( + address collection, + uint256 tokenId, + bytes4 emoji, + bool state + ) internal virtual {} + + /** + * @inheritdoc IERC165 + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual returns (bool) { + console.logBytes4(type(IRMRKEmoteTracker).interfaceId); + return + interfaceId == type(IRMRKEmoteTracker).interfaceId || + interfaceId == type(IERC165).interfaceId; + } +} diff --git a/contracts/mocks/ERC721Mock.sol b/contracts/mocks/ERC721Mock.sol index e6346a69..4e67f7bd 100644 --- a/contracts/mocks/ERC721Mock.sol +++ b/contracts/mocks/ERC721Mock.sol @@ -13,4 +13,8 @@ contract ERC721Mock is ERC721 { string memory name, string memory symbol ) ERC721(name, symbol) {} + + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } } diff --git a/contracts/mocks/extensions/emotable/RMRKEmoteTrackerMock.sol b/contracts/mocks/extensions/emotable/RMRKEmoteTrackerMock.sol new file mode 100644 index 00000000..bf5cbf8a --- /dev/null +++ b/contracts/mocks/extensions/emotable/RMRKEmoteTrackerMock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.16; + +import "../../../RMRK/extension/emotable/RMRKEmoteTracker.sol"; + +contract RMRKEmoteTrackerMock is RMRKEmoteTracker { + function emote( + address collection, + uint256 tokenId, + bytes4 emoji, + bool on + ) public { + _emote(collection, tokenId, emoji, on); + } +} diff --git a/docs/RMRK/extension/emotable/IRMRKEmotable.md b/docs/RMRK/extension/emotable/IRMRKEmotable.md index 46947bf3..892b8c65 100644 --- a/docs/RMRK/extension/emotable/IRMRKEmotable.md +++ b/docs/RMRK/extension/emotable/IRMRKEmotable.md @@ -18,7 +18,7 @@ function emote(uint256 tokenId, bytes4 emoji, bool state) external nonpayable Used to emote or undo an emote on a token. - +*Does nothing if attempting to set a pre-existent state* #### Parameters @@ -26,7 +26,7 @@ Used to emote or undo an emote on a token. |---|---|---| | tokenId | uint256 | ID of the token being emoted | | emoji | bytes4 | Unicode identifier of the emoji | -| state | bool | whether to turn emote or undo. True for emote, false for undo | +| state | bool | Boolean value signifying whether to emote (`true`) or undo (`false`) emote | ### getEmoteCount diff --git a/docs/RMRK/extension/emotable/IRMRKEmoteTracker.md b/docs/RMRK/extension/emotable/IRMRKEmoteTracker.md new file mode 100644 index 00000000..25d5293c --- /dev/null +++ b/docs/RMRK/extension/emotable/IRMRKEmoteTracker.md @@ -0,0 +1,80 @@ +# IRMRKEmoteTracker + +*RMRK team* + +> IRMRKEmoteTracker + +Interface smart contract of the RMRK emote tracker module. + + + +## Methods + +### emote + +```solidity +function emote(address collection, uint256 tokenId, bytes4 emoji, bool state) external nonpayable +``` + +Used to emote or undo an emote on a token. + +*Does nothing if attempting to set a pre-existent state* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| collection | address | Address of the collection with the token being emoted | +| tokenId | uint256 | ID of the token being emoted | +| emoji | bytes4 | Unicode identifier of the emoji | +| state | bool | Boolean value signifying whether to emote (`true`) or undo (`false`) emote | + +### getEmoteCount + +```solidity +function getEmoteCount(address collection, uint256 tokenId, bytes4 emoji) external view returns (uint256) +``` + +Used to get the number of emotes for a specific emoji on a token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| collection | address | Address of the collection with the token to check for emoji count | +| tokenId | uint256 | ID of the token to check for emoji count | +| emoji | bytes4 | Unicode identifier of the emoji | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | Number of emotes with the emoji on the token | + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + +*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + + + + diff --git a/docs/RMRK/extension/emotable/RMRKEmotable.md b/docs/RMRK/extension/emotable/RMRKEmotable.md index 7bd55005..3ca3db66 100644 --- a/docs/RMRK/extension/emotable/RMRKEmotable.md +++ b/docs/RMRK/extension/emotable/RMRKEmotable.md @@ -18,7 +18,7 @@ function emote(uint256 tokenId, bytes4 emoji, bool state) external nonpayable Used to emote or undo an emote on a token. - +*Does nothing if attempting to set a pre-existent state* #### Parameters @@ -26,7 +26,7 @@ Used to emote or undo an emote on a token. |---|---|---| | tokenId | uint256 | ID of the token being emoted | | emoji | bytes4 | Unicode identifier of the emoji | -| state | bool | whether to turn emote or undo. True for emote, false for undo | +| state | bool | Boolean value signifying whether to emote (`true`) or undo (`false`) emote | ### getEmoteCount diff --git a/docs/RMRK/extension/emotable/RMRKEmoteTracker.md b/docs/RMRK/extension/emotable/RMRKEmoteTracker.md new file mode 100644 index 00000000..58d9d302 --- /dev/null +++ b/docs/RMRK/extension/emotable/RMRKEmoteTracker.md @@ -0,0 +1,103 @@ +# RMRKEmoteTracker + +*RMRK team* + +> RMRKEmotable + +Smart contract of the RMRK Emotable module. + + + +## Methods + +### emote + +```solidity +function emote(address collection, uint256 tokenId, bytes4 emoji, bool state) external nonpayable +``` + +Used to emote or undo an emote on a token. + +*Does nothing if attempting to set a pre-existent state* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| collection | address | Address of the collection with the token being emoted | +| tokenId | uint256 | ID of the token being emoted | +| emoji | bytes4 | Unicode identifier of the emoji | +| state | bool | Boolean value signifying whether to emote (`true`) or undo (`false`) emote | + +### getEmoteCount + +```solidity +function getEmoteCount(address collection, uint256 tokenId, bytes4 emoji) external view returns (uint256) +``` + +Used to get the number of emotes for a specific emoji on a token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| collection | address | Address of the collection with the token to check for emoji count | +| tokenId | uint256 | ID of the token to check for emoji count | +| emoji | bytes4 | Unicode identifier of the emoji | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | Number of emotes with the emoji on the token | + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + +*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + + + +## Events + +### Emoted + +```solidity +event Emoted(address indexed emoter, address indexed collection, uint256 indexed tokenId, bytes4 emoji, bool on) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| emoter `indexed` | address | undefined | +| collection `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | +| emoji | bytes4 | undefined | +| on | bool | undefined | + + + diff --git a/docs/mocks/ERC721Mock.md b/docs/mocks/ERC721Mock.md index 1427e56b..425cdb08 100644 --- a/docs/mocks/ERC721Mock.md +++ b/docs/mocks/ERC721Mock.md @@ -94,6 +94,23 @@ function isApprovedForAll(address owner, address operator) external view returns |---|---|---| | _0 | bool | undefined | +### mint + +```solidity +function mint(address to, uint256 tokenId) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| to | address | undefined | +| tokenId | uint256 | undefined | + ### name ```solidity diff --git a/docs/mocks/extensions/emotable/RMRKEmoteTrackerMock.md b/docs/mocks/extensions/emotable/RMRKEmoteTrackerMock.md new file mode 100644 index 00000000..e8ce9db4 --- /dev/null +++ b/docs/mocks/extensions/emotable/RMRKEmoteTrackerMock.md @@ -0,0 +1,103 @@ +# RMRKEmoteTrackerMock + + + + + + + + + +## Methods + +### emote + +```solidity +function emote(address collection, uint256 tokenId, bytes4 emoji, bool on) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| collection | address | undefined | +| tokenId | uint256 | undefined | +| emoji | bytes4 | undefined | +| on | bool | undefined | + +### getEmoteCount + +```solidity +function getEmoteCount(address collection, uint256 tokenId, bytes4 emoji) external view returns (uint256) +``` + +Used to get the number of emotes for a specific emoji on a token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| collection | address | Address of the collection with the token to check for emoji count | +| tokenId | uint256 | ID of the token to check for emoji count | +| emoji | bytes4 | Unicode identifier of the emoji | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | Number of emotes with the emoji on the token | + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + +*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + + + +## Events + +### Emoted + +```solidity +event Emoted(address indexed emoter, address indexed collection, uint256 indexed tokenId, bytes4 emoji, bool on) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| emoter `indexed` | address | undefined | +| collection `indexed` | address | undefined | +| tokenId `indexed` | uint256 | undefined | +| emoji | bytes4 | undefined | +| on | bool | undefined | + + + diff --git a/test/extensions/emotable.ts b/test/extensions/emotable.ts index b47b092e..7e830878 100644 --- a/test/extensions/emotable.ts +++ b/test/extensions/emotable.ts @@ -3,8 +3,18 @@ import { expect } from 'chai'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { bn } from '../utils'; -import { IERC165, IRMRKMultiAsset, IRMRKEmotable, IOtherInterface } from '../interfaces'; -import { RMRKMultiAssetEmotableMock } from '../../typechain-types'; +import { + IERC165, + IRMRKMultiAsset, + IRMRKEmotable, + IRMRKEmoteTracker, + IOtherInterface, +} from '../interfaces'; +import { + RMRKMultiAssetEmotableMock, + ERC721Mock, + RMRKEmoteTrackerMock, +} from '../../typechain-types'; // --------------- FIXTURES ----------------------- @@ -16,6 +26,19 @@ async function multiAssetEmotableFixture() { return token; } +async function emoteTrackerFixture() { + const factory = await ethers.getContractFactory('RMRKEmoteTrackerMock'); + const erc721Factory = await ethers.getContractFactory('ERC721Mock'); + const emoteTracker = await factory.deploy(); + const tokenA = await erc721Factory.deploy('Token A', 'TKA'); + const tokenB = await erc721Factory.deploy('Token B', 'TKB'); + await emoteTracker.deployed(); + await tokenA.deployed(); + await tokenB.deployed(); + + return { emoteTracker, tokenA, tokenB }; +} + describe('RMRKMultiAssetEmotableMock', async function () { let token: RMRKMultiAssetEmotableMock; let owner: SignerWithAddress; @@ -91,3 +114,72 @@ describe('RMRKMultiAssetEmotableMock', async function () { }); }); }); + +describe('RMRKEmoteTrackerMock', async function () { + let emoteTracker: RMRKEmoteTrackerMock; + let tokenA: ERC721Mock; + let tokenB: ERC721Mock; + let owner: SignerWithAddress; + let addrs: SignerWithAddress[]; + const tokenId = bn(1); + const emoji1 = Buffer.from('😎'); + const emoji2 = Buffer.from('😁'); + + beforeEach(async function () { + [owner, ...addrs] = await ethers.getSigners(); + ({ emoteTracker, tokenA, tokenB } = await loadFixture(emoteTrackerFixture)); + }); + + it('can support IERC165', async function () { + expect(await emoteTracker.supportsInterface(IERC165)).to.equal(true); + }); + it('can support IRMRKEmoteTracker', async function () { + expect(await emoteTracker.supportsInterface(IRMRKEmoteTracker)).to.equal(true); + }); + + it('does not support other interfaces', async function () { + expect(await emoteTracker.supportsInterface(IOtherInterface)).to.equal(false); + }); + + describe('With minted tokens', async function () { + beforeEach(async function () { + await tokenA.mint(owner.address, 1); + await tokenA.mint(owner.address, 2); + await tokenB.mint(owner.address, 1); + await tokenB.mint(owner.address, 2); + }); + + it('can emote', async function () { + await expect(emoteTracker.emote(tokenA.address, tokenId, emoji1, true)) + .to.emit(emoteTracker, 'Emoted') + .withArgs(owner.address, tokenA.address, tokenId.toNumber(), emoji1, true); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji1)).to.equal(bn(1)); + }); + + it('can undo emote', async function () { + await emoteTracker.emote(tokenA.address, tokenId, emoji1, true); + + await expect(emoteTracker.emote(tokenA.address, tokenId, emoji1, false)) + .to.emit(emoteTracker, 'Emoted') + .withArgs(owner.address, tokenA.address, tokenId.toNumber(), emoji1, false); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji1)).to.equal(bn(0)); + }); + + it('can be emoted from different accounts', async function () { + await emoteTracker.connect(addrs[0]).emote(tokenA.address, tokenId, emoji1, true); + await emoteTracker.connect(addrs[1]).emote(tokenA.address, tokenId, emoji1, true); + await emoteTracker.connect(addrs[2]).emote(tokenA.address, tokenId, emoji2, true); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji1)).to.equal(bn(2)); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji2)).to.equal(bn(1)); + }); + + it('does nothing if new state is the same as old state', async function () { + await emoteTracker.emote(tokenA.address, tokenId, emoji1, true); + await emoteTracker.emote(tokenA.address, tokenId, emoji1, true); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji1)).to.equal(bn(1)); + + await emoteTracker.emote(tokenA.address, tokenId, emoji2, false); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji2)).to.equal(bn(0)); + }); + }); +}); diff --git a/test/interfaces.ts b/test/interfaces.ts index 6b73701f..0adb7f4a 100644 --- a/test/interfaces.ts +++ b/test/interfaces.ts @@ -4,6 +4,7 @@ const IERC721Metadata = '0x5b5e139f'; const IOtherInterface = '0xffffffff'; const IRMRKCatalog = '0xd912401f'; const IRMRKEmotable = '0xf8d6854d'; +const IRMRKEmoteTracker = '0x64148b98'; const IRMRKEquippable = '0x28bc9ae4'; const IRMRKExternalEquip = '0x289dfee5'; const IRMRKMultiAsset = '0xd1526708'; @@ -22,6 +23,7 @@ export { IOtherInterface, IRMRKCatalog, IRMRKEmotable, + IRMRKEmoteTracker, IRMRKEquippable, IRMRKExternalEquip, IRMRKMultiAsset, From 1d1498a0f937f908417d80e9de735a0adc0afedb Mon Sep 17 00:00:00 2001 From: steven2308 Date: Fri, 6 Jan 2023 16:03:28 -0500 Subject: [PATCH 09/26] Adds missing test case for emote tracker. --- test/extensions/emotable.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/extensions/emotable.ts b/test/extensions/emotable.ts index 7e830878..a8525d24 100644 --- a/test/extensions/emotable.ts +++ b/test/extensions/emotable.ts @@ -173,6 +173,14 @@ describe('RMRKEmoteTrackerMock', async function () { expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji2)).to.equal(bn(1)); }); + it('can emote different collections', async function () { + await emoteTracker.connect(addrs[0]).emote(tokenA.address, tokenId, emoji1, true); + await emoteTracker.connect(addrs[1]).emote(tokenB.address, tokenId, emoji1, true); + await emoteTracker.connect(addrs[2]).emote(tokenB.address, tokenId, emoji1, true); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji1)).to.equal(bn(1)); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji2)).to.equal(bn(2)); + }); + it('does nothing if new state is the same as old state', async function () { await emoteTracker.emote(tokenA.address, tokenId, emoji1, true); await emoteTracker.emote(tokenA.address, tokenId, emoji1, true); From 471a131409085bff234950373f5fdf66bd50d956 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Mon, 9 Jan 2023 06:44:19 -0500 Subject: [PATCH 10/26] Removes console log on contract and fixes testcase. --- contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol | 2 -- test/extensions/emotable.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol b/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol index af1320ab..6112d9be 100644 --- a/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol +++ b/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.16; import "./IRMRKEmoteTracker.sol"; -import "hardhat/console.sol"; /** * @title RMRKEmotable @@ -88,7 +87,6 @@ abstract contract RMRKEmoteTracker is IRMRKEmoteTracker { function supportsInterface( bytes4 interfaceId ) public view virtual returns (bool) { - console.logBytes4(type(IRMRKEmoteTracker).interfaceId); return interfaceId == type(IRMRKEmoteTracker).interfaceId || interfaceId == type(IERC165).interfaceId; diff --git a/test/extensions/emotable.ts b/test/extensions/emotable.ts index a8525d24..ebee7aad 100644 --- a/test/extensions/emotable.ts +++ b/test/extensions/emotable.ts @@ -178,7 +178,7 @@ describe('RMRKEmoteTrackerMock', async function () { await emoteTracker.connect(addrs[1]).emote(tokenB.address, tokenId, emoji1, true); await emoteTracker.connect(addrs[2]).emote(tokenB.address, tokenId, emoji1, true); expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji1)).to.equal(bn(1)); - expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji2)).to.equal(bn(2)); + expect(await emoteTracker.getEmoteCount(tokenB.address, tokenId, emoji1)).to.equal(bn(2)); }); it('does nothing if new state is the same as old state', async function () { From 8fa729640853562517be508d87dc06cfadbc2259 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Mon, 9 Jan 2023 09:52:41 -0500 Subject: [PATCH 11/26] Adds new test for emotables. --- test/extensions/emotable.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/extensions/emotable.ts b/test/extensions/emotable.ts index ebee7aad..73c9a603 100644 --- a/test/extensions/emotable.ts +++ b/test/extensions/emotable.ts @@ -97,6 +97,13 @@ describe('RMRKMultiAssetEmotableMock', async function () { expect(await token.getEmoteCount(tokenId, emoji2)).to.equal(bn(1)); }); + it('can add multiple emojis to same NFT', async function () { + await token.emote(tokenId, emoji1, true); + await token.emote(tokenId, emoji2, true); + expect(await token.getEmoteCount(tokenId, emoji1)).to.equal(bn(1)); + expect(await token.getEmoteCount(tokenId, emoji2)).to.equal(bn(1)); + }); + it('does nothing if new state is the same as old state', async function () { await token.emote(tokenId, emoji1, true); await token.emote(tokenId, emoji1, true); @@ -173,6 +180,13 @@ describe('RMRKEmoteTrackerMock', async function () { expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji2)).to.equal(bn(1)); }); + it('can add multiple emojis to same NFT', async function () { + await emoteTracker.emote(tokenA.address, tokenId, emoji1, true); + await emoteTracker.emote(tokenA.address, tokenId, emoji2, true); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji1)).to.equal(bn(1)); + expect(await emoteTracker.getEmoteCount(tokenA.address, tokenId, emoji2)).to.equal(bn(1)); + }); + it('can emote different collections', async function () { await emoteTracker.connect(addrs[0]).emote(tokenA.address, tokenId, emoji1, true); await emoteTracker.connect(addrs[1]).emote(tokenB.address, tokenId, emoji1, true); From 5e966f2196c29d417c72b309255d271dc4cf71f1 Mon Sep 17 00:00:00 2001 From: Steven Pineda Date: Mon, 9 Jan 2023 09:54:04 -0500 Subject: [PATCH 12/26] Apply suggestions from code review Co-authored-by: Jan Turk --- contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol | 2 +- contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol b/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol index 22d78261..3ae816ce 100644 --- a/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol +++ b/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol @@ -12,7 +12,7 @@ import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; interface IRMRKEmoteTracker is IERC165 { /** * @notice Used to get the number of emotes for a specific emoji on a token. - * @param collection Address of the collection with the token to check for emoji count + * @param collection Address of the collection containing the token being checked for emoji count * @param tokenId ID of the token to check for emoji count * @param emoji Unicode identifier of the emoji * @return Number of emotes with the emoji on the token diff --git a/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol b/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol index 6112d9be..ba35f180 100644 --- a/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol +++ b/contracts/RMRK/extension/emotable/RMRKEmoteTracker.sol @@ -11,7 +11,7 @@ import "./IRMRKEmoteTracker.sol"; */ abstract contract RMRKEmoteTracker is IRMRKEmoteTracker { // Used to avoid double emoting and control undoing - // emoter address => collection => tokenId => emoji => state (1 for emoted, 0 for not.) + // emoter address => collection => tokenId => emoji => state (1 for emoted, 0 for not) mapping(address => mapping(address => mapping(uint256 => mapping(bytes4 => uint256)))) private _emotesPerAddress; // Cheaper than using a bool // collection => tokenId => emoji => count @@ -39,7 +39,7 @@ abstract contract RMRKEmoteTracker is IRMRKEmoteTracker { /** * @notice Used to emote or undo an emote on a token. - * @param collection Address of the collection with the token being emoted + * @param collection Address of the collection containing the token being emoted * @param tokenId ID of the token being emoted * @param emoji Unicode identifier of the emoji * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote @@ -69,7 +69,7 @@ abstract contract RMRKEmoteTracker is IRMRKEmoteTracker { /** * @notice Hook that is called before emote is added or removed. - * @param collection Address of the collection with the token being emoted + * @param collection Address of the collection containing the token being emoted * @param tokenId ID of the token being emoted * @param emoji Unicode identifier of the emoji * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote From f90234b03b6f7397505fa99e2500f608ee6010cf Mon Sep 17 00:00:00 2001 From: steven2308 Date: Tue, 10 Jan 2023 07:48:08 -0500 Subject: [PATCH 13/26] Updates docs for emotable. --- contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol | 2 +- docs/RMRK/extension/emotable/IRMRKEmoteTracker.md | 4 ++-- docs/RMRK/extension/emotable/RMRKEmoteTracker.md | 4 ++-- docs/mocks/extensions/emotable/RMRKEmoteTrackerMock.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol b/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol index 3ae816ce..9c9bb06a 100644 --- a/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol +++ b/contracts/RMRK/extension/emotable/IRMRKEmoteTracker.sol @@ -26,7 +26,7 @@ interface IRMRKEmoteTracker is IERC165 { /** * @notice Used to emote or undo an emote on a token. * @dev Does nothing if attempting to set a pre-existent state - * @param collection Address of the collection with the token being emoted + * @param collection Address of the collection containing the token being checked for emoji count * @param tokenId ID of the token being emoted * @param emoji Unicode identifier of the emoji * @param state Boolean value signifying whether to emote (`true`) or undo (`false`) emote diff --git a/docs/RMRK/extension/emotable/IRMRKEmoteTracker.md b/docs/RMRK/extension/emotable/IRMRKEmoteTracker.md index 25d5293c..b98056f4 100644 --- a/docs/RMRK/extension/emotable/IRMRKEmoteTracker.md +++ b/docs/RMRK/extension/emotable/IRMRKEmoteTracker.md @@ -24,7 +24,7 @@ Used to emote or undo an emote on a token. | Name | Type | Description | |---|---|---| -| collection | address | Address of the collection with the token being emoted | +| collection | address | Address of the collection containing the token being checked for emoji count | | tokenId | uint256 | ID of the token being emoted | | emoji | bytes4 | Unicode identifier of the emoji | | state | bool | Boolean value signifying whether to emote (`true`) or undo (`false`) emote | @@ -43,7 +43,7 @@ Used to get the number of emotes for a specific emoji on a token. | Name | Type | Description | |---|---|---| -| collection | address | Address of the collection with the token to check for emoji count | +| collection | address | Address of the collection containing the token being checked for emoji count | | tokenId | uint256 | ID of the token to check for emoji count | | emoji | bytes4 | Unicode identifier of the emoji | diff --git a/docs/RMRK/extension/emotable/RMRKEmoteTracker.md b/docs/RMRK/extension/emotable/RMRKEmoteTracker.md index 58d9d302..3f7e2cb5 100644 --- a/docs/RMRK/extension/emotable/RMRKEmoteTracker.md +++ b/docs/RMRK/extension/emotable/RMRKEmoteTracker.md @@ -24,7 +24,7 @@ Used to emote or undo an emote on a token. | Name | Type | Description | |---|---|---| -| collection | address | Address of the collection with the token being emoted | +| collection | address | Address of the collection containing the token being checked for emoji count | | tokenId | uint256 | ID of the token being emoted | | emoji | bytes4 | Unicode identifier of the emoji | | state | bool | Boolean value signifying whether to emote (`true`) or undo (`false`) emote | @@ -43,7 +43,7 @@ Used to get the number of emotes for a specific emoji on a token. | Name | Type | Description | |---|---|---| -| collection | address | Address of the collection with the token to check for emoji count | +| collection | address | Address of the collection containing the token being checked for emoji count | | tokenId | uint256 | ID of the token to check for emoji count | | emoji | bytes4 | Unicode identifier of the emoji | diff --git a/docs/mocks/extensions/emotable/RMRKEmoteTrackerMock.md b/docs/mocks/extensions/emotable/RMRKEmoteTrackerMock.md index e8ce9db4..8e359f71 100644 --- a/docs/mocks/extensions/emotable/RMRKEmoteTrackerMock.md +++ b/docs/mocks/extensions/emotable/RMRKEmoteTrackerMock.md @@ -43,7 +43,7 @@ Used to get the number of emotes for a specific emoji on a token. | Name | Type | Description | |---|---|---| -| collection | address | Address of the collection with the token to check for emoji count | +| collection | address | Address of the collection containing the token being checked for emoji count | | tokenId | uint256 | ID of the token to check for emoji count | | emoji | bytes4 | Unicode identifier of the emoji | From b3875de945b8f817326b71976b0808cbbc380c7d Mon Sep 17 00:00:00 2001 From: steven2308 Date: Tue, 10 Jan 2023 08:18:13 -0500 Subject: [PATCH 14/26] Bumps version to 0.23.0. --- contracts/RMRK/core/RMRKCore.sol | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/RMRK/core/RMRKCore.sol b/contracts/RMRK/core/RMRKCore.sol index 7a0ce7cd..caa2fa31 100644 --- a/contracts/RMRK/core/RMRKCore.sol +++ b/contracts/RMRK/core/RMRKCore.sol @@ -12,7 +12,7 @@ import "./IRMRKCore.sol"; */ contract RMRKCore is IRMRKCore { /// @notice Version of the @rmrk-team/evm-contracts package - string public constant VERSION = "0.22.1"; + string public constant VERSION = "0.23.0"; /** * @notice Used to initialize the smart contract. diff --git a/package.json b/package.json index 386e7ee0..29ef31cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmrk-team/evm-contracts", - "version": "0.22.1", + "version": "0.23.0", "license": "Apache-2.0", "files": [ "contracts/RMRK/*", From 02c1671bdf1309f467034692e9c0dc9886f7d756 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Wed, 11 Jan 2023 05:36:52 -0500 Subject: [PATCH 15/26] getEquipped now returns childrenAssetMetadata array. --- contracts/RMRK/utils/RMRKEquipRenderUtils.sol | 38 ++++++++++++------- docs/RMRK/utils/RMRKEquipRenderUtils.md | 3 +- scripts/deploy_renderUtils.ts | 32 ++++++++++++++++ test/behavior/equippableParts.ts | 3 +- test/behavior/equippableSlots.ts | 6 +++ 5 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 scripts/deploy_renderUtils.ts diff --git a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol index c2ac6731..3c177023 100644 --- a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol @@ -227,6 +227,7 @@ contract RMRKEquipRenderUtils { * @param assetId ID of the asset being queried for equipped parts * @return slotPartIds An array of the IDs of the slot parts present in the given asset * @return childrenEquipped An array of `Equipment` structs containing info about the equipped children + * @return childrenAssetMetadata An array of strings corresponding to asset metadata of the equipped children */ function getEquipped( address target, @@ -237,7 +238,8 @@ contract RMRKEquipRenderUtils { view returns ( uint64[] memory slotPartIds, - IRMRKEquippable.Equipment[] memory childrenEquipped + IRMRKEquippable.Equipment[] memory childrenEquipped, + string[] memory childrenAssetMetadata ) { IRMRKEquippable target_ = IRMRKEquippable(target); @@ -246,20 +248,27 @@ contract RMRKEquipRenderUtils { .getAssetAndEquippableData(tokenId, assetId); (slotPartIds, ) = splitSlotAndFixedParts(partIds, catalogAddress); - childrenEquipped = new IRMRKEquippable.Equipment[](slotPartIds.length); - uint256 len = slotPartIds.length; - for (uint256 i; i < len; ) { - IRMRKEquippable.Equipment memory equipment = target_.getEquipment( - tokenId, - catalogAddress, - slotPartIds[i] - ); - if (equipment.assetId == assetId) { - childrenEquipped[i] = equipment; - } - unchecked { - ++i; + + if (len != 0) { + childrenEquipped = new IRMRKEquippable.Equipment[](len); + childrenAssetMetadata = new string[](len); + + for (uint256 i; i < len; ) { + IRMRKEquippable.Equipment memory equipment = target_ + .getEquipment(tokenId, catalogAddress, slotPartIds[i]); + if (equipment.assetId == assetId) { + childrenEquipped[i] = equipment; + childrenAssetMetadata[i] = IRMRKEquippable( + equipment.childEquippableAddress + ).getAssetMetadata( + equipment.childId, + equipment.childAssetId + ); + } + unchecked { + ++i; + } } } } @@ -377,6 +386,7 @@ contract RMRKEquipRenderUtils { slotParts = new EquippedSlotPart[](slotPartIds.length); uint256 len = slotPartIds.length; + // TODO: is this check really needed? if (len != 0) { string memory metadata; IRMRKCatalog.Part[] memory catalogSlotParts = IRMRKCatalog( diff --git a/docs/RMRK/utils/RMRKEquipRenderUtils.md b/docs/RMRK/utils/RMRKEquipRenderUtils.md index 2f951d9d..f9d60371 100644 --- a/docs/RMRK/utils/RMRKEquipRenderUtils.md +++ b/docs/RMRK/utils/RMRKEquipRenderUtils.md @@ -41,7 +41,7 @@ Used to compose the given equippables. ### getEquipped ```solidity -function getEquipped(address target, uint64 tokenId, uint64 assetId) external view returns (uint64[] slotPartIds, struct IRMRKEquippable.Equipment[] childrenEquipped) +function getEquipped(address target, uint64 tokenId, uint64 assetId) external view returns (uint64[] slotPartIds, struct IRMRKEquippable.Equipment[] childrenEquipped, string[] childrenAssetMetadata) ``` Used to retrieve the equipped parts of the given token. @@ -62,6 +62,7 @@ Used to retrieve the equipped parts of the given token. |---|---|---| | slotPartIds | uint64[] | An array of the IDs of the slot parts present in the given asset | | childrenEquipped | IRMRKEquippable.Equipment[] | An array of `Equipment` structs containing info about the equipped children | +| childrenAssetMetadata | string[] | An array of strings corresponding to asset metadata of the equipped children | ### getExtendedActiveAssets diff --git a/scripts/deploy_renderUtils.ts b/scripts/deploy_renderUtils.ts new file mode 100644 index 00000000..3d98ffaf --- /dev/null +++ b/scripts/deploy_renderUtils.ts @@ -0,0 +1,32 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { ethers, run } from 'hardhat'; + +export const sleep = (ms: number): Promise => { + return new Promise((resolve) => { + setTimeout(() => resolve(), ms); + }); +}; + +async function main() { + const accounts: SignerWithAddress[] = await ethers.getSigners(); + const owner = accounts[0]; + console.log('Deployer address: ' + owner.address); + // We get the contract to deploy + const renderUtilsFactory = await ethers.getContractFactory('RMRKEquipRenderUtils'); + const renderUtils = await renderUtilsFactory.deploy(); + await renderUtils.deployed(); + console.log('RMRK Equip Render Utils deployed to:', renderUtils.address); + await sleep(1000); + + await run('verify:verify', { + address: renderUtils.address, + constructorArguments: [], + }); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/test/behavior/equippableParts.ts b/test/behavior/equippableParts.ts index e747e600..1da57ab8 100644 --- a/test/behavior/equippableParts.ts +++ b/test/behavior/equippableParts.ts @@ -67,9 +67,10 @@ async function shouldBehaveLikeEquippableWithParts() { const expectedEquips = [ [bn(neonResIds[0]), bn(weaponResId), bn(masks[0]), maskEquipContract.address], ]; + const expectedMetadata = ['ipfs:weapon/equip/5']; expect( await viewContract.getEquipped(neonEquipContract.address, neons[0], neonResIds[0]), - ).to.eql([expectedSlots, expectedEquips]); + ).to.eql([expectedSlots, expectedEquips, expectedMetadata]); // Child is marked as equipped: expect( diff --git a/test/behavior/equippableSlots.ts b/test/behavior/equippableSlots.ts index dac9e126..2fad6395 100644 --- a/test/behavior/equippableSlots.ts +++ b/test/behavior/equippableSlots.ts @@ -199,9 +199,11 @@ async function shouldBehaveLikeEquippableWithSlots( [bn(soldierResId), bn(weaponResId), bn(weaponsIds[0]), weaponEquip.address], [bn(soldierResId), bn(backgroundAssetId), bn(backgroundsIds[0]), backgroundEquip.address], ]; + const expectedMetadata = ['ipfs:weapon/equip/5', 'ipfs:background/']; expect(await view.getEquipped(soldierEquip.address, soldiersIds[0], soldierResId)).to.eql([ expectedSlots, expectedEquips, + expectedMetadata, ]); // Children are marked as equipped: @@ -585,9 +587,11 @@ async function shouldBehaveLikeEquippableWithSlots( [bn(soldierResId), bn(weaponResId), bn(weaponsIds[0]), weaponEquip.address], [bn(0), bn(0), bn(0), ethers.constants.AddressZero], ]; + const expectedMetadata = ['ipfs:weapon/equip/5', '']; expect(await view.getEquipped(soldierEquip.address, soldiersIds[0], soldierResId)).to.eql([ expectedSlots, expectedEquips, + expectedMetadata, ]); // Child is marked as equipped: @@ -614,9 +618,11 @@ async function shouldBehaveLikeEquippableWithSlots( [bn(0), bn(0), bn(0), ethers.constants.AddressZero], [bn(0), bn(0), bn(0), ethers.constants.AddressZero], ]; + const expectedMetadata = ['', '']; expect(await view.getEquipped(soldierEquip.address, soldiersIds[0], soldierResId)).to.eql([ expectedSlots, expectedEquips, + expectedMetadata, ]); // Child is marked as not equipped: From c09dfed793b3f573bcda2a4dc3418a87c1ede8e4 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Fri, 13 Jan 2023 15:55:49 -0500 Subject: [PATCH 16/26] Implements more utilities into Equip render utils. RMRKEquipRenderUtils now inherits RMRKEquipRenderUtils, so it has all utils in a single place. Adds getTopAssetAndEquippableDataForToken. Adds getEquippableSlotsFromParent. --- contracts/RMRK/library/RMRKErrors.sol | 2 + contracts/RMRK/utils/RMRKEquipRenderUtils.sol | 167 +++++++++- .../RMRK/utils/RMRKMultiAssetRenderUtils.sol | 27 +- docs/RMRK/utils/RMRKEquipRenderUtils.md | 193 ++++++++++- docs/RMRK/utils/RMRKMultiAssetRenderUtils.md | 24 ++ test/renderUtils.ts | 312 +++++++++++++++++- 6 files changed, 690 insertions(+), 35 deletions(-) diff --git a/contracts/RMRK/library/RMRKErrors.sol b/contracts/RMRK/library/RMRKErrors.sol index 1ee552ec..c5eec467 100644 --- a/contracts/RMRK/library/RMRKErrors.sol +++ b/contracts/RMRK/library/RMRKErrors.sol @@ -105,6 +105,8 @@ error RMRKNotOwnerOrContributor(); error RMRKNewOwnerIsZeroAddress(); /// Attempting to assign a 0x0 address as a contributor error RMRKNewContributorIsZeroAddress(); +/// Attempting do an operation assuming a token is nested, while it is not +error RMRKParentIsNotNFT(); /// Attempting to add a `Part` with an ID that is already used error RMRKPartAlreadyExists(); /// Attempting to use a `Part` that doesn't exist diff --git a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol index 3c177023..2c7c3cdd 100644 --- a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol @@ -2,8 +2,10 @@ pragma solidity ^0.8.16; +import "./RMRKMultiAssetRenderUtils.sol"; import "../catalog/IRMRKCatalog.sol"; import "../equippable/IRMRKEquippable.sol"; +import "../nestable/IRMRKNestable.sol"; import "../library/RMRKLib.sol"; import "../library/RMRKErrors.sol"; @@ -13,7 +15,7 @@ import "../library/RMRKErrors.sol"; * @notice Smart contract of the RMRK Equip render utils module. * @dev Extra utility functions for composing RMRK extended assets. */ -contract RMRKEquipRenderUtils { +contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { using RMRKLib for uint64[]; /** @@ -86,6 +88,11 @@ contract RMRKEquipRenderUtils { string metadataURI; //n bytes 32+ } + struct AssetWithSlot { + uint64 slotPartId; + uint64 assetId; + } + /** * @notice Used to get extended active assets of the given token. * @dev The full `ExtendedActiveAsset` looks like this: @@ -106,7 +113,7 @@ contract RMRKEquipRenderUtils { * ] * @param target Address of the smart contract of the given token * @param tokenId ID of the token to retrieve the extended active assets for - * @return sturct[] An array of ExtendedActiveAssets present on the given token + * @return ExtendedActiveAssets[] An array of ExtendedActiveAssets present on the given token */ function getExtendedActiveAssets( address target, @@ -168,7 +175,7 @@ contract RMRKEquipRenderUtils { * ] * @param target Address of the smart contract of the given token * @param tokenId ID of the token to retrieve the extended pending assets for - * @return sturct[] An array of ExtendedPendingAssets present on the given token + * @return ExtendedPendingAssets[] An array of ExtendedPendingAssets present on the given token */ function getExtendedPendingAssets( address target, @@ -247,7 +254,7 @@ contract RMRKEquipRenderUtils { (, , address catalogAddress, uint64[] memory partIds) = target_ .getAssetAndEquippableData(tokenId, assetId); - (slotPartIds, ) = splitSlotAndFixedParts(partIds, catalogAddress); + (slotPartIds, ) = _splitSlotAndFixedParts(partIds, catalogAddress); uint256 len = slotPartIds.length; if (len != 0) { @@ -326,7 +333,7 @@ contract RMRKEquipRenderUtils { ( uint64[] memory slotPartIds, uint64[] memory fixedPartIds - ) = splitSlotAndFixedParts(partIds, catalogAddress); + ) = _splitSlotAndFixedParts(partIds, catalogAddress); // Fixed parts: fixedParts = new FixedPart[](fixedPartIds.length); @@ -348,7 +355,7 @@ contract RMRKEquipRenderUtils { } } - slotParts = getEquippedSlotParts( + slotParts = _getEquippedSlotParts( target_, tokenId, assetId, @@ -357,6 +364,150 @@ contract RMRKEquipRenderUtils { ); } + /** + * @notice Used to get the child's assets and slot parts pairs, for which the child asset can be equipped into parent's slot part. + * @dev The full `AssetWithSlot` struct looks like this: + * [ + * assetId, + * slotPartId + * ] + * @param target Address of the smart contract of the given token + * @param tokenId ID of the child token whose assets will be matched against parent's slot parts + * @param parentAssetId ID of the target parent asset to use to equip the child + * @return assetsWithSlots An array of `AssetWithSlot` structs containing info about the equippable child assets and their corresponding slot parts + */ + function getEquippableSlotsFromParent( + address target, + uint256 tokenId, + uint64 parentAssetId + ) public view returns (AssetWithSlot[] memory assetsWithSlots) { + ( + address parentAddress, + uint64[] memory parentSlotPartIds + ) = _getParentAndSlotParts(target, tokenId, parentAssetId); + + IRMRKEquippable targetChild = IRMRKEquippable(target); + uint64[] memory childAssets = targetChild.getActiveAssets(tokenId); + uint256 totalChildAssets = childAssets.length; + uint256 totalParentSlots = parentSlotPartIds.length; + // There can be at most min(totalChildAssets, totalParentSlots) resulting matches, we just pick one of them. + AssetWithSlot[] memory tempAssetsWithSlots = new AssetWithSlot[]( + totalParentSlots + ); + uint256 totalMatches; + + for (uint256 i; i < totalChildAssets; ) { + for (uint256 j; j < totalParentSlots; ) { + if ( + targetChild.canTokenBeEquippedWithAssetIntoSlot( + parentAddress, + tokenId, + childAssets[i], + parentSlotPartIds[j] + ) + ) { + tempAssetsWithSlots[totalMatches] = AssetWithSlot({ + assetId: childAssets[i], + slotPartId: parentSlotPartIds[j] + }); + unchecked { + ++totalMatches; + } + } + unchecked { + ++j; + } + } + unchecked { + ++i; + } + } + + // Finally, we copy the matches into the final array which has the right lenght according to results + assetsWithSlots = new AssetWithSlot[](totalMatches); + for (uint256 i; i < totalMatches; ) { + assetsWithSlots[i] = tempAssetsWithSlots[i]; + unchecked { + ++i; + } + } + } + + /** + * @notice Used to retrieve the equippable data of the specified token's asset with the highest priority. + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token for which to retrieve the equippable data of the asset with the highest priority + * @return topAsset ExtendedActiveAsset struct with the equippable data for the the asset with the highest priority + */ + function getTopAssetAndEquippableDataForToken( + address target, + uint256 tokenId + ) public view returns (ExtendedActiveAsset memory topAsset) { + (uint64 topAssetId, uint16 topPriority) = getAssetIdWithTopPriority( + target, + tokenId + ); + ( + string memory metadataURI, + uint64 equippableGroupId, + address catalogAddress, + uint64[] memory partIds + ) = IRMRKEquippable(target).getAssetAndEquippableData( + tokenId, + topAssetId + ); + topAsset = ExtendedActiveAsset({ + id: topAssetId, + equippableGroupId: equippableGroupId, + priority: topPriority, + catalogAddress: catalogAddress, + metadata: metadataURI, + partIds: partIds + }); + } + + /** + * @notice Used to retrieve the parent address and is slot part ids for a given target child. + * @param target Address of the smart contract of the given token + * @param tokenId ID of the child token + * @param parentAssetId ID of the parent asset from which to get the slot parts + * @return parentAddress Address of the parent token owning the target child + * @return parentSlotPartIds Array of slot part ids of the parent asset + * @dev Reverts if the parent is not an NFT or if the parent asset is not composable. + */ + function _getParentAndSlotParts( + address target, + uint256 tokenId, + uint64 parentAssetId + ) + private + view + returns (address parentAddress, uint64[] memory parentSlotPartIds) + { + uint256 parentId; + bool isNFT; + (parentAddress, parentId, isNFT) = IRMRKNestable(target).directOwnerOf( + tokenId + ); + if (!isNFT) revert RMRKParentIsNotNFT(); + + ( + , + , + address catalogAddress, + uint64[] memory parentPartIds + ) = IRMRKEquippable(parentAddress).getAssetAndEquippableData( + parentId, + parentAssetId + ); + if (catalogAddress == address(0)) revert RMRKNotComposableAsset(); + + (parentSlotPartIds, ) = _splitSlotAndFixedParts( + parentPartIds, + catalogAddress + ); + } + /** * @notice Used to retrieve the equipped slot parts. * @dev The full `EquippedSlotPart` struct looks like this: @@ -376,7 +527,7 @@ contract RMRKEquipRenderUtils { * @param slotPartIds An array of slot part IDs in the asset for which to retrieve the equipped slot parts * @return slotParts An array of `EquippedSlotPart` structs representing the equipped slot parts */ - function getEquippedSlotParts( + function _getEquippedSlotParts( IRMRKEquippable target_, uint256 tokenId, uint64 assetId, @@ -435,7 +586,7 @@ contract RMRKEquipRenderUtils { * @return slotPartIds An array of IDs of the `Slot` parts included in the `allPartIds` * @return fixedPartIds An array of IDs of the `Fixed` parts included in the `allPartIds` */ - function splitSlotAndFixedParts( + function _splitSlotAndFixedParts( uint64[] memory allPartIds, address catalogAddress ) diff --git a/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol b/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol index 9c114c94..5a426810 100644 --- a/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol @@ -163,6 +163,27 @@ contract RMRKMultiAssetRenderUtils { address target, uint256 tokenId ) external view returns (string memory) { + (uint64 maxPriorityAssetId, ) = getAssetIdWithTopPriority( + target, + tokenId + ); + return + IRMRKMultiAsset(target).getAssetMetadata( + tokenId, + maxPriorityAssetId + ); + } + + /** + * @notice Used to retrieve the ID of the specified token's asset with the highest priority. + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token for which to retrieve the ID of the asset with the highest priority + * @return string The ID of the asset with the highest priority + */ + function getAssetIdWithTopPriority( + address target, + uint256 tokenId + ) public view returns (uint64, uint16) { IRMRKMultiAsset target_ = IRMRKMultiAsset(target); uint16[] memory priorities = target_.getActiveAssetPriorities(tokenId); uint64[] memory assets = target_.getActiveAssets(tokenId); @@ -172,17 +193,17 @@ contract RMRKMultiAssetRenderUtils { } uint16 maxPriority = _LOWEST_POSSIBLE_PRIORITY; - uint64 maxPriorityAsset; + uint64 maxPriorityAssetId; for (uint64 i; i < len; ) { uint16 currentPrio = priorities[i]; if (currentPrio < maxPriority) { maxPriority = currentPrio; - maxPriorityAsset = assets[i]; + maxPriorityAssetId = assets[i]; } unchecked { ++i; } } - return target_.getAssetMetadata(tokenId, maxPriorityAsset); + return (maxPriorityAssetId, maxPriority); } } diff --git a/docs/RMRK/utils/RMRKEquipRenderUtils.md b/docs/RMRK/utils/RMRKEquipRenderUtils.md index f9d60371..818c6209 100644 --- a/docs/RMRK/utils/RMRKEquipRenderUtils.md +++ b/docs/RMRK/utils/RMRKEquipRenderUtils.md @@ -10,6 +10,30 @@ Smart contract of the RMRK Equip render utils module. ## Methods +### _splitSlotAndFixedParts + +```solidity +function _splitSlotAndFixedParts(uint64[] allPartIds, address catalogAddress) external view returns (uint64[] slotPartIds, uint64[] fixedPartIds) +``` + +Used to split slot and fixed parts. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| allPartIds | uint64[] | [] An array of `Part` IDs containing both, `Slot` and `Fixed` parts | +| catalogAddress | address | An address of the catalog to which the given `Part`s belong to | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| slotPartIds | uint64[] | An array of IDs of the `Slot` parts included in the `allPartIds` | +| fixedPartIds | uint64[] | An array of IDs of the `Fixed` parts included in the `allPartIds` | + ### composeEquippables ```solidity @@ -38,6 +62,101 @@ Used to compose the given equippables. | fixedParts | RMRKEquipRenderUtils.FixedPart[] | An array of fixed parts respresented by the `FixedPart` structs present on the asset | | slotParts | RMRKEquipRenderUtils.EquippedSlotPart[] | An array of slot parts represented by the `EquippedSlotPart` structs present on the asset | +### getActiveAssets + +```solidity +function getActiveAssets(address target, uint256 tokenId) external view returns (struct RMRKMultiAssetRenderUtils.ActiveAsset[]) +``` + +Used to get the active assets of the given token. + +*The full `ActiveAsset` looks like this: [ id, priority, metadata ]* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| tokenId | uint256 | ID of the token to retrieve the active assets for | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | RMRKMultiAssetRenderUtils.ActiveAsset[] | struct[] An array of ActiveAssets present on the given token | + +### getAssetIdWithTopPriority + +```solidity +function getAssetIdWithTopPriority(address target, uint256 tokenId) external view returns (uint64, uint16) +``` + +Used to retrieve the ID of the specified token's asset with the highest priority. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| tokenId | uint256 | ID of the token for which to retrieve the ID of the asset with the highest priority | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint64 | string The ID of the asset with the highest priority | +| _1 | uint16 | undefined | + +### getAssetsById + +```solidity +function getAssetsById(address target, uint256 tokenId, uint64[] assetIds) external view returns (string[]) +``` + +Used to retrieve the metadata URI of specified assets in the specified token. + +*Requirements: - `assetIds` must exist.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| tokenId | uint256 | ID of the token to retrieve the specified assets for | +| assetIds | uint64[] | [] An array of asset IDs for which to retrieve the metadata URIs | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | string[] | string[] An array of metadata URIs belonging to specified assets | + +### getEquippableSlotsFromParent + +```solidity +function getEquippableSlotsFromParent(address target, uint256 tokenId, uint64 parentAssetId) external view returns (struct RMRKEquipRenderUtils.AssetWithSlot[] assetsWithSlots) +``` + +Used to get the child's assets and slot parts pairs, for which the child asset can be equipped into parent's slot part. + +*The full `AssetWithSlot` struct looks like this: [ assetId, slotPartId ]* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| tokenId | uint256 | ID of the child token whose assets will be matched against parent's slot parts | +| parentAssetId | uint64 | ID of the target parent asset to use to equip the child | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| assetsWithSlots | RMRKEquipRenderUtils.AssetWithSlot[] | An array of `AssetWithSlot` structs containing info about the equippable child assets and their corresponding slot parts | + ### getEquipped ```solidity @@ -85,7 +204,7 @@ Used to get extended active assets of the given token. | Name | Type | Description | |---|---|---| -| _0 | RMRKEquipRenderUtils.ExtendedActiveAsset[] | sturct[] An array of ExtendedActiveAssets present on the given token | +| _0 | RMRKEquipRenderUtils.ExtendedActiveAsset[] | ExtendedActiveAssets[] An array of ExtendedActiveAssets present on the given token | ### getExtendedPendingAssets @@ -108,15 +227,38 @@ Used to get the extended pending assets of the given token. | Name | Type | Description | |---|---|---| -| _0 | RMRKEquipRenderUtils.ExtendedPendingAsset[] | sturct[] An array of ExtendedPendingAssets present on the given token | +| _0 | RMRKEquipRenderUtils.ExtendedPendingAsset[] | ExtendedPendingAssets[] An array of ExtendedPendingAssets present on the given token | -### splitSlotAndFixedParts +### getPendingAssets ```solidity -function splitSlotAndFixedParts(uint64[] allPartIds, address catalogAddress) external view returns (uint64[] slotPartIds, uint64[] fixedPartIds) +function getPendingAssets(address target, uint256 tokenId) external view returns (struct RMRKMultiAssetRenderUtils.PendingAsset[]) ``` -Used to split slot and fixed parts. +Used to get the pending assets of the given token. + +*The full `PendingAsset` looks like this: [ id, acceptRejectIndex, replacesAssetWithId, metadata ]* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| tokenId | uint256 | ID of the token to retrieve the pending assets for | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | RMRKMultiAssetRenderUtils.PendingAsset[] | struct[] An array of PendingAssets present on the given token | + +### getTopAssetAndEquippableDataForToken + +```solidity +function getTopAssetAndEquippableDataForToken(address target, uint256 tokenId) external view returns (struct RMRKEquipRenderUtils.ExtendedActiveAsset topAsset) +``` + +Used to retrieve the equippable data of the specified token's asset with the highest priority. @@ -124,15 +266,37 @@ Used to split slot and fixed parts. | Name | Type | Description | |---|---|---| -| allPartIds | uint64[] | [] An array of `Part` IDs containing both, `Slot` and `Fixed` parts | -| catalogAddress | address | An address of the catalog to which the given `Part`s belong to | +| target | address | Address of the smart contract of the given token | +| tokenId | uint256 | ID of the token for which to retrieve the equippable data of the asset with the highest priority | #### Returns | Name | Type | Description | |---|---|---| -| slotPartIds | uint64[] | An array of IDs of the `Slot` parts included in the `allPartIds` | -| fixedPartIds | uint64[] | An array of IDs of the `Fixed` parts included in the `allPartIds` | +| topAsset | RMRKEquipRenderUtils.ExtendedActiveAsset | ExtendedActiveAsset struct with the equippable data for the the asset with the highest priority | + +### getTopAssetMetaForToken + +```solidity +function getTopAssetMetaForToken(address target, uint256 tokenId) external view returns (string) +``` + +Used to retrieve the metadata URI of the specified token's asset with the highest priority. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| tokenId | uint256 | ID of the token for which to retrieve the metadata URI of the asset with the highest priority | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | string | string The metadata URI of the asset with the highest priority | @@ -150,6 +314,17 @@ Attempting to compose an asset wihtout having an associated Catalog +### RMRKParentIsNotNFT + +```solidity +error RMRKParentIsNotNFT() +``` + +Attempting do an operation assuming a token is nested, while it is not + + + + ### RMRKTokenHasNoAssets ```solidity diff --git a/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md b/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md index bfe1e141..799012b2 100644 --- a/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md +++ b/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md @@ -33,6 +33,30 @@ Used to get the active assets of the given token. |---|---|---| | _0 | RMRKMultiAssetRenderUtils.ActiveAsset[] | struct[] An array of ActiveAssets present on the given token | +### getAssetIdWithTopPriority + +```solidity +function getAssetIdWithTopPriority(address target, uint256 tokenId) external view returns (uint64, uint16) +``` + +Used to retrieve the ID of the specified token's asset with the highest priority. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| tokenId | uint256 | ID of the token for which to retrieve the ID of the asset with the highest priority | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint64 | string The ID of the asset with the highest priority | +| _1 | uint16 | undefined | + ### getAssetsById ```solidity diff --git a/test/renderUtils.ts b/test/renderUtils.ts index bb65e8d3..eaece152 100644 --- a/test/renderUtils.ts +++ b/test/renderUtils.ts @@ -1,9 +1,10 @@ import { ethers } from 'hardhat'; import { expect } from 'chai'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { ADDRESS_ZERO, bn, mintFromMock } from './utils'; +import { ADDRESS_ZERO, bn, mintFromMock, nestMintFromMock } from './utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { + RMRKCatalogMock, RMRKEquippableMock, RMRKEquipRenderUtils, RMRKMultiAssetRenderUtils, @@ -12,10 +13,14 @@ import { // --------------- FIXTURES ----------------------- async function assetsFixture() { + const catalogFactory = await ethers.getContractFactory('RMRKCatalogMock'); const equipFactory = await ethers.getContractFactory('RMRKEquippableMock'); const renderUtilsFactory = await ethers.getContractFactory('RMRKMultiAssetRenderUtils'); const renderUtilsEquipFactory = await ethers.getContractFactory('RMRKEquipRenderUtils'); + const catalog = await catalogFactory.deploy('ipfs://catalog.json', 'misc'); + await catalog.deployed(); + const equip = await equipFactory.deploy('Chunky', 'CHNK'); await equip.deployed(); @@ -25,12 +30,31 @@ async function assetsFixture() { const renderUtilsEquip = await renderUtilsEquipFactory.deploy(); await renderUtilsEquip.deployed(); - return { equip, renderUtils, renderUtilsEquip }; + return { catalog, equip, renderUtils, renderUtilsEquip }; +} + +async function advancedAssetsFixture() { + const catalogFactory = await ethers.getContractFactory('RMRKCatalogMock'); + const equipFactory = await ethers.getContractFactory('RMRKEquippableMock'); + const renderUtilsEquipFactory = await ethers.getContractFactory('RMRKEquipRenderUtils'); + + const catalog = await catalogFactory.deploy('ipfs://catalog.json', 'misc'); + + const kanaria = await equipFactory.deploy('Kanaria', 'KAN'); + kanaria.deployed(); + + const gem = await equipFactory.deploy('Kanaria Gem', 'KGEM'); + gem.deployed(); + + const renderUtilsEquip = await renderUtilsEquipFactory.deploy(); + await renderUtilsEquip.deployed(); + + return { catalog, kanaria, gem, renderUtilsEquip }; } describe('Render Utils', async function () { let owner: SignerWithAddress; - let someCatalog: SignerWithAddress; + let catalog: RMRKCatalogMock; let equip: RMRKEquippableMock; let renderUtils: RMRKMultiAssetRenderUtils; let renderUtilsEquip: RMRKEquipRenderUtils; @@ -42,23 +66,16 @@ describe('Render Utils', async function () { const resId4 = bn(4); beforeEach(async function () { - ({ equip, renderUtils, renderUtilsEquip } = await loadFixture(assetsFixture)); + ({ catalog, equip, renderUtils, renderUtilsEquip } = await loadFixture(assetsFixture)); const signers = await ethers.getSigners(); owner = signers[0]; - someCatalog = signers[1]; tokenId = await mintFromMock(equip, owner.address); await equip.addEquippableAssetEntry(resId, 0, ADDRESS_ZERO, 'ipfs://res1.jpg', []); - await equip.addEquippableAssetEntry( - resId2, - 1, - someCatalog.address, - 'ipfs://res2.jpg', - [1, 3, 4], - ); + await equip.addEquippableAssetEntry(resId2, 1, catalog.address, 'ipfs://res2.jpg', [1, 3, 4]); await equip.addEquippableAssetEntry(resId3, 0, ADDRESS_ZERO, 'ipfs://res3.jpg', []); - await equip.addEquippableAssetEntry(resId4, 2, someCatalog.address, 'ipfs://res4.jpg', [4]); + await equip.addEquippableAssetEntry(resId4, 2, catalog.address, 'ipfs://res4.jpg', [4]); await equip.addAssetToToken(tokenId, resId, 0); await equip.addAssetToToken(tokenId, resId2, 0); await equip.addAssetToToken(tokenId, resId3, resId); @@ -134,15 +151,280 @@ describe('Render Utils', async function () { it('can get active assets', async function () { expect(await renderUtilsEquip.getExtendedActiveAssets(equip.address, tokenId)).to.eql([ [resId, bn(0), 10, ADDRESS_ZERO, 'ipfs://res1.jpg', []], - [resId2, bn(1), 5, someCatalog.address, 'ipfs://res2.jpg', [bn(1), bn(3), bn(4)]], + [resId2, bn(1), 5, catalog.address, 'ipfs://res2.jpg', [bn(1), bn(3), bn(4)]], ]); }); it('can get pending assets', async function () { expect(await renderUtilsEquip.getExtendedPendingAssets(equip.address, tokenId)).to.eql([ - [resId4, bn(2), bn(0), bn(0), someCatalog.address, 'ipfs://res4.jpg', [bn(4)]], + [resId4, bn(2), bn(0), bn(0), catalog.address, 'ipfs://res4.jpg', [bn(4)]], [resId3, bn(0), bn(1), resId, ADDRESS_ZERO, 'ipfs://res3.jpg', []], ]); }); + + it('can get top equippable data for asset by priority', async function () { + expect( + await renderUtilsEquip.getTopAssetAndEquippableDataForToken(equip.address, tokenId), + ).to.eql([resId2, bn(1), 5, catalog.address, 'ipfs://res2.jpg', [bn(1), bn(3), bn(4)]]); + }); + + it('cannot get equippable slots from parent if parent is not an NFT', async function () { + await expect( + renderUtilsEquip.getEquippableSlotsFromParent(equip.address, tokenId, 1), + ).to.be.revertedWithCustomError(renderUtilsEquip, 'RMRKParentIsNotNFT'); + }); }); }); + +// These refIds are used from the child's perspective, to group assets that can be equipped into a parent +// With it, we avoid the need to do set it asset by asset +const noEquippableGroup = 0; +const equippableRefIdLeftGem = 1; +const equippableRefIdMidGem = 2; +const equippableRefIdRightGem = 3; + +const assetForGemAFull = 1; +const assetForGemALeft = 2; +const assetForGemAMid = 3; +const assetForGemARight = 4; +const assetForGemBFull = 5; +const assetForGemBLeft = 6; +const assetForGemBMid = 7; +const assetForGemBRight = 8; +const assetForKanariaFull = 9; + +const slotIdGemLeft = 1; +const slotIdGemMid = 2; +const slotIdGemRight = 3; + +describe('Advanced Equip Render Utils', async function () { + let owner: SignerWithAddress; + let catalog: RMRKCatalogMock; + let kanaria: RMRKEquippableMock; + let gem: RMRKEquippableMock; + let renderUtilsEquip: RMRKEquipRenderUtils; + let kanariaId: number; + let gemId1: number; + let gemId2: number; + let gemId3: number; + + beforeEach(async function () { + ({ catalog, kanaria, gem, renderUtilsEquip } = await loadFixture(advancedAssetsFixture)); + [owner] = await ethers.getSigners(); + + kanariaId = await mintFromMock(kanaria, owner.address); + gemId1 = await nestMintFromMock(gem, kanaria.address, kanariaId); + gemId2 = await nestMintFromMock(gem, kanaria.address, kanariaId); + gemId3 = await nestMintFromMock(gem, kanaria.address, kanariaId); + }); + + it('can get equippable slots from parent', async function () { + await setUpCatalog(catalog, gem.address); + await setUpKanariaAsset(kanaria, kanariaId, catalog.address); + await setUpGemAssets(gem, gemId1, gemId2, gemId3, kanaria.address, catalog.address); + + expect( + await renderUtilsEquip.getEquippableSlotsFromParent(gem.address, gemId1, assetForKanariaFull), + ).to.eql([ + [bn(slotIdGemRight), bn(assetForGemARight)], + [bn(slotIdGemMid), bn(assetForGemAMid)], + [bn(slotIdGemLeft), bn(assetForGemALeft)], + ]); + expect( + await renderUtilsEquip.getEquippableSlotsFromParent(gem.address, gemId2, assetForKanariaFull), + ).to.eql([ + [bn(slotIdGemRight), bn(assetForGemARight)], + [bn(slotIdGemMid), bn(assetForGemAMid)], + [bn(slotIdGemLeft), bn(assetForGemALeft)], + ]); + expect( + await renderUtilsEquip.getEquippableSlotsFromParent(gem.address, gemId3, assetForKanariaFull), + ).to.eql([ + [bn(slotIdGemRight), bn(assetForGemBRight)], + [bn(slotIdGemMid), bn(assetForGemBMid)], + [bn(slotIdGemLeft), bn(assetForGemBLeft)], + ]); + }); + + it('cannot get equippable slots from parent if the asset id is not composable', async function () { + const assetForKanariaNotEquippable = 10; + await setUpCatalog(catalog, gem.address); + await setUpKanariaAsset(kanaria, kanariaId, catalog.address); + await setUpGemAssets(gem, gemId1, gemId2, gemId3, kanaria.address, catalog.address); + + await kanaria.addEquippableAssetEntry( + assetForKanariaNotEquippable, + 0, + ADDRESS_ZERO, + 'ipfs://kanaria.jpg', + [], + ); + await kanaria.addAssetToToken(kanariaId, assetForKanariaNotEquippable, 0); + await kanaria.acceptAsset(kanariaId, 0, assetForKanariaNotEquippable); + await expect( + renderUtilsEquip.getEquippableSlotsFromParent( + gem.address, + gemId1, + assetForKanariaNotEquippable, + ), + ).to.be.revertedWithCustomError(renderUtilsEquip, 'RMRKNotComposableAsset'); + }); +}); + +async function setUpCatalog(catalog: RMRKCatalogMock, gemAddress: string): Promise { + await catalog.addPartList([ + { + // Gems slot 1 + partId: slotIdGemLeft, + part: { + itemType: 1, // Slot + z: 4, + equippable: [gemAddress], // Only gems tokens can be equipped here + metadataURI: '', + }, + }, + { + // Gems slot 2 + partId: slotIdGemMid, + part: { + itemType: 1, // Slot + z: 4, + equippable: [gemAddress], // Only gems tokens can be equipped here + metadataURI: '', + }, + }, + { + // Gems slot 3 + partId: slotIdGemRight, + part: { + itemType: 1, // Slot + z: 4, + equippable: [gemAddress], // Only gems tokens can be equipped here + metadataURI: '', + }, + }, + ]); +} + +async function setUpKanariaAsset( + kanaria: RMRKEquippableMock, + kanariaId: number, + catalogAddress: string, +): Promise { + await kanaria.addEquippableAssetEntry( + assetForKanariaFull, + noEquippableGroup, + catalogAddress, + `ipfs://kanaria/full.svg`, + [slotIdGemLeft, slotIdGemMid, slotIdGemRight], + ); + await kanaria.addAssetToToken(kanariaId, assetForKanariaFull, 0); + await kanaria.acceptAsset(kanariaId, 0, assetForKanariaFull); +} + +async function setUpGemAssets( + gem: RMRKEquippableMock, + gemId1: number, + gemId2: number, + gemId3: number, + kanariaAddress: string, + catalogAddress: string, +): Promise { + const [owner] = await ethers.getSigners(); + // We'll add 4 assets for each gem, a full version and 3 versions matching each slot. + // We will have only 2 types of gems -> 4x2: 8 assets. + // This is not composed by others, so fixed and slot parts are never used. + await gem.addEquippableAssetEntry( + assetForGemAFull, + noEquippableGroup, + catalogAddress, + `ipfs://gems/typeA/full.svg`, + [], + ); + await gem.addEquippableAssetEntry( + assetForGemALeft, + equippableRefIdLeftGem, + catalogAddress, + `ipfs://gems/typeA/left.svg`, + [], + ); + await gem.addEquippableAssetEntry( + assetForGemAMid, + equippableRefIdMidGem, + catalogAddress, + `ipfs://gems/typeA/mid.svg`, + [], + ); + await gem.addEquippableAssetEntry( + assetForGemARight, + equippableRefIdRightGem, + catalogAddress, + `ipfs://gems/typeA/right.svg`, + [], + ); + await gem.addEquippableAssetEntry( + assetForGemBFull, + noEquippableGroup, + catalogAddress, + `ipfs://gems/typeB/full.svg`, + [], + ); + await gem.addEquippableAssetEntry( + assetForGemBLeft, + equippableRefIdLeftGem, + catalogAddress, + `ipfs://gems/typeB/left.svg`, + [], + ); + await gem.addEquippableAssetEntry( + assetForGemBMid, + equippableRefIdMidGem, + catalogAddress, + `ipfs://gems/typeB/mid.svg`, + [], + ); + await gem.addEquippableAssetEntry( + assetForGemBRight, + equippableRefIdRightGem, + catalogAddress, + `ipfs://gems/typeB/right.svg`, + [], + ); + + await gem.setValidParentForEquippableGroup(equippableRefIdLeftGem, kanariaAddress, slotIdGemLeft); + await gem.setValidParentForEquippableGroup(equippableRefIdMidGem, kanariaAddress, slotIdGemMid); + await gem.setValidParentForEquippableGroup( + equippableRefIdRightGem, + kanariaAddress, + slotIdGemRight, + ); + + // We add assets of type A to gem 1 and 2, and type Bto gem 3. Both are nested into the first kanaria + // This means gems 1 and 2 will have the same asset, which is totally valid. + await gem.addAssetToToken(gemId1, assetForGemAFull, 0); + await gem.addAssetToToken(gemId1, assetForGemALeft, 0); + await gem.addAssetToToken(gemId1, assetForGemAMid, 0); + await gem.addAssetToToken(gemId1, assetForGemARight, 0); + await gem.addAssetToToken(gemId2, assetForGemAFull, 0); + await gem.addAssetToToken(gemId2, assetForGemALeft, 0); + await gem.addAssetToToken(gemId2, assetForGemAMid, 0); + await gem.addAssetToToken(gemId2, assetForGemARight, 0); + await gem.addAssetToToken(gemId3, assetForGemBFull, 0); + await gem.addAssetToToken(gemId3, assetForGemBLeft, 0); + await gem.addAssetToToken(gemId3, assetForGemBMid, 0); + await gem.addAssetToToken(gemId3, assetForGemBRight, 0); + + // We accept them backwards to easily know the right indices + await gem.acceptAsset(gemId1, 3, assetForGemARight); + await gem.acceptAsset(gemId1, 2, assetForGemAMid); + await gem.acceptAsset(gemId1, 1, assetForGemALeft); + await gem.acceptAsset(gemId1, 0, assetForGemAFull); + await gem.acceptAsset(gemId2, 3, assetForGemARight); + await gem.acceptAsset(gemId2, 2, assetForGemAMid); + await gem.acceptAsset(gemId2, 1, assetForGemALeft); + await gem.acceptAsset(gemId2, 0, assetForGemAFull); + await gem.acceptAsset(gemId3, 3, assetForGemBRight); + await gem.acceptAsset(gemId3, 2, assetForGemBMid); + await gem.acceptAsset(gemId3, 1, assetForGemBLeft); + await gem.acceptAsset(gemId3, 0, assetForGemBFull); +} From fdeea2600faa3f34011230e12717e1794ec23d82 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Mon, 16 Jan 2023 17:30:47 -0500 Subject: [PATCH 17/26] Implements getPaginatedMintedIds and renames some utils for clarity. --- contracts/RMRK/utils/RMRKEquipRenderUtils.sol | 58 +++++++------- .../RMRK/utils/RMRKMultiAssetRenderUtils.sol | 14 ++-- contracts/RMRK/utils/RMRKRenderUtils.sol | 59 ++++++++++++++ docs/RMRK/utils/RMRKEquipRenderUtils.md | 80 ++++++++++++------- docs/RMRK/utils/RMRKMultiAssetRenderUtils.md | 40 +++++----- docs/RMRK/utils/RMRKRenderUtils.md | 39 +++++++++ test/behavior/equippableSlots.ts | 12 ++- test/renderUtils.ts | 72 ++++++++++++++--- 8 files changed, 280 insertions(+), 94 deletions(-) create mode 100644 contracts/RMRK/utils/RMRKRenderUtils.sol create mode 100644 docs/RMRK/utils/RMRKRenderUtils.md diff --git a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol index 2c7c3cdd..b4e5a9c6 100644 --- a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.16; +import "./RMRKRenderUtils.sol"; import "./RMRKMultiAssetRenderUtils.sol"; import "../catalog/IRMRKCatalog.sol"; import "../equippable/IRMRKEquippable.sol"; @@ -15,7 +16,7 @@ import "../library/RMRKErrors.sol"; * @notice Smart contract of the RMRK Equip render utils module. * @dev Extra utility functions for composing RMRK extended assets. */ -contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { +contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { using RMRKLib for uint64[]; /** @@ -27,7 +28,7 @@ contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { * @return metadata Metadata URI of the asset * @return partIds[] An array of IDs of fixed and slot parts present in the asset */ - struct ExtendedActiveAsset { + struct ExtendedEquippableActiveAsset { uint64 id; uint64 equippableGroupId; uint16 priority; @@ -95,7 +96,7 @@ contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { /** * @notice Used to get extended active assets of the given token. - * @dev The full `ExtendedActiveAsset` looks like this: + * @dev The full `ExtendedEquippableActiveAsset` looks like this: * [ * ID, * equippableGroupId, @@ -115,10 +116,10 @@ contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { * @param tokenId ID of the token to retrieve the extended active assets for * @return ExtendedActiveAssets[] An array of ExtendedActiveAssets present on the given token */ - function getExtendedActiveAssets( + function getExtendedEquippableActiveAssets( address target, uint256 tokenId - ) public view virtual returns (ExtendedActiveAsset[] memory) { + ) public view virtual returns (ExtendedEquippableActiveAsset[] memory) { IRMRKEquippable target_ = IRMRKEquippable(target); uint64[] memory assets = target_.getActiveAssets(tokenId); @@ -128,9 +129,8 @@ contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { revert RMRKTokenHasNoAssets(); } - ExtendedActiveAsset[] memory activeAssets = new ExtendedActiveAsset[]( - len - ); + ExtendedEquippableActiveAsset[] + memory activeAssets = new ExtendedEquippableActiveAsset[](len); for (uint256 i; i < len; ) { ( @@ -139,7 +139,7 @@ contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { address catalogAddress, uint64[] memory partIds ) = target_.getAssetAndEquippableData(tokenId, assets[i]); - activeAssets[i] = ExtendedActiveAsset({ + activeAssets[i] = ExtendedEquippableActiveAsset({ id: assets[i], equippableGroupId: equippableGroupId, priority: priorities[i], @@ -257,25 +257,23 @@ contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { (slotPartIds, ) = _splitSlotAndFixedParts(partIds, catalogAddress); uint256 len = slotPartIds.length; - if (len != 0) { - childrenEquipped = new IRMRKEquippable.Equipment[](len); - childrenAssetMetadata = new string[](len); + childrenEquipped = new IRMRKEquippable.Equipment[](len); + childrenAssetMetadata = new string[](len); - for (uint256 i; i < len; ) { - IRMRKEquippable.Equipment memory equipment = target_ - .getEquipment(tokenId, catalogAddress, slotPartIds[i]); - if (equipment.assetId == assetId) { - childrenEquipped[i] = equipment; - childrenAssetMetadata[i] = IRMRKEquippable( - equipment.childEquippableAddress - ).getAssetMetadata( - equipment.childId, - equipment.childAssetId - ); - } - unchecked { - ++i; - } + for (uint256 i; i < len; ) { + IRMRKEquippable.Equipment memory equipment = target_.getEquipment( + tokenId, + catalogAddress, + slotPartIds[i] + ); + if (equipment.assetId == assetId) { + childrenEquipped[i] = equipment; + childrenAssetMetadata[i] = IRMRKEquippable( + equipment.childEquippableAddress + ).getAssetMetadata(equipment.childId, equipment.childAssetId); + } + unchecked { + ++i; } } } @@ -437,12 +435,12 @@ contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { * @notice Used to retrieve the equippable data of the specified token's asset with the highest priority. * @param target Address of the smart contract of the given token * @param tokenId ID of the token for which to retrieve the equippable data of the asset with the highest priority - * @return topAsset ExtendedActiveAsset struct with the equippable data for the the asset with the highest priority + * @return topAsset ExtendedEquippableActiveAsset struct with the equippable data for the the asset with the highest priority */ function getTopAssetAndEquippableDataForToken( address target, uint256 tokenId - ) public view returns (ExtendedActiveAsset memory topAsset) { + ) public view returns (ExtendedEquippableActiveAsset memory topAsset) { (uint64 topAssetId, uint16 topPriority) = getAssetIdWithTopPriority( target, tokenId @@ -456,7 +454,7 @@ contract RMRKEquipRenderUtils is RMRKMultiAssetRenderUtils { tokenId, topAssetId ); - topAsset = ExtendedActiveAsset({ + topAsset = ExtendedEquippableActiveAsset({ id: topAssetId, equippableGroupId: equippableGroupId, priority: topPriority, diff --git a/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol b/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol index 5a426810..7a3a7c02 100644 --- a/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol @@ -18,7 +18,7 @@ contract RMRKMultiAssetRenderUtils { * @return priority The priority assigned to the asset * @return metadata The metadata URI of the asset */ - struct ActiveAsset { + struct ExtendedActiveAsset { uint64 id; uint16 priority; string metadata; @@ -40,7 +40,7 @@ contract RMRKMultiAssetRenderUtils { /** * @notice Used to get the active assets of the given token. - * @dev The full `ActiveAsset` looks like this: + * @dev The full `ExtendedActiveAsset` looks like this: * [ * id, * priority, @@ -50,10 +50,10 @@ contract RMRKMultiAssetRenderUtils { * @param tokenId ID of the token to retrieve the active assets for * @return struct[] An array of ActiveAssets present on the given token */ - function getActiveAssets( + function getExtendedActiveAssets( address target, uint256 tokenId - ) public view virtual returns (ActiveAsset[] memory) { + ) public view virtual returns (ExtendedActiveAsset[] memory) { IRMRKMultiAsset target_ = IRMRKMultiAsset(target); uint64[] memory assets = target_.getActiveAssets(tokenId); @@ -63,11 +63,13 @@ contract RMRKMultiAssetRenderUtils { revert RMRKTokenHasNoAssets(); } - ActiveAsset[] memory activeAssets = new ActiveAsset[](len); + ExtendedActiveAsset[] memory activeAssets = new ExtendedActiveAsset[]( + len + ); string memory metadata; for (uint256 i; i < len; ) { metadata = target_.getAssetMetadata(tokenId, assets[i]); - activeAssets[i] = ActiveAsset({ + activeAssets[i] = ExtendedActiveAsset({ id: assets[i], priority: priorities[i], metadata: metadata diff --git a/contracts/RMRK/utils/RMRKRenderUtils.sol b/contracts/RMRK/utils/RMRKRenderUtils.sol new file mode 100644 index 00000000..0f4a6897 --- /dev/null +++ b/contracts/RMRK/utils/RMRKRenderUtils.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.16; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "../library/RMRKLib.sol"; +import "../library/RMRKErrors.sol"; + +/** + * @title RMRKRenderUtils + * @author RMRK team + * @notice Smart contract of the RMRK render utils module. + * @dev Extra utility functions for RMRK contracts. + */ +contract RMRKRenderUtils { + /** + * @notice Used to get a list of existing token Ids in the range given by pageStart and pageSize + * @dev It does not optimize to avoid checking Ids out of max supply nor total supply. + * @dev The resulting array might be smaller than the given pageSize since not existing Ids are not included. + * @param target Address of the smart contract of the given token + * @param pageStart The first Id to check + * @param pageSize The number of Ids to check + * @return page An array of existing token Ids + */ + function getPaginatedMintedIds( + address target, + uint256 pageStart, + uint256 pageSize + ) public view returns (uint256[] memory page) { + uint256[] memory tmpIds = new uint[](pageSize); + uint256 found; + for (uint256 i = 0; i < pageSize; ) { + try IERC721(target).ownerOf(pageStart + i) returns (address) { + tmpIds[i] = pageStart + i; + unchecked { + found += 1; + } + } catch { + // do nothing + } + unchecked { + ++i; + } + } + page = new uint256[](found); + uint256 actualIndex; + for (uint256 i = 0; i < pageSize; ) { + if (tmpIds[i] != 0) { + page[actualIndex] = tmpIds[i]; + unchecked { + ++actualIndex; + } + } + unchecked { + ++i; + } + } + } +} diff --git a/docs/RMRK/utils/RMRKEquipRenderUtils.md b/docs/RMRK/utils/RMRKEquipRenderUtils.md index 818c6209..df3c3106 100644 --- a/docs/RMRK/utils/RMRKEquipRenderUtils.md +++ b/docs/RMRK/utils/RMRKEquipRenderUtils.md @@ -62,29 +62,6 @@ Used to compose the given equippables. | fixedParts | RMRKEquipRenderUtils.FixedPart[] | An array of fixed parts respresented by the `FixedPart` structs present on the asset | | slotParts | RMRKEquipRenderUtils.EquippedSlotPart[] | An array of slot parts represented by the `EquippedSlotPart` structs present on the asset | -### getActiveAssets - -```solidity -function getActiveAssets(address target, uint256 tokenId) external view returns (struct RMRKMultiAssetRenderUtils.ActiveAsset[]) -``` - -Used to get the active assets of the given token. - -*The full `ActiveAsset` looks like this: [ id, priority, metadata ]* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| target | address | Address of the smart contract of the given token | -| tokenId | uint256 | ID of the token to retrieve the active assets for | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | RMRKMultiAssetRenderUtils.ActiveAsset[] | struct[] An array of ActiveAssets present on the given token | - ### getAssetIdWithTopPriority ```solidity @@ -186,12 +163,35 @@ Used to retrieve the equipped parts of the given token. ### getExtendedActiveAssets ```solidity -function getExtendedActiveAssets(address target, uint256 tokenId) external view returns (struct RMRKEquipRenderUtils.ExtendedActiveAsset[]) +function getExtendedActiveAssets(address target, uint256 tokenId) external view returns (struct RMRKMultiAssetRenderUtils.ExtendedActiveAsset[]) +``` + +Used to get the active assets of the given token. + +*The full `ExtendedActiveAsset` looks like this: [ id, priority, metadata ]* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| tokenId | uint256 | ID of the token to retrieve the active assets for | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | RMRKMultiAssetRenderUtils.ExtendedActiveAsset[] | struct[] An array of ActiveAssets present on the given token | + +### getExtendedEquippableActiveAssets + +```solidity +function getExtendedEquippableActiveAssets(address target, uint256 tokenId) external view returns (struct RMRKEquipRenderUtils.ExtendedEquippableActiveAsset[]) ``` Used to get extended active assets of the given token. -*The full `ExtendedActiveAsset` looks like this: [ ID, equippableGroupId, priority, catalogAddress, metadata, [ fixedPartId0, fixedPartId1, fixedPartId2, slotPartId0, slotPartId1, slotPartId2 ] ]* +*The full `ExtendedEquippableActiveAsset` looks like this: [ ID, equippableGroupId, priority, catalogAddress, metadata, [ fixedPartId0, fixedPartId1, fixedPartId2, slotPartId0, slotPartId1, slotPartId2 ] ]* #### Parameters @@ -204,7 +204,7 @@ Used to get extended active assets of the given token. | Name | Type | Description | |---|---|---| -| _0 | RMRKEquipRenderUtils.ExtendedActiveAsset[] | ExtendedActiveAssets[] An array of ExtendedActiveAssets present on the given token | +| _0 | RMRKEquipRenderUtils.ExtendedEquippableActiveAsset[] | ExtendedActiveAssets[] An array of ExtendedActiveAssets present on the given token | ### getExtendedPendingAssets @@ -229,6 +229,30 @@ Used to get the extended pending assets of the given token. |---|---|---| | _0 | RMRKEquipRenderUtils.ExtendedPendingAsset[] | ExtendedPendingAssets[] An array of ExtendedPendingAssets present on the given token | +### getPaginatedMintedIds + +```solidity +function getPaginatedMintedIds(address target, uint256 pageStart, uint256 pageSize) external view returns (uint256[] page) +``` + +Used to get a list of existing token Ids in the range given by pageStart and pageSize + +*It does not optimize to avoid checking Ids out of max supply nor total supply.The resulting array might be smaller than the given pageSize since not existing Ids are not included.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| pageStart | uint256 | The first Id to check | +| pageSize | uint256 | The number of Ids to check | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| page | uint256[] | An array of existing token Ids | + ### getPendingAssets ```solidity @@ -255,7 +279,7 @@ Used to get the pending assets of the given token. ### getTopAssetAndEquippableDataForToken ```solidity -function getTopAssetAndEquippableDataForToken(address target, uint256 tokenId) external view returns (struct RMRKEquipRenderUtils.ExtendedActiveAsset topAsset) +function getTopAssetAndEquippableDataForToken(address target, uint256 tokenId) external view returns (struct RMRKEquipRenderUtils.ExtendedEquippableActiveAsset topAsset) ``` Used to retrieve the equippable data of the specified token's asset with the highest priority. @@ -273,7 +297,7 @@ Used to retrieve the equippable data of the specified token's asset with the | Name | Type | Description | |---|---|---| -| topAsset | RMRKEquipRenderUtils.ExtendedActiveAsset | ExtendedActiveAsset struct with the equippable data for the the asset with the highest priority | +| topAsset | RMRKEquipRenderUtils.ExtendedEquippableActiveAsset | ExtendedEquippableActiveAsset struct with the equippable data for the the asset with the highest priority | ### getTopAssetMetaForToken diff --git a/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md b/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md index 799012b2..640edca2 100644 --- a/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md +++ b/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md @@ -10,76 +10,76 @@ ## Methods -### getActiveAssets +### getAssetIdWithTopPriority ```solidity -function getActiveAssets(address target, uint256 tokenId) external view returns (struct RMRKMultiAssetRenderUtils.ActiveAsset[]) +function getAssetIdWithTopPriority(address target, uint256 tokenId) external view returns (uint64, uint16) ``` -Used to get the active assets of the given token. +Used to retrieve the ID of the specified token's asset with the highest priority. + -*The full `ActiveAsset` looks like this: [ id, priority, metadata ]* #### Parameters | Name | Type | Description | |---|---|---| | target | address | Address of the smart contract of the given token | -| tokenId | uint256 | ID of the token to retrieve the active assets for | +| tokenId | uint256 | ID of the token for which to retrieve the ID of the asset with the highest priority | #### Returns | Name | Type | Description | |---|---|---| -| _0 | RMRKMultiAssetRenderUtils.ActiveAsset[] | struct[] An array of ActiveAssets present on the given token | +| _0 | uint64 | string The ID of the asset with the highest priority | +| _1 | uint16 | undefined | -### getAssetIdWithTopPriority +### getAssetsById ```solidity -function getAssetIdWithTopPriority(address target, uint256 tokenId) external view returns (uint64, uint16) +function getAssetsById(address target, uint256 tokenId, uint64[] assetIds) external view returns (string[]) ``` -Used to retrieve the ID of the specified token's asset with the highest priority. - +Used to retrieve the metadata URI of specified assets in the specified token. +*Requirements: - `assetIds` must exist.* #### Parameters | Name | Type | Description | |---|---|---| | target | address | Address of the smart contract of the given token | -| tokenId | uint256 | ID of the token for which to retrieve the ID of the asset with the highest priority | +| tokenId | uint256 | ID of the token to retrieve the specified assets for | +| assetIds | uint64[] | [] An array of asset IDs for which to retrieve the metadata URIs | #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint64 | string The ID of the asset with the highest priority | -| _1 | uint16 | undefined | +| _0 | string[] | string[] An array of metadata URIs belonging to specified assets | -### getAssetsById +### getExtendedActiveAssets ```solidity -function getAssetsById(address target, uint256 tokenId, uint64[] assetIds) external view returns (string[]) +function getExtendedActiveAssets(address target, uint256 tokenId) external view returns (struct RMRKMultiAssetRenderUtils.ExtendedActiveAsset[]) ``` -Used to retrieve the metadata URI of specified assets in the specified token. +Used to get the active assets of the given token. -*Requirements: - `assetIds` must exist.* +*The full `ExtendedActiveAsset` looks like this: [ id, priority, metadata ]* #### Parameters | Name | Type | Description | |---|---|---| | target | address | Address of the smart contract of the given token | -| tokenId | uint256 | ID of the token to retrieve the specified assets for | -| assetIds | uint64[] | [] An array of asset IDs for which to retrieve the metadata URIs | +| tokenId | uint256 | ID of the token to retrieve the active assets for | #### Returns | Name | Type | Description | |---|---|---| -| _0 | string[] | string[] An array of metadata URIs belonging to specified assets | +| _0 | RMRKMultiAssetRenderUtils.ExtendedActiveAsset[] | struct[] An array of ActiveAssets present on the given token | ### getPendingAssets diff --git a/docs/RMRK/utils/RMRKRenderUtils.md b/docs/RMRK/utils/RMRKRenderUtils.md new file mode 100644 index 00000000..4f12f277 --- /dev/null +++ b/docs/RMRK/utils/RMRKRenderUtils.md @@ -0,0 +1,39 @@ +# RMRKRenderUtils + +*RMRK team* + +> RMRKRenderUtils + +Smart contract of the RMRK render utils module. + +*Extra utility functions for RMRK contracts.* + +## Methods + +### getPaginatedMintedIds + +```solidity +function getPaginatedMintedIds(address target, uint256 pageStart, uint256 pageSize) external view returns (uint256[] page) +``` + +Used to get a list of existing token Ids in the range given by pageStart and pageSize + +*It does not optimize to avoid checking Ids out of max supply nor total supply.The resulting array might be smaller than the given pageSize since not existing Ids are not included.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| target | address | Address of the smart contract of the given token | +| pageStart | uint256 | The first Id to check | +| pageSize | uint256 | The number of Ids to check | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| page | uint256[] | An array of existing token Ids | + + + + diff --git a/test/behavior/equippableSlots.ts b/test/behavior/equippableSlots.ts index 2fad6395..d3e7ebae 100644 --- a/test/behavior/equippableSlots.ts +++ b/test/behavior/equippableSlots.ts @@ -566,6 +566,17 @@ async function shouldBehaveLikeEquippableWithSlots( childIndex: number, weaponResId: number, ): Promise { + // It's ok if nothing equipped + const expectedSlots = [bn(partIdForWeapon), bn(partIdForBackground)]; + expect(await view.getEquipped(soldierEquip.address, soldiersIds[0], soldierResId)).to.eql([ + expectedSlots, + [ + [bn(0), bn(0), bn(0), ethers.constants.AddressZero], + [bn(0), bn(0), bn(0), ethers.constants.AddressZero], + ], + ['', ''], + ]); + await expect( soldierEquip .connect(from) @@ -581,7 +592,6 @@ async function shouldBehaveLikeEquippableWithSlots( weaponAssetsEquip[0], ); // All part slots are included on the response: - const expectedSlots = [bn(partIdForWeapon), bn(partIdForBackground)]; // If a slot has nothing equipped, it returns an empty equip: const expectedEquips = [ [bn(soldierResId), bn(weaponResId), bn(weaponsIds[0]), weaponEquip.address], diff --git a/test/renderUtils.ts b/test/renderUtils.ts index eaece152..5f3f92e0 100644 --- a/test/renderUtils.ts +++ b/test/renderUtils.ts @@ -4,6 +4,7 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { ADDRESS_ZERO, bn, mintFromMock, nestMintFromMock } from './utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { + RMRKRenderUtils, RMRKCatalogMock, RMRKEquippableMock, RMRKEquipRenderUtils, @@ -12,7 +13,7 @@ import { // --------------- FIXTURES ----------------------- -async function assetsFixture() { +async function multiAsetAndEquipRenderUtilsFixture() { const catalogFactory = await ethers.getContractFactory('RMRKCatalogMock'); const equipFactory = await ethers.getContractFactory('RMRKEquippableMock'); const renderUtilsFactory = await ethers.getContractFactory('RMRKMultiAssetRenderUtils'); @@ -33,7 +34,7 @@ async function assetsFixture() { return { catalog, equip, renderUtils, renderUtilsEquip }; } -async function advancedAssetsFixture() { +async function advancedEquipRenderUtilsFixture() { const catalogFactory = await ethers.getContractFactory('RMRKCatalogMock'); const equipFactory = await ethers.getContractFactory('RMRKEquippableMock'); const renderUtilsEquipFactory = await ethers.getContractFactory('RMRKEquipRenderUtils'); @@ -52,7 +53,20 @@ async function advancedAssetsFixture() { return { catalog, kanaria, gem, renderUtilsEquip }; } -describe('Render Utils', async function () { +async function simpleRenderUtilsFixture() { + const equipFactory = await ethers.getContractFactory('RMRKEquippableMock'); + const renderUtilsFactory = await ethers.getContractFactory('RMRKRenderUtils'); + + const token = await equipFactory.deploy('Kanaria', 'KAN'); + token.deployed(); + + const renderUtils = await renderUtilsFactory.deploy(); + await renderUtils.deployed(); + + return { token, renderUtils }; +} + +describe('MultiAsset and Equip Render Utils', async function () { let owner: SignerWithAddress; let catalog: RMRKCatalogMock; let equip: RMRKEquippableMock; @@ -66,7 +80,9 @@ describe('Render Utils', async function () { const resId4 = bn(4); beforeEach(async function () { - ({ catalog, equip, renderUtils, renderUtilsEquip } = await loadFixture(assetsFixture)); + ({ catalog, equip, renderUtils, renderUtilsEquip } = await loadFixture( + multiAsetAndEquipRenderUtilsFixture, + )); const signers = await ethers.getSigners(); owner = signers[0]; @@ -88,7 +104,7 @@ describe('Render Utils', async function () { describe('Render Utils MultiAsset', async function () { it('can get active assets', async function () { - expect(await renderUtils.getActiveAssets(equip.address, tokenId)).to.eql([ + expect(await renderUtils.getExtendedActiveAssets(equip.address, tokenId)).to.eql([ [resId, 10, 'ipfs://res1.jpg'], [resId2, 5, 'ipfs://res2.jpg'], ]); @@ -122,10 +138,10 @@ describe('Render Utils', async function () { it('cannot get active assets if token has no assets', async function () { const otherTokenId = await mintFromMock(equip, owner.address); await expect( - renderUtils.getActiveAssets(equip.address, otherTokenId), + renderUtils.getExtendedActiveAssets(equip.address, otherTokenId), ).to.be.revertedWithCustomError(renderUtils, 'RMRKTokenHasNoAssets'); await expect( - renderUtilsEquip.getExtendedActiveAssets(equip.address, otherTokenId), + renderUtilsEquip.getExtendedEquippableActiveAssets(equip.address, otherTokenId), ).to.be.revertedWithCustomError(renderUtils, 'RMRKTokenHasNoAssets'); }); @@ -149,7 +165,9 @@ describe('Render Utils', async function () { describe('Render Utils Equip', async function () { it('can get active assets', async function () { - expect(await renderUtilsEquip.getExtendedActiveAssets(equip.address, tokenId)).to.eql([ + expect( + await renderUtilsEquip.getExtendedEquippableActiveAssets(equip.address, tokenId), + ).to.eql([ [resId, bn(0), 10, ADDRESS_ZERO, 'ipfs://res1.jpg', []], [resId2, bn(1), 5, catalog.address, 'ipfs://res2.jpg', [bn(1), bn(3), bn(4)]], ]); @@ -209,7 +227,9 @@ describe('Advanced Equip Render Utils', async function () { let gemId3: number; beforeEach(async function () { - ({ catalog, kanaria, gem, renderUtilsEquip } = await loadFixture(advancedAssetsFixture)); + ({ catalog, kanaria, gem, renderUtilsEquip } = await loadFixture( + advancedEquipRenderUtilsFixture, + )); [owner] = await ethers.getSigners(); kanariaId = await mintFromMock(kanaria, owner.address); @@ -428,3 +448,37 @@ async function setUpGemAssets( await gem.acceptAsset(gemId3, 1, assetForGemBLeft); await gem.acceptAsset(gemId3, 0, assetForGemBFull); } + +describe('Render Utils', async function () { + let owner: SignerWithAddress; + let token: RMRKEquippableMock; + let renderUtils: RMRKRenderUtils; + + beforeEach(async function () { + ({ token, renderUtils } = await loadFixture(simpleRenderUtilsFixture)); + + const signers = await ethers.getSigners(); + owner = signers[0]; + }); + + it('can get pages of available ids', async function () { + for (let i = 0; i < 9; i++) { + await token.mint(owner.address, i + 1); + } + + await token['burn(uint256)'](3); + await token['burn(uint256)'](8); + + expect(await renderUtils.getPaginatedMintedIds(token.address, 1, 5)).to.eql([ + bn(1), + bn(2), + bn(4), + bn(5), + ]); + expect(await renderUtils.getPaginatedMintedIds(token.address, 6, 10)).to.eql([ + bn(6), + bn(7), + bn(9), + ]); + }); +}); From 8ac48a6f9bf1a9564ecb067238d0d1552ea012f0 Mon Sep 17 00:00:00 2001 From: Steven Pineda Date: Wed, 18 Jan 2023 08:29:08 -0500 Subject: [PATCH 18/26] Apply suggestions from code review Co-authored-by: Jan Turk --- contracts/RMRK/library/RMRKErrors.sol | 2 +- contracts/RMRK/utils/RMRKEquipRenderUtils.sol | 14 +++++++------- contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol | 3 ++- contracts/RMRK/utils/RMRKRenderUtils.sol | 14 +++++++------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/contracts/RMRK/library/RMRKErrors.sol b/contracts/RMRK/library/RMRKErrors.sol index c5eec467..7936122c 100644 --- a/contracts/RMRK/library/RMRKErrors.sol +++ b/contracts/RMRK/library/RMRKErrors.sol @@ -105,7 +105,7 @@ error RMRKNotOwnerOrContributor(); error RMRKNewOwnerIsZeroAddress(); /// Attempting to assign a 0x0 address as a contributor error RMRKNewContributorIsZeroAddress(); -/// Attempting do an operation assuming a token is nested, while it is not +/// Attempting an operation requiring the token being nested, while it is not error RMRKParentIsNotNFT(); /// Attempting to add a `Part` with an ID that is already used error RMRKPartAlreadyExists(); diff --git a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol index b4e5a9c6..3a043495 100644 --- a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol @@ -114,7 +114,7 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { * ] * @param target Address of the smart contract of the given token * @param tokenId ID of the token to retrieve the extended active assets for - * @return ExtendedActiveAssets[] An array of ExtendedActiveAssets present on the given token + * @return ExtendedEquippableActiveAsset[] An array of ExtendedEquippableActiveAssets present on the given token */ function getExtendedEquippableActiveAssets( address target, @@ -363,7 +363,7 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { } /** - * @notice Used to get the child's assets and slot parts pairs, for which the child asset can be equipped into parent's slot part. + * @notice Used to get the child's assets and slot parts pairs, identifying parts the said assets can be equipped into. * @dev The full `AssetWithSlot` struct looks like this: * [ * assetId, @@ -433,9 +433,9 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { /** * @notice Used to retrieve the equippable data of the specified token's asset with the highest priority. - * @param target Address of the smart contract of the given token + * @param target Address of the collection smart contract of the specified token * @param tokenId ID of the token for which to retrieve the equippable data of the asset with the highest priority - * @return topAsset ExtendedEquippableActiveAsset struct with the equippable data for the the asset with the highest priority + * @return topAsset `ExtendedEquippableActiveAsset` struct with the equippable data containing the asset with the highest priority */ function getTopAssetAndEquippableDataForToken( address target, @@ -465,12 +465,12 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { } /** - * @notice Used to retrieve the parent address and is slot part ids for a given target child. - * @param target Address of the smart contract of the given token + * @notice Used to retrieve the parent address and its slot part IDs for a given target child. + * @param target Address of the collection smart contract of the given token * @param tokenId ID of the child token * @param parentAssetId ID of the parent asset from which to get the slot parts * @return parentAddress Address of the parent token owning the target child - * @return parentSlotPartIds Array of slot part ids of the parent asset + * @return parentSlotPartIds Array of slot part IDs of the parent token's asset * @dev Reverts if the parent is not an NFT or if the parent asset is not composable. */ function _getParentAndSlotParts( diff --git a/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol b/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol index 7a3a7c02..f2edb678 100644 --- a/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKMultiAssetRenderUtils.sol @@ -180,7 +180,8 @@ contract RMRKMultiAssetRenderUtils { * @notice Used to retrieve the ID of the specified token's asset with the highest priority. * @param target Address of the smart contract of the given token * @param tokenId ID of the token for which to retrieve the ID of the asset with the highest priority - * @return string The ID of the asset with the highest priority + * @return The ID of the asset with the highest priority + * @return The priority value of the asset with the highest priority */ function getAssetIdWithTopPriority( address target, diff --git a/contracts/RMRK/utils/RMRKRenderUtils.sol b/contracts/RMRK/utils/RMRKRenderUtils.sol index 0f4a6897..2c2ce4b6 100644 --- a/contracts/RMRK/utils/RMRKRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKRenderUtils.sol @@ -14,13 +14,13 @@ import "../library/RMRKErrors.sol"; */ contract RMRKRenderUtils { /** - * @notice Used to get a list of existing token Ids in the range given by pageStart and pageSize - * @dev It does not optimize to avoid checking Ids out of max supply nor total supply. - * @dev The resulting array might be smaller than the given pageSize since not existing Ids are not included. - * @param target Address of the smart contract of the given token - * @param pageStart The first Id to check - * @param pageSize The number of Ids to check - * @return page An array of existing token Ids + * @notice Used to get a list of existing token IDs in the range between `pageStart` and `pageSize`. + * @dev It is not optimized to avoid checking IDs out of max supply nor total supply, since this is not meant to be used during transaction execution; it is only meant to be used as a getter. + * @dev The resulting array might be smaller than the given `pageSize` since no-existent IDs are not included. + * @param target Address of the collection smart contract of the given token + * @param pageStart The first ID to check + * @param pageSize The number of IDs to check + * @return page An array of IDs of the existing tokens */ function getPaginatedMintedIds( address target, From 5d190b9770b4421e0f6876db31cc2fd46c16d713 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Wed, 18 Jan 2023 09:49:06 -0500 Subject: [PATCH 19/26] Minor updates. --- contracts/RMRK/utils/RMRKEquipRenderUtils.sol | 25 +++++++++------ docs/RMRK/utils/RMRKEquipRenderUtils.md | 32 +++++++++---------- docs/RMRK/utils/RMRKMultiAssetRenderUtils.md | 4 +-- docs/RMRK/utils/RMRKRenderUtils.md | 12 +++---- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol index 3a043495..7d8ac44c 100644 --- a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol @@ -89,6 +89,11 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { string metadataURI; //n bytes 32+ } + /** + * @notice The structure used to represent an asset with a Slot. + * @return slotPartId ID of the Slot part + * @return assetId ID of the asset + */ struct AssetWithSlot { uint64 slotPartId; uint64 assetId; @@ -369,23 +374,23 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { * assetId, * slotPartId * ] - * @param target Address of the smart contract of the given token - * @param tokenId ID of the child token whose assets will be matched against parent's slot parts + * @param targetChild Address of the smart contract of the given token + * @param childId ID of the child token whose assets will be matched against parent's slot parts * @param parentAssetId ID of the target parent asset to use to equip the child * @return assetsWithSlots An array of `AssetWithSlot` structs containing info about the equippable child assets and their corresponding slot parts */ function getEquippableSlotsFromParent( - address target, - uint256 tokenId, + address targetChild, + uint256 childId, uint64 parentAssetId ) public view returns (AssetWithSlot[] memory assetsWithSlots) { ( address parentAddress, uint64[] memory parentSlotPartIds - ) = _getParentAndSlotParts(target, tokenId, parentAssetId); + ) = _getParentAndSlotParts(targetChild, childId, parentAssetId); - IRMRKEquippable targetChild = IRMRKEquippable(target); - uint64[] memory childAssets = targetChild.getActiveAssets(tokenId); + IRMRKEquippable targetChild_ = IRMRKEquippable(targetChild); + uint64[] memory childAssets = targetChild_.getActiveAssets(childId); uint256 totalChildAssets = childAssets.length; uint256 totalParentSlots = parentSlotPartIds.length; // There can be at most min(totalChildAssets, totalParentSlots) resulting matches, we just pick one of them. @@ -397,9 +402,9 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { for (uint256 i; i < totalChildAssets; ) { for (uint256 j; j < totalParentSlots; ) { if ( - targetChild.canTokenBeEquippedWithAssetIntoSlot( + targetChild_.canTokenBeEquippedWithAssetIntoSlot( parentAddress, - tokenId, + childId, childAssets[i], parentSlotPartIds[j] ) @@ -466,12 +471,12 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { /** * @notice Used to retrieve the parent address and its slot part IDs for a given target child. + * @dev Reverts if the parent is not an NFT or if the parent asset is not composable. * @param target Address of the collection smart contract of the given token * @param tokenId ID of the child token * @param parentAssetId ID of the parent asset from which to get the slot parts * @return parentAddress Address of the parent token owning the target child * @return parentSlotPartIds Array of slot part IDs of the parent token's asset - * @dev Reverts if the parent is not an NFT or if the parent asset is not composable. */ function _getParentAndSlotParts( address target, diff --git a/docs/RMRK/utils/RMRKEquipRenderUtils.md b/docs/RMRK/utils/RMRKEquipRenderUtils.md index df3c3106..3cbc67b2 100644 --- a/docs/RMRK/utils/RMRKEquipRenderUtils.md +++ b/docs/RMRK/utils/RMRKEquipRenderUtils.md @@ -83,8 +83,8 @@ Used to retrieve the ID of the specified token's asset with the highest prio | Name | Type | Description | |---|---|---| -| _0 | uint64 | string The ID of the asset with the highest priority | -| _1 | uint16 | undefined | +| _0 | uint64 | The ID of the asset with the highest priority | +| _1 | uint16 | The priority value of the asset with the highest priority | ### getAssetsById @@ -113,10 +113,10 @@ Used to retrieve the metadata URI of specified assets in the specified token. ### getEquippableSlotsFromParent ```solidity -function getEquippableSlotsFromParent(address target, uint256 tokenId, uint64 parentAssetId) external view returns (struct RMRKEquipRenderUtils.AssetWithSlot[] assetsWithSlots) +function getEquippableSlotsFromParent(address targetChild, uint256 childId, uint64 parentAssetId) external view returns (struct RMRKEquipRenderUtils.AssetWithSlot[] assetsWithSlots) ``` -Used to get the child's assets and slot parts pairs, for which the child asset can be equipped into parent's slot part. +Used to get the child's assets and slot parts pairs, identifying parts the said assets can be equipped into. *The full `AssetWithSlot` struct looks like this: [ assetId, slotPartId ]* @@ -124,8 +124,8 @@ Used to get the child's assets and slot parts pairs, for which the child ass | Name | Type | Description | |---|---|---| -| target | address | Address of the smart contract of the given token | -| tokenId | uint256 | ID of the child token whose assets will be matched against parent's slot parts | +| targetChild | address | Address of the smart contract of the given token | +| childId | uint256 | ID of the child token whose assets will be matched against parent's slot parts | | parentAssetId | uint64 | ID of the target parent asset to use to equip the child | #### Returns @@ -204,7 +204,7 @@ Used to get extended active assets of the given token. | Name | Type | Description | |---|---|---| -| _0 | RMRKEquipRenderUtils.ExtendedEquippableActiveAsset[] | ExtendedActiveAssets[] An array of ExtendedActiveAssets present on the given token | +| _0 | RMRKEquipRenderUtils.ExtendedEquippableActiveAsset[] | ExtendedEquippableActiveAsset[] An array of ExtendedEquippableActiveAssets present on the given token | ### getExtendedPendingAssets @@ -235,23 +235,23 @@ Used to get the extended pending assets of the given token. function getPaginatedMintedIds(address target, uint256 pageStart, uint256 pageSize) external view returns (uint256[] page) ``` -Used to get a list of existing token Ids in the range given by pageStart and pageSize +Used to get a list of existing token IDs in the range between `pageStart` and `pageSize`. -*It does not optimize to avoid checking Ids out of max supply nor total supply.The resulting array might be smaller than the given pageSize since not existing Ids are not included.* +*It is not optimized to avoid checking IDs out of max supply nor total supply, since this is not meant to be used during transaction execution; it is only meant to be used as a getter.The resulting array might be smaller than the given `pageSize` since no-existent IDs are not included.* #### Parameters | Name | Type | Description | |---|---|---| -| target | address | Address of the smart contract of the given token | -| pageStart | uint256 | The first Id to check | -| pageSize | uint256 | The number of Ids to check | +| target | address | Address of the collection smart contract of the given token | +| pageStart | uint256 | The first ID to check | +| pageSize | uint256 | The number of IDs to check | #### Returns | Name | Type | Description | |---|---|---| -| page | uint256[] | An array of existing token Ids | +| page | uint256[] | An array of IDs of the existing tokens | ### getPendingAssets @@ -290,14 +290,14 @@ Used to retrieve the equippable data of the specified token's asset with the | Name | Type | Description | |---|---|---| -| target | address | Address of the smart contract of the given token | +| target | address | Address of the collection smart contract of the specified token | | tokenId | uint256 | ID of the token for which to retrieve the equippable data of the asset with the highest priority | #### Returns | Name | Type | Description | |---|---|---| -| topAsset | RMRKEquipRenderUtils.ExtendedEquippableActiveAsset | ExtendedEquippableActiveAsset struct with the equippable data for the the asset with the highest priority | +| topAsset | RMRKEquipRenderUtils.ExtendedEquippableActiveAsset | `ExtendedEquippableActiveAsset` struct with the equippable data containing the asset with the highest priority | ### getTopAssetMetaForToken @@ -344,7 +344,7 @@ Attempting to compose an asset wihtout having an associated Catalog error RMRKParentIsNotNFT() ``` -Attempting do an operation assuming a token is nested, while it is not +Attempting an operation requiring the token being nested, while it is not diff --git a/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md b/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md index 640edca2..dec6547c 100644 --- a/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md +++ b/docs/RMRK/utils/RMRKMultiAssetRenderUtils.md @@ -31,8 +31,8 @@ Used to retrieve the ID of the specified token's asset with the highest prio | Name | Type | Description | |---|---|---| -| _0 | uint64 | string The ID of the asset with the highest priority | -| _1 | uint16 | undefined | +| _0 | uint64 | The ID of the asset with the highest priority | +| _1 | uint16 | The priority value of the asset with the highest priority | ### getAssetsById diff --git a/docs/RMRK/utils/RMRKRenderUtils.md b/docs/RMRK/utils/RMRKRenderUtils.md index 4f12f277..c0707c9f 100644 --- a/docs/RMRK/utils/RMRKRenderUtils.md +++ b/docs/RMRK/utils/RMRKRenderUtils.md @@ -16,23 +16,23 @@ Smart contract of the RMRK render utils module. function getPaginatedMintedIds(address target, uint256 pageStart, uint256 pageSize) external view returns (uint256[] page) ``` -Used to get a list of existing token Ids in the range given by pageStart and pageSize +Used to get a list of existing token IDs in the range between `pageStart` and `pageSize`. -*It does not optimize to avoid checking Ids out of max supply nor total supply.The resulting array might be smaller than the given pageSize since not existing Ids are not included.* +*It is not optimized to avoid checking IDs out of max supply nor total supply, since this is not meant to be used during transaction execution; it is only meant to be used as a getter.The resulting array might be smaller than the given `pageSize` since no-existent IDs are not included.* #### Parameters | Name | Type | Description | |---|---|---| -| target | address | Address of the smart contract of the given token | -| pageStart | uint256 | The first Id to check | -| pageSize | uint256 | The number of Ids to check | +| target | address | Address of the collection smart contract of the given token | +| pageStart | uint256 | The first ID to check | +| pageSize | uint256 | The number of IDs to check | #### Returns | Name | Type | Description | |---|---|---| -| page | uint256[] | An array of existing token Ids | +| page | uint256[] | An array of IDs of the existing tokens | From 95904ae6600321b2a4a2f6e5beb10579b2e03320 Mon Sep 17 00:00:00 2001 From: steven2308 Date: Wed, 18 Jan 2023 11:27:40 -0500 Subject: [PATCH 20/26] Updates Equip render utils: getEquippableSlotsFromParent It now receives the expected parent and checks against it It now includes priority in the matched assets with slots It now returns the childIndex from the parent's array perspective. --- contracts/RMRK/library/RMRKErrors.sol | 4 + contracts/RMRK/utils/RMRKEquipRenderUtils.sol | 187 ++++++++++++------ .../RMRK/utils/RMRKNestableRenderUtils.sol | 60 ++++++ docs/RMRK/utils/RMRKEquipRenderUtils.md | 73 ++++++- docs/RMRK/utils/RMRKNestableRenderUtils.md | 95 +++++++++ test/renderUtils.ts | 82 ++++++-- 6 files changed, 426 insertions(+), 75 deletions(-) create mode 100644 contracts/RMRK/utils/RMRKNestableRenderUtils.sol create mode 100644 docs/RMRK/utils/RMRKNestableRenderUtils.md diff --git a/contracts/RMRK/library/RMRKErrors.sol b/contracts/RMRK/library/RMRKErrors.sol index 7936122c..df8b84f0 100644 --- a/contracts/RMRK/library/RMRKErrors.sol +++ b/contracts/RMRK/library/RMRKErrors.sol @@ -48,6 +48,8 @@ error RMRKCannotTransferSoulbound(); error RMRKChildAlreadyExists(); /// Attempting to interact with a child, using index that is higher than the number of children error RMRKChildIndexOutOfRange(); +/// Attempting to find the index of a child token on a parent which does not own it. +error RMRKChildNotFoundInParent(); /// Attempting to equip a `Part` with a child not approved by the Catalog error RMRKEquippableEquipNotAllowedByCatalog(); /// Attempting to use ID 0, which is not supported @@ -135,5 +137,7 @@ error RMRKUnexpectedNumberOfAssets(); error RMRKUnexpectedNumberOfChildren(); /// Attempting to accept or reject an asset which does not match the one at the specified index error RMRKUnexpectedAssetId(); +/// Attempting an operation expecting a parent to the token which is not the actual one +error RMRKUnexpectedParent(); /// Attempting not to pass an empty array of equippable addresses when adding or setting the equippable addresses error RMRKZeroLengthIdsPassed(); diff --git a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol index 7d8ac44c..192dbe15 100644 --- a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.16; import "./RMRKRenderUtils.sol"; import "./RMRKMultiAssetRenderUtils.sol"; +import "./RMRKNestableRenderUtils.sol"; import "../catalog/IRMRKCatalog.sol"; import "../equippable/IRMRKEquippable.sol"; import "../nestable/IRMRKNestable.sol"; @@ -16,7 +17,11 @@ import "../library/RMRKErrors.sol"; * @notice Smart contract of the RMRK Equip render utils module. * @dev Extra utility functions for composing RMRK extended assets. */ -contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { +contract RMRKEquipRenderUtils is + RMRKRenderUtils, + RMRKMultiAssetRenderUtils, + RMRKNestableRenderUtils +{ using RMRKLib for uint64[]; /** @@ -93,10 +98,12 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { * @notice The structure used to represent an asset with a Slot. * @return slotPartId ID of the Slot part * @return assetId ID of the asset + * @return priority Priority of the asset on the active assets list */ struct AssetWithSlot { uint64 slotPartId; uint64 assetId; + uint16 priority; } /** @@ -369,62 +376,54 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { /** * @notice Used to get the child's assets and slot parts pairs, identifying parts the said assets can be equipped into. + * @dev Reverts if child token is not owned by an NFT. + * @dev Reverts if child token is not owned by the expected parent. * @dev The full `AssetWithSlot` struct looks like this: * [ * assetId, - * slotPartId + * slotPartId, + * priority, * ] * @param targetChild Address of the smart contract of the given token * @param childId ID of the child token whose assets will be matched against parent's slot parts * @param parentAssetId ID of the target parent asset to use to equip the child + * @return childIndex Index of the child in the parent's list of active children * @return assetsWithSlots An array of `AssetWithSlot` structs containing info about the equippable child assets and their corresponding slot parts */ function getEquippableSlotsFromParent( address targetChild, uint256 childId, + address expectedParent, + uint256 expectedParentId, uint64 parentAssetId - ) public view returns (AssetWithSlot[] memory assetsWithSlots) { - ( - address parentAddress, - uint64[] memory parentSlotPartIds - ) = _getParentAndSlotParts(targetChild, childId, parentAssetId); - - IRMRKEquippable targetChild_ = IRMRKEquippable(targetChild); - uint64[] memory childAssets = targetChild_.getActiveAssets(childId); - uint256 totalChildAssets = childAssets.length; - uint256 totalParentSlots = parentSlotPartIds.length; - // There can be at most min(totalChildAssets, totalParentSlots) resulting matches, we just pick one of them. - AssetWithSlot[] memory tempAssetsWithSlots = new AssetWithSlot[]( - totalParentSlots + ) + public + view + returns (uint256 childIndex, AssetWithSlot[] memory assetsWithSlots) + { + uint64[] memory parentSlotPartIds = _getParentAndSlotParts( + targetChild, + childId, + expectedParent, + expectedParentId, + parentAssetId + ); + childIndex = getChildIndex( + expectedParent, + expectedParentId, + targetChild, + childId ); - uint256 totalMatches; - for (uint256 i; i < totalChildAssets; ) { - for (uint256 j; j < totalParentSlots; ) { - if ( - targetChild_.canTokenBeEquippedWithAssetIntoSlot( - parentAddress, - childId, - childAssets[i], - parentSlotPartIds[j] - ) - ) { - tempAssetsWithSlots[totalMatches] = AssetWithSlot({ - assetId: childAssets[i], - slotPartId: parentSlotPartIds[j] - }); - unchecked { - ++totalMatches; - } - } - unchecked { - ++j; - } - } - unchecked { - ++i; - } - } + ( + AssetWithSlot[] memory tempAssetsWithSlots, + uint256 totalMatches + ) = _matchAllAssetsWithSlots( + IRMRKEquippable(targetChild), + childId, + parentSlotPartIds, + expectedParent + ); // Finally, we copy the matches into the final array which has the right lenght according to results assetsWithSlots = new AssetWithSlot[](totalMatches); @@ -469,38 +468,106 @@ contract RMRKEquipRenderUtils is RMRKRenderUtils, RMRKMultiAssetRenderUtils { }); } + /** + * @notice Matches all child's assets with the corresponding slot parts of the parent, if they apply. + * @dev The full `AssetWithSlot` struct looks like this: + * [ + * assetId, + * slotPartId, + * priority, + * ] + * @dev The size of the returning array is equal to the total of available parent slots, even if there's not a match for each one. + * @dev The valid matches are located at the beginning of the array, and the rest of the slots are filled with empty structs. Use totalMatches to know how many valid matches there are. + * @param childContract IRMRKEquippable instance of the child smart contract + * @param childId ID of the child token whose assets will be matched against parent's slot parts + * @param parentSlotPartIds Array of slot part IDs of the parent token's asset + * @param parentAddress Address of the parent smart contract + * @return allAssetsWithSlots An array of `AssetWithSlot` structs containing info about the equippable child assets and their corresponding slot parts + * @return totalMatches Total of valid matches found + */ + function _matchAllAssetsWithSlots( + IRMRKEquippable childContract, + uint256 childId, + uint64[] memory parentSlotPartIds, + address parentAddress + ) + private + view + returns ( + AssetWithSlot[] memory allAssetsWithSlots, + uint256 totalMatches + ) + { + uint64[] memory childAssets = childContract.getActiveAssets(childId); + uint16[] memory priorities = childContract.getActiveAssetPriorities( + childId + ); + + uint256 totalChildAssets = childAssets.length; + uint256 totalParentSlots = parentSlotPartIds.length; + + // There can be at most min(totalChildAssets, totalParentSlots) resulting matches, we just pick one of them. + allAssetsWithSlots = new AssetWithSlot[](totalParentSlots); + + for (uint256 i; i < totalChildAssets; ) { + for (uint256 j; j < totalParentSlots; ) { + if ( + childContract.canTokenBeEquippedWithAssetIntoSlot( + parentAddress, + childId, + childAssets[i], + parentSlotPartIds[j] + ) + ) { + allAssetsWithSlots[totalMatches] = AssetWithSlot({ + assetId: childAssets[i], + slotPartId: parentSlotPartIds[j], + priority: priorities[i] + }); + unchecked { + ++totalMatches; + } + } + unchecked { + ++j; + } + } + unchecked { + ++i; + } + } + } + /** * @notice Used to retrieve the parent address and its slot part IDs for a given target child. * @dev Reverts if the parent is not an NFT or if the parent asset is not composable. - * @param target Address of the collection smart contract of the given token - * @param tokenId ID of the child token + * @param targetChild Address of the collection smart contract of the given token + * @param childId ID of the child token * @param parentAssetId ID of the parent asset from which to get the slot parts - * @return parentAddress Address of the parent token owning the target child + * @param expectedParent ID of the parent asset from which to get the slot parts + * @param expectedParentId ID of the parent asset from which to get the slot parts * @return parentSlotPartIds Array of slot part IDs of the parent token's asset */ function _getParentAndSlotParts( - address target, - uint256 tokenId, + address targetChild, + uint256 childId, + address expectedParent, + uint256 expectedParentId, uint64 parentAssetId - ) - private - view - returns (address parentAddress, uint64[] memory parentSlotPartIds) - { - uint256 parentId; - bool isNFT; - (parentAddress, parentId, isNFT) = IRMRKNestable(target).directOwnerOf( - tokenId + ) private view returns (uint64[] memory parentSlotPartIds) { + checkExpectedParent( + targetChild, + childId, + expectedParent, + expectedParentId ); - if (!isNFT) revert RMRKParentIsNotNFT(); - ( , , address catalogAddress, uint64[] memory parentPartIds - ) = IRMRKEquippable(parentAddress).getAssetAndEquippableData( - parentId, + ) = IRMRKEquippable(expectedParent).getAssetAndEquippableData( + expectedParentId, parentAssetId ); if (catalogAddress == address(0)) revert RMRKNotComposableAsset(); diff --git a/contracts/RMRK/utils/RMRKNestableRenderUtils.sol b/contracts/RMRK/utils/RMRKNestableRenderUtils.sol new file mode 100644 index 00000000..acfb394a --- /dev/null +++ b/contracts/RMRK/utils/RMRKNestableRenderUtils.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.16; + +import "../nestable/IRMRKNestable.sol"; +import "../library/RMRKErrors.sol"; + +/** + * @title RMRKNestableRenderUtils + * @author RMRK team + * @notice Smart contract of the RMRK Nestable render utils module. + */ +contract RMRKNestableRenderUtils { + function getChildIndex( + address parentAddress, + uint256 parentId, + address childAddress, + uint256 childId + ) public view returns (uint256) { + IRMRKNestable.Child[] memory children = IRMRKNestable(parentAddress) + .childrenOf(parentId); + (parentId); + uint256 len = children.length; + for (uint256 i; i < len; ) { + if ( + children[i].tokenId == childId && + children[i].contractAddress == childAddress + ) return i; + unchecked { + ++i; + } + } + revert RMRKChildNotFoundInParent(); + } + + /** + * @notice Check if the child is owned by the expected parent. + * @dev Reverts if child token is not owned by an NFT. + * @dev Reverts if child token is not owned by the expected parent. + * @param childAddress Address of the child contract. + * @param childId ID of the child token. + * @param expectedParent Address of the expected parent contract. + * @param expectedParentId ID of the expected parent token. + */ + function checkExpectedParent( + address childAddress, + uint256 childId, + address expectedParent, + uint256 expectedParentId + ) public view { + address parentAddress; + uint256 parentId; + bool isNFT; + (parentAddress, parentId, isNFT) = IRMRKNestable(childAddress) + .directOwnerOf(childId); + if (!isNFT) revert RMRKParentIsNotNFT(); + if (parentAddress != expectedParent || expectedParentId != parentId) + revert RMRKUnexpectedParent(); + } +} diff --git a/docs/RMRK/utils/RMRKEquipRenderUtils.md b/docs/RMRK/utils/RMRKEquipRenderUtils.md index 3cbc67b2..96a75c4a 100644 --- a/docs/RMRK/utils/RMRKEquipRenderUtils.md +++ b/docs/RMRK/utils/RMRKEquipRenderUtils.md @@ -34,6 +34,25 @@ Used to split slot and fixed parts. | slotPartIds | uint64[] | An array of IDs of the `Slot` parts included in the `allPartIds` | | fixedPartIds | uint64[] | An array of IDs of the `Fixed` parts included in the `allPartIds` | +### checkExpectedParent + +```solidity +function checkExpectedParent(address childAddress, uint256 childId, address expectedParent, uint256 expectedParentId) external view +``` + +Check if the child is owned by the expected parent. + +*Reverts if child token is not owned by an NFT.Reverts if child token is not owned by the expected parent.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| childAddress | address | Address of the child contract. | +| childId | uint256 | ID of the child token. | +| expectedParent | address | Address of the expected parent contract. | +| expectedParentId | uint256 | ID of the expected parent token. | + ### composeEquippables ```solidity @@ -110,15 +129,40 @@ Used to retrieve the metadata URI of specified assets in the specified token. |---|---|---| | _0 | string[] | string[] An array of metadata URIs belonging to specified assets | +### getChildIndex + +```solidity +function getChildIndex(address parentAddress, uint256 parentId, address childAddress, uint256 childId) external view returns (uint256) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| parentAddress | address | undefined | +| parentId | uint256 | undefined | +| childAddress | address | undefined | +| childId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + ### getEquippableSlotsFromParent ```solidity -function getEquippableSlotsFromParent(address targetChild, uint256 childId, uint64 parentAssetId) external view returns (struct RMRKEquipRenderUtils.AssetWithSlot[] assetsWithSlots) +function getEquippableSlotsFromParent(address targetChild, uint256 childId, address expectedParent, uint256 expectedParentId, uint64 parentAssetId) external view returns (uint256 childIndex, struct RMRKEquipRenderUtils.AssetWithSlot[] assetsWithSlots) ``` Used to get the child's assets and slot parts pairs, identifying parts the said assets can be equipped into. -*The full `AssetWithSlot` struct looks like this: [ assetId, slotPartId ]* +*Reverts if child token is not owned by an NFT.Reverts if child token is not owned by the expected parent.The full `AssetWithSlot` struct looks like this: [ assetId, slotPartId, priority, ]* #### Parameters @@ -126,12 +170,15 @@ Used to get the child's assets and slot parts pairs, identifying parts the s |---|---|---| | targetChild | address | Address of the smart contract of the given token | | childId | uint256 | ID of the child token whose assets will be matched against parent's slot parts | +| expectedParent | address | undefined | +| expectedParentId | uint256 | undefined | | parentAssetId | uint64 | ID of the target parent asset to use to equip the child | #### Returns | Name | Type | Description | |---|---|---| +| childIndex | uint256 | Index of the child in the parent's list of active children | | assetsWithSlots | RMRKEquipRenderUtils.AssetWithSlot[] | An array of `AssetWithSlot` structs containing info about the equippable child assets and their corresponding slot parts | ### getEquipped @@ -327,6 +374,17 @@ Used to retrieve the metadata URI of the specified token's asset with the hi ## Errors +### RMRKChildNotFoundInParent + +```solidity +error RMRKChildNotFoundInParent() +``` + +Attempting to find the index of a child token on a parent which does not own it. + + + + ### RMRKNotComposableAsset ```solidity @@ -360,4 +418,15 @@ Attempting to determine the asset with the top priority on a token without asset +### RMRKUnexpectedParent + +```solidity +error RMRKUnexpectedParent() +``` + +Attempting an operation expecting a parent to the token which is not the actual one + + + + diff --git a/docs/RMRK/utils/RMRKNestableRenderUtils.md b/docs/RMRK/utils/RMRKNestableRenderUtils.md new file mode 100644 index 00000000..97f20631 --- /dev/null +++ b/docs/RMRK/utils/RMRKNestableRenderUtils.md @@ -0,0 +1,95 @@ +# RMRKNestableRenderUtils + +*RMRK team* + +> RMRKNestableRenderUtils + +Smart contract of the RMRK Nestable render utils module. + + + +## Methods + +### checkExpectedParent + +```solidity +function checkExpectedParent(address childAddress, uint256 childId, address expectedParent, uint256 expectedParentId) external view +``` + +Check if the child is owned by the expected parent. + +*Reverts if child token is not owned by an NFT.Reverts if child token is not owned by the expected parent.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| childAddress | address | Address of the child contract. | +| childId | uint256 | ID of the child token. | +| expectedParent | address | Address of the expected parent contract. | +| expectedParentId | uint256 | ID of the expected parent token. | + +### getChildIndex + +```solidity +function getChildIndex(address parentAddress, uint256 parentId, address childAddress, uint256 childId) external view returns (uint256) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| parentAddress | address | undefined | +| parentId | uint256 | undefined | +| childAddress | address | undefined | +| childId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + + + + +## Errors + +### RMRKChildNotFoundInParent + +```solidity +error RMRKChildNotFoundInParent() +``` + +Attempting to find the index of a child token on a parent which does not own it. + + + + +### RMRKParentIsNotNFT + +```solidity +error RMRKParentIsNotNFT() +``` + +Attempting an operation requiring the token being nested, while it is not + + + + +### RMRKUnexpectedParent + +```solidity +error RMRKUnexpectedParent() +``` + +Attempting an operation expecting a parent to the token which is not the actual one + + + + + diff --git a/test/renderUtils.ts b/test/renderUtils.ts index 5f3f92e0..f934130d 100644 --- a/test/renderUtils.ts +++ b/test/renderUtils.ts @@ -188,7 +188,7 @@ describe('MultiAsset and Equip Render Utils', async function () { it('cannot get equippable slots from parent if parent is not an NFT', async function () { await expect( - renderUtilsEquip.getEquippableSlotsFromParent(equip.address, tokenId, 1), + renderUtilsEquip.getEquippableSlotsFromParent(equip.address, tokenId, equip.address, 1, 1), ).to.be.revertedWithCustomError(renderUtilsEquip, 'RMRKParentIsNotNFT'); }); }); @@ -236,6 +236,9 @@ describe('Advanced Equip Render Utils', async function () { gemId1 = await nestMintFromMock(gem, kanaria.address, kanariaId); gemId2 = await nestMintFromMock(gem, kanaria.address, kanariaId); gemId3 = await nestMintFromMock(gem, kanaria.address, kanariaId); + await kanaria.acceptChild(kanariaId, 0, gem.address, gemId1); + await kanaria.acceptChild(kanariaId, 1, gem.address, gemId2); + await kanaria.acceptChild(kanariaId, 0, gem.address, gemId3); }); it('can get equippable slots from parent', async function () { @@ -244,25 +247,55 @@ describe('Advanced Equip Render Utils', async function () { await setUpGemAssets(gem, gemId1, gemId2, gemId3, kanaria.address, catalog.address); expect( - await renderUtilsEquip.getEquippableSlotsFromParent(gem.address, gemId1, assetForKanariaFull), + await renderUtilsEquip.getEquippableSlotsFromParent( + gem.address, + gemId1, + kanaria.address, + kanariaId, + assetForKanariaFull, + ), ).to.eql([ - [bn(slotIdGemRight), bn(assetForGemARight)], - [bn(slotIdGemMid), bn(assetForGemAMid)], - [bn(slotIdGemLeft), bn(assetForGemALeft)], + bn(0), // child Index + [ + // [Slot Id, asset Id, Asset priority] + [bn(slotIdGemRight), bn(assetForGemARight), 0], + [bn(slotIdGemMid), bn(assetForGemAMid), 1], + [bn(slotIdGemLeft), bn(assetForGemALeft), 2], + ], ]); expect( - await renderUtilsEquip.getEquippableSlotsFromParent(gem.address, gemId2, assetForKanariaFull), + await renderUtilsEquip.getEquippableSlotsFromParent( + gem.address, + gemId2, + kanaria.address, + kanariaId, + assetForKanariaFull, + ), ).to.eql([ - [bn(slotIdGemRight), bn(assetForGemARight)], - [bn(slotIdGemMid), bn(assetForGemAMid)], - [bn(slotIdGemLeft), bn(assetForGemALeft)], + bn(1), // child Index + [ + // [Slot Id, asset Id, Asset priority] + [bn(slotIdGemRight), bn(assetForGemARight), 0], + [bn(slotIdGemMid), bn(assetForGemAMid), 1], + [bn(slotIdGemLeft), bn(assetForGemALeft), 2], + ], ]); expect( - await renderUtilsEquip.getEquippableSlotsFromParent(gem.address, gemId3, assetForKanariaFull), + await renderUtilsEquip.getEquippableSlotsFromParent( + gem.address, + gemId3, + kanaria.address, + kanariaId, + assetForKanariaFull, + ), ).to.eql([ - [bn(slotIdGemRight), bn(assetForGemBRight)], - [bn(slotIdGemMid), bn(assetForGemBMid)], - [bn(slotIdGemLeft), bn(assetForGemBLeft)], + bn(2), // child Index + [ + // [Slot Id, asset Id, Asset priority] + [bn(slotIdGemRight), bn(assetForGemBRight), 0], + [bn(slotIdGemMid), bn(assetForGemBMid), 1], + [bn(slotIdGemLeft), bn(assetForGemBLeft), 2], + ], ]); }); @@ -285,10 +318,33 @@ describe('Advanced Equip Render Utils', async function () { renderUtilsEquip.getEquippableSlotsFromParent( gem.address, gemId1, + kanaria.address, + kanariaId, assetForKanariaNotEquippable, ), ).to.be.revertedWithCustomError(renderUtilsEquip, 'RMRKNotComposableAsset'); }); + + it('cannot get equippable slots from parent if parent is not the expected one', async function () { + await expect( + renderUtilsEquip.getEquippableSlotsFromParent( + gem.address, + gemId1, + gem.address, // Wrong parent address + kanariaId, + assetForKanariaFull, + ), + ).to.be.revertedWithCustomError(renderUtilsEquip, 'RMRKUnexpectedParent'); + await expect( + renderUtilsEquip.getEquippableSlotsFromParent( + gem.address, + gemId1, + kanaria.address, + 2, // Wrong parent id + assetForKanariaFull, + ), + ).to.be.revertedWithCustomError(renderUtilsEquip, 'RMRKUnexpectedParent'); + }); }); async function setUpCatalog(catalog: RMRKCatalogMock, gemAddress: string): Promise { From 4a2ecd4491d8c355019d3eae236cf7836488027b Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Fri, 20 Jan 2023 01:40:38 +0100 Subject: [PATCH 21/26] Add getExtendedNft to RMRKRenderUtils --- contracts/RMRK/utils/RMRKRenderUtils.sol | 92 +++++++++ docs/RMRK/utils/RMRKEquipRenderUtils.md | 23 +++ docs/RMRK/utils/RMRKRenderUtils.md | 23 +++ test/renderUtils.ts | 245 +++++++++++++++++++++++ 4 files changed, 383 insertions(+) diff --git a/contracts/RMRK/utils/RMRKRenderUtils.sol b/contracts/RMRK/utils/RMRKRenderUtils.sol index 2c2ce4b6..0665c424 100644 --- a/contracts/RMRK/utils/RMRKRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKRenderUtils.sol @@ -3,8 +3,11 @@ pragma solidity ^0.8.16; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "../equippable/RMRKEquippable.sol"; +import "../extension/soulbound/RMRKSoulbound.sol"; import "../library/RMRKLib.sol"; import "../library/RMRKErrors.sol"; +import "./RMRKTokenURI.sol"; /** * @title RMRKRenderUtils @@ -13,6 +16,38 @@ import "../library/RMRKErrors.sol"; * @dev Extra utility functions for RMRK contracts. */ contract RMRKRenderUtils { + /** + * @notice Structure used to represent the extended NFT. + * @return directOwner Address of the direct owner of the token (smart contract if the token is nested, otherwise + * EOA) + * @return rootOwner Address of the root owner + * @return activeAssetCount Number of active assets present on the token + * @return pendingAssetcount Number of pending assets on the token + * @return priorities The array of priorities of the active asset + * @return name Name of the collection the token belongs to + * @return symbol Symbol of the collection the token belongs to + * @return activeChildrenNumber Number of active child tokens of the given token (only account for direct child + * tokens) + * @return isSoulbound Boolean value signifying whether the token is soulbound or not + * @return hasMultiAssetInterface Boolean value signifying whether the toke supports MultiAsset interface + * @return hasNestingInterface Boolean value signifying whether the toke supports Nestable interface + * @return hasEquippableInterface Boolean value signifying whether the toke supports Equippable interface + */ + struct ExtendedNft { + address directOwner; + address rootOwner; + uint256 activeAssetCount; + uint256 pendingAssetCount; + uint16[] priorities; + string name; + string symbol; + uint256 activeChildrenNumber; + bool isSoulbound; + bool hasMultiAssetInterface; + bool hasNestingInterface; + bool hasEquippableInterface; + } + /** * @notice Used to get a list of existing token IDs in the range between `pageStart` and `pageSize`. * @dev It is not optimized to avoid checking IDs out of max supply nor total supply, since this is not meant to be used during transaction execution; it is only meant to be used as a getter. @@ -56,4 +91,61 @@ contract RMRKRenderUtils { } } } + + /** + * @notice Used to get extended information about a specified token. + * @dev The full `ExtendedNft` struct looks like this: + * [ + * directOwner, + * rootOwner, + * activeAssetCount, + * pendingAssetCount + * priorities, + * name, + * symbol, + * activeChildrenNumber, + * isSoulbound, + * hasMultiAssetInterface, + * hasNestingInterface, + * hasEquippableInterface + * ] + * @param tokenId ID of the token for which to retireve the `ExtendedNft` struct + * @param targetCollection Address of the collection to which the specified token belongs to + * @return data The `ExtendedNft` struct containing the specified token's data + */ + function getExtendedNft( + uint256 tokenId, + address targetCollection + ) public view returns (ExtendedNft memory data) { + RMRKEquippable target = RMRKEquippable(targetCollection); + data.hasMultiAssetInterface = target.supportsInterface( + type(IRMRKMultiAsset).interfaceId + ); + data.hasNestingInterface = target.supportsInterface( + type(IRMRKNestable).interfaceId + ); + data.hasEquippableInterface = target.supportsInterface( + type(IRMRKEquippable).interfaceId + ); + if (data.hasNestingInterface) { + (data.directOwner, , ) = target.directOwnerOf(tokenId); + data.activeChildrenNumber = target.childrenOf(tokenId).length; + } + if (data.hasMultiAssetInterface) { + data.activeAssetCount = target.getActiveAssets(tokenId).length; + data.pendingAssetCount = target.getPendingAssets(tokenId).length; + data.priorities = target.getActiveAssetPriorities(tokenId); + } + if (target.supportsInterface(type(IRMRKSoulbound).interfaceId)) { + data.isSoulbound = IRMRKSoulbound(targetCollection).isSoulbound( + tokenId + ); + } + data.rootOwner = target.ownerOf(tokenId); + if (data.directOwner == address(0x0)) { + data.directOwner = data.rootOwner; + } + data.name = target.name(); + data.symbol = target.symbol(); + } } diff --git a/docs/RMRK/utils/RMRKEquipRenderUtils.md b/docs/RMRK/utils/RMRKEquipRenderUtils.md index 96a75c4a..a4546907 100644 --- a/docs/RMRK/utils/RMRKEquipRenderUtils.md +++ b/docs/RMRK/utils/RMRKEquipRenderUtils.md @@ -253,6 +253,29 @@ Used to get extended active assets of the given token. |---|---|---| | _0 | RMRKEquipRenderUtils.ExtendedEquippableActiveAsset[] | ExtendedEquippableActiveAsset[] An array of ExtendedEquippableActiveAssets present on the given token | +### getExtendedNft + +```solidity +function getExtendedNft(uint256 tokenId, address targetCollection) external view returns (struct RMRKRenderUtils.ExtendedNft data) +``` + +Used to get extended information about a specified token. + +*The full `ExtendedNft` struct looks like this: [ directOwner, rootOwner, activeAssetCount, pendingAssetCount priorities, name, symbol, activeChildrenNumber, isSoulbound, hasMultiAssetInterface, hasNestingInterface, hasEquippableInterface ]* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token for which to retireve the `ExtendedNft` struct | +| targetCollection | address | Address of the collection to which the specified token belongs to | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| data | RMRKRenderUtils.ExtendedNft | The `ExtendedNft` struct containing the specified token's data | + ### getExtendedPendingAssets ```solidity diff --git a/docs/RMRK/utils/RMRKRenderUtils.md b/docs/RMRK/utils/RMRKRenderUtils.md index c0707c9f..a438da4f 100644 --- a/docs/RMRK/utils/RMRKRenderUtils.md +++ b/docs/RMRK/utils/RMRKRenderUtils.md @@ -10,6 +10,29 @@ Smart contract of the RMRK render utils module. ## Methods +### getExtendedNft + +```solidity +function getExtendedNft(uint256 tokenId, address targetCollection) external view returns (struct RMRKRenderUtils.ExtendedNft data) +``` + +Used to get extended information about a specified token. + +*The full `ExtendedNft` struct looks like this: [ directOwner, rootOwner, activeAssetCount, pendingAssetCount priorities, name, symbol, activeChildrenNumber, isSoulbound, hasMultiAssetInterface, hasNestingInterface, hasEquippableInterface ]* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| tokenId | uint256 | ID of the token for which to retireve the `ExtendedNft` struct | +| targetCollection | address | Address of the collection to which the specified token belongs to | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| data | RMRKRenderUtils.ExtendedNft | The `ExtendedNft` struct containing the specified token's data | + ### getPaginatedMintedIds ```solidity diff --git a/test/renderUtils.ts b/test/renderUtils.ts index f934130d..a7e1ffcc 100644 --- a/test/renderUtils.ts +++ b/test/renderUtils.ts @@ -10,6 +10,11 @@ import { RMRKEquipRenderUtils, RMRKMultiAssetRenderUtils, } from '../typechain-types'; +import { RMRKMultiAssetMock } from '../typechain-types'; +import { RMRKNestableMock } from '../typechain-types'; +import { RMRKNestableMultiAssetMock } from '../typechain-types'; +import { RMRKSoulboundNestableMock } from '../typechain-types'; +import { connect } from 'http2'; // --------------- FIXTURES ----------------------- @@ -66,6 +71,51 @@ async function simpleRenderUtilsFixture() { return { token, renderUtils }; } +async function extendedNftRenderUtilsFixture() { + const multiAssetFactory = await ethers.getContractFactory('RMRKMultiAssetMock'); + const nestableFactory = await ethers.getContractFactory('RMRKNestableMock'); + const nestableSoulboundFactory = await ethers.getContractFactory('RMRKSoulboundNestableMock'); + const nestableMultiAssetFactory = await ethers.getContractFactory('RMRKNestableMultiAssetMock'); + const catalogFactory = await ethers.getContractFactory('RMRKCatalogMock'); + const equipFactory = await ethers.getContractFactory('RMRKEquippableMock'); + const renderUtilsFactory = await ethers.getContractFactory('RMRKRenderUtils'); + + const multiAsset = await multiAssetFactory.deploy('MultiAsset', 'MA'); + await multiAsset.deployed(); + + const nestable = await nestableFactory.deploy('Nestable', 'Ne'); + await nestable.deployed(); + + const nestableSoulbound = ( + await nestableSoulboundFactory.deploy('NestableSoulbound', 'NS') + ); + await nestableSoulbound.deployed(); + + const nestableMultiAsset = ( + await nestableMultiAssetFactory.deploy('NestableMultiAsset', 'NMA') + ); + await nestableMultiAsset.deployed(); + + const catalog = await catalogFactory.deploy('ipfs://catalog.json', 'misc'); + await catalog.deployed(); + + const equip = await equipFactory.deploy('Equippable', 'EQ'); + await equip.deployed(); + + const renderUtils = await renderUtilsFactory.deploy(); + await renderUtils.deployed(); + + return { + multiAsset, + nestable, + nestableSoulbound, + nestableMultiAsset, + catalog, + equip, + renderUtils, + }; +} + describe('MultiAsset and Equip Render Utils', async function () { let owner: SignerWithAddress; let catalog: RMRKCatalogMock; @@ -347,6 +397,201 @@ describe('Advanced Equip Render Utils', async function () { }); }); +describe('Extended NFT render utils', function () { + let issuer: SignerWithAddress; + let rootOwner: SignerWithAddress; + let multiAsset: RMRKMultiAssetMock; + let nestable: RMRKNestableMock; + let nestableSoulbound: RMRKSoulboundNestableMock; + let nestableMultiAsset: RMRKNestableMultiAssetMock; + let catalog: RMRKCatalogMock; + let equip: RMRKEquippableMock; + let renderUtils: RMRKRenderUtils; + + const metaURI = 'ipfs://meta'; + const supply = bn(10000); + + beforeEach(async function () { + ({ multiAsset, nestable, nestableSoulbound, nestableMultiAsset, catalog, equip, renderUtils } = + await loadFixture(extendedNftRenderUtilsFixture)); + + [issuer, rootOwner] = await ethers.getSigners(); + }); + + it('renders correct data for MultiAsset', async function () { + const tokenId = await mintFromMock(multiAsset, rootOwner.address); + await multiAsset.addAssetEntry(1, metaURI); + await multiAsset.addAssetEntry(2, metaURI); + await multiAsset.addAssetEntry(3, metaURI); + await multiAsset.addAssetEntry(4, metaURI); + await multiAsset.addAssetToToken(tokenId, 1, 0); + await multiAsset.addAssetToToken(tokenId, 2, 0); + await multiAsset.addAssetToToken(tokenId, 3, 0); + await multiAsset.addAssetToToken(tokenId, 4, 0); + await multiAsset.connect(rootOwner).acceptAsset(tokenId, 0, 1); + await multiAsset.connect(rootOwner).acceptAsset(tokenId, 1, 2); + await multiAsset.connect(rootOwner).setPriority(tokenId, [10, 42]); + + const data = await renderUtils.getExtendedNft(tokenId, multiAsset.address); + + expect(data.directOwner).to.eql(rootOwner.address); + expect(data.rootOwner).to.eql(rootOwner.address); + expect(data.activeAssetCount).to.eql(bn(2)); + expect(data.pendingAssetCount).to.eql(bn(2)); + expect(data.priorities).to.eql([10, 42]); + expect(data.name).to.eql('MultiAsset'); + expect(data.symbol).to.eql('MA'); + expect(data.activeChildrenNumber).to.eql(bn(0)); + expect(data.isSoulbound).to.be.false; + expect(data.hasMultiAssetInterface).to.be.true; + expect(data.hasNestingInterface).to.be.false; + expect(data.hasEquippableInterface).to.be.false; + }); + + it('renders correct data for Nestable', async function () { + const parentId = await mintFromMock(nestable, rootOwner.address); + const tokenId = await mintFromMock(nestable, rootOwner.address); + const child1 = await nestMintFromMock(nestable, nestable.address, tokenId); + await nestMintFromMock(nestable, nestable.address, tokenId); + const child3 = await nestMintFromMock(nestable, nestable.address, tokenId); + await nestable.connect(rootOwner).acceptChild(tokenId, 0, nestable.address, child1); + await nestable.connect(rootOwner).acceptChild(tokenId, 0, nestable.address, child3); + await nestable + .connect(rootOwner) + .nestTransferFrom(rootOwner.address, nestable.address, tokenId, parentId, '0x'); + await nestable.connect(rootOwner).acceptChild(parentId, 0, nestable.address, tokenId); + + const data = await renderUtils.getExtendedNft(tokenId, nestable.address); + + expect(data.directOwner).to.eql(nestable.address); + expect(data.rootOwner).to.eql(rootOwner.address); + expect(data.activeAssetCount).to.eql(bn(0)); + expect(data.pendingAssetCount).to.eql(bn(0)); + expect(data.priorities).to.eql([]); + expect(data.name).to.eql('Nestable'); + expect(data.symbol).to.eql('Ne'); + expect(data.activeChildrenNumber).to.eql(bn(2)); + expect(data.isSoulbound).to.be.false; + expect(data.hasMultiAssetInterface).to.be.false; + expect(data.hasNestingInterface).to.be.true; + expect(data.hasEquippableInterface).to.be.false; + }); + + it('renders correct data for soulbound Nestable', async function () { + const tokenId = await mintFromMock(nestableSoulbound, rootOwner.address); + const child1 = await nestMintFromMock(nestableSoulbound, nestableSoulbound.address, tokenId); + await nestMintFromMock(nestableSoulbound, nestableSoulbound.address, tokenId); + const child3 = await nestMintFromMock(nestableSoulbound, nestableSoulbound.address, tokenId); + await nestableSoulbound + .connect(rootOwner) + .acceptChild(tokenId, 0, nestableSoulbound.address, child1); + await nestableSoulbound + .connect(rootOwner) + .acceptChild(tokenId, 0, nestableSoulbound.address, child3); + + const data = await renderUtils.getExtendedNft(tokenId, nestableSoulbound.address); + + expect(data.directOwner).to.eql(rootOwner.address); + expect(data.rootOwner).to.eql(rootOwner.address); + expect(data.activeAssetCount).to.eql(bn(0)); + expect(data.pendingAssetCount).to.eql(bn(0)); + expect(data.priorities).to.eql([]); + expect(data.name).to.eql('NestableSoulbound'); + expect(data.symbol).to.eql('NS'); + expect(data.activeChildrenNumber).to.eql(bn(2)); + expect(data.isSoulbound).to.be.true; + expect(data.hasMultiAssetInterface).to.be.false; + expect(data.hasNestingInterface).to.be.true; + expect(data.hasEquippableInterface).to.be.false; + }); + + it('renders correct data for Nestable with MultiAsset', async function () { + const parentId = await mintFromMock(nestableMultiAsset, rootOwner.address); + const tokenId = await mintFromMock(nestableMultiAsset, rootOwner.address); + await nestableMultiAsset.addAssetEntry(1, metaURI); + await nestableMultiAsset.addAssetEntry(2, metaURI); + await nestableMultiAsset.addAssetEntry(3, metaURI); + await nestableMultiAsset.addAssetEntry(4, metaURI); + await nestableMultiAsset.addAssetToToken(tokenId, 1, 0); + await nestableMultiAsset.addAssetToToken(tokenId, 2, 0); + await nestableMultiAsset.addAssetToToken(tokenId, 3, 0); + await nestableMultiAsset.addAssetToToken(tokenId, 4, 0); + await nestableMultiAsset.connect(rootOwner).acceptAsset(tokenId, 0, 1); + await nestableMultiAsset.connect(rootOwner).acceptAsset(tokenId, 1, 2); + await nestableMultiAsset.connect(rootOwner).setPriority(tokenId, [10, 42]); + const child1 = await nestMintFromMock(nestableMultiAsset, nestableMultiAsset.address, tokenId); + await nestMintFromMock(nestableMultiAsset, nestableMultiAsset.address, tokenId); + const child3 = await nestMintFromMock(nestableMultiAsset, nestableMultiAsset.address, tokenId); + await nestableMultiAsset + .connect(rootOwner) + .acceptChild(tokenId, 0, nestableMultiAsset.address, child1); + await nestableMultiAsset + .connect(rootOwner) + .acceptChild(tokenId, 0, nestableMultiAsset.address, child3); + await nestableMultiAsset + .connect(rootOwner) + .nestTransferFrom(rootOwner.address, nestableMultiAsset.address, tokenId, parentId, '0x'); + await nestableMultiAsset + .connect(rootOwner) + .acceptChild(parentId, 0, nestableMultiAsset.address, tokenId); + + const data = await renderUtils.getExtendedNft(tokenId, nestableMultiAsset.address); + + expect(data.directOwner).to.eql(nestableMultiAsset.address); + expect(data.rootOwner).to.eql(rootOwner.address); + expect(data.activeAssetCount).to.eql(bn(2)); + expect(data.pendingAssetCount).to.eql(bn(2)); + expect(data.priorities).to.eql([10, 42]); + expect(data.name).to.eql('NestableMultiAsset'); + expect(data.symbol).to.eql('NMA'); + expect(data.activeChildrenNumber).to.eql(bn(2)); + expect(data.isSoulbound).to.be.false; + expect(data.hasMultiAssetInterface).to.be.true; + expect(data.hasNestingInterface).to.be.true; + expect(data.hasEquippableInterface).to.be.false; + }); + + it('renders correct data for Equippable', async function () { + const parentId = await mintFromMock(equip, rootOwner.address); + const tokenId = await mintFromMock(equip, rootOwner.address); + await equip.addEquippableAssetEntry(1, 0, ADDRESS_ZERO, 'ipfs://res1.jpg', []); + await equip.addEquippableAssetEntry(2, 1, catalog.address, 'ipfs://res2.jpg', [1, 3, 4]); + await equip.addEquippableAssetEntry(3, 0, ADDRESS_ZERO, 'ipfs://res3.jpg', []); + await equip.addEquippableAssetEntry(4, 2, catalog.address, 'ipfs://res4.jpg', [4]); + await equip.addAssetToToken(tokenId, 1, 0); + await equip.addAssetToToken(tokenId, 2, 0); + await equip.addAssetToToken(tokenId, 3, 1); + await equip.addAssetToToken(tokenId, 4, 0); + await equip.connect(rootOwner).acceptAsset(tokenId, 0, 1); + await equip.connect(rootOwner).acceptAsset(tokenId, 1, 2); + await equip.connect(rootOwner).setPriority(tokenId, [10, 42]); + const child1 = await nestMintFromMock(equip, equip.address, tokenId); + await nestMintFromMock(equip, equip.address, tokenId); + const child3 = await nestMintFromMock(equip, equip.address, tokenId); + await equip.connect(rootOwner).acceptChild(tokenId, 0, equip.address, child1); + await equip.connect(rootOwner).acceptChild(tokenId, 0, equip.address, child3); + await equip + .connect(rootOwner) + .nestTransferFrom(rootOwner.address, equip.address, tokenId, parentId, '0x'); + await equip.connect(rootOwner).acceptChild(parentId, 0, equip.address, tokenId); + + const data = await renderUtils.getExtendedNft(tokenId, equip.address); + + expect(data.directOwner).to.eql(equip.address); + expect(data.rootOwner).to.eql(rootOwner.address); + expect(data.activeAssetCount).to.eql(bn(2)); + expect(data.pendingAssetCount).to.eql(bn(2)); + expect(data.priorities).to.eql([10, 42]); + expect(data.name).to.eql('Equippable'); + expect(data.symbol).to.eql('EQ'); + expect(data.activeChildrenNumber).to.eql(bn(2)); + expect(data.isSoulbound).to.be.false; + expect(data.hasMultiAssetInterface).to.be.true; + expect(data.hasNestingInterface).to.be.true; + expect(data.hasEquippableInterface).to.be.true; + }); +}); + async function setUpCatalog(catalog: RMRKCatalogMock, gemAddress: string): Promise { await catalog.addPartList([ { From 59256042946c02aa83c027aaa693c975be0ef288 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Fri, 20 Jan 2023 13:45:02 +0100 Subject: [PATCH 22/26] Add fields to ExtendedNft Added additional fields to `ExtendedNft`. --- contracts/RMRK/utils/RMRKRenderUtils.sol | 41 ++++++++++++ docs/RMRK/utils/RMRKEquipRenderUtils.md | 2 +- docs/RMRK/utils/RMRKRenderUtils.md | 2 +- test/renderUtils.ts | 82 +++++++++++++++++++++++- 4 files changed, 123 insertions(+), 4 deletions(-) diff --git a/contracts/RMRK/utils/RMRKRenderUtils.sol b/contracts/RMRK/utils/RMRKRenderUtils.sol index 0665c424..288bf8b2 100644 --- a/contracts/RMRK/utils/RMRKRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKRenderUtils.sol @@ -3,10 +3,13 @@ pragma solidity ^0.8.16; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import "../access/Ownable.sol"; import "../equippable/RMRKEquippable.sol"; import "../extension/soulbound/RMRKSoulbound.sol"; import "../library/RMRKLib.sol"; import "../library/RMRKErrors.sol"; +import "./RMRKMintingUtils.sol"; import "./RMRKTokenURI.sol"; /** @@ -18,12 +21,16 @@ import "./RMRKTokenURI.sol"; contract RMRKRenderUtils { /** * @notice Structure used to represent the extended NFT. + * @return tokenMetadataUri Metadata URI of the specified token * @return directOwner Address of the direct owner of the token (smart contract if the token is nested, otherwise * EOA) * @return rootOwner Address of the root owner * @return activeAssetCount Number of active assets present on the token * @return pendingAssetcount Number of pending assets on the token * @return priorities The array of priorities of the active asset + * @return maxSupply The maximum supply of the collection the specified token belongs to + * @return totalSupply The total supply of the collection the specified token belongs to + * @return issuer Address of the issuer of the token's collection * @return name Name of the collection the token belongs to * @return symbol Symbol of the collection the token belongs to * @return activeChildrenNumber Number of active child tokens of the given token (only account for direct child @@ -34,11 +41,15 @@ contract RMRKRenderUtils { * @return hasEquippableInterface Boolean value signifying whether the toke supports Equippable interface */ struct ExtendedNft { + string tokenMetadataUri; address directOwner; address rootOwner; uint256 activeAssetCount; uint256 pendingAssetCount; uint16[] priorities; + uint256 maxSupply; + uint256 totalSupply; + address issuer; string name; string symbol; uint256 activeChildrenNumber; @@ -96,11 +107,15 @@ contract RMRKRenderUtils { * @notice Used to get extended information about a specified token. * @dev The full `ExtendedNft` struct looks like this: * [ + * tokenMetadataUri, * directOwner, * rootOwner, * activeAssetCount, * pendingAssetCount * priorities, + * maxSupply, + * totalSupply, + * issuer, * name, * symbol, * activeChildrenNumber, @@ -146,6 +161,32 @@ contract RMRKRenderUtils { data.directOwner = data.rootOwner; } data.name = target.name(); + try IERC721Metadata(targetCollection).tokenURI(tokenId) returns ( + string memory metadataUri_ + ) { + data.tokenMetadataUri = metadataUri_; + } catch { + // Retain default value + } + try RMRKMintingUtils(targetCollection).totalSupply() returns ( + uint256 totalSupplly_ + ) { + data.totalSupply = totalSupplly_; + } catch { + // Retain default value + } + try RMRKMintingUtils(targetCollection).maxSupply() returns ( + uint256 maxSupplly_ + ) { + data.maxSupply = maxSupplly_; + } catch { + // Retain default value + } + try Ownable(targetCollection).owner() returns (address issuer_) { + data.issuer = issuer_; + } catch { + // Retain default value + } data.symbol = target.symbol(); } } diff --git a/docs/RMRK/utils/RMRKEquipRenderUtils.md b/docs/RMRK/utils/RMRKEquipRenderUtils.md index a4546907..7f0ef9fd 100644 --- a/docs/RMRK/utils/RMRKEquipRenderUtils.md +++ b/docs/RMRK/utils/RMRKEquipRenderUtils.md @@ -261,7 +261,7 @@ function getExtendedNft(uint256 tokenId, address targetCollection) external view Used to get extended information about a specified token. -*The full `ExtendedNft` struct looks like this: [ directOwner, rootOwner, activeAssetCount, pendingAssetCount priorities, name, symbol, activeChildrenNumber, isSoulbound, hasMultiAssetInterface, hasNestingInterface, hasEquippableInterface ]* +*The full `ExtendedNft` struct looks like this: [ tokenMetadataUri, directOwner, rootOwner, activeAssetCount, pendingAssetCount priorities, maxSupply, totalSupply, issuer, name, symbol, activeChildrenNumber, isSoulbound, hasMultiAssetInterface, hasNestingInterface, hasEquippableInterface ]* #### Parameters diff --git a/docs/RMRK/utils/RMRKRenderUtils.md b/docs/RMRK/utils/RMRKRenderUtils.md index a438da4f..3d3d0c17 100644 --- a/docs/RMRK/utils/RMRKRenderUtils.md +++ b/docs/RMRK/utils/RMRKRenderUtils.md @@ -18,7 +18,7 @@ function getExtendedNft(uint256 tokenId, address targetCollection) external view Used to get extended information about a specified token. -*The full `ExtendedNft` struct looks like this: [ directOwner, rootOwner, activeAssetCount, pendingAssetCount priorities, name, symbol, activeChildrenNumber, isSoulbound, hasMultiAssetInterface, hasNestingInterface, hasEquippableInterface ]* +*The full `ExtendedNft` struct looks like this: [ tokenMetadataUri, directOwner, rootOwner, activeAssetCount, pendingAssetCount priorities, maxSupply, totalSupply, issuer, name, symbol, activeChildrenNumber, isSoulbound, hasMultiAssetInterface, hasNestingInterface, hasEquippableInterface ]* #### Parameters diff --git a/test/renderUtils.ts b/test/renderUtils.ts index a7e1ffcc..5b4b29e2 100644 --- a/test/renderUtils.ts +++ b/test/renderUtils.ts @@ -9,12 +9,14 @@ import { RMRKEquippableMock, RMRKEquipRenderUtils, RMRKMultiAssetRenderUtils, + RMRKMultiAssetImplPreMint__factory, } from '../typechain-types'; import { RMRKMultiAssetMock } from '../typechain-types'; import { RMRKNestableMock } from '../typechain-types'; import { RMRKNestableMultiAssetMock } from '../typechain-types'; import { RMRKSoulboundNestableMock } from '../typechain-types'; import { connect } from 'http2'; +import { RMRKMultiAssetImplPreMint } from '../typechain-types'; // --------------- FIXTURES ----------------------- @@ -73,6 +75,7 @@ async function simpleRenderUtilsFixture() { async function extendedNftRenderUtilsFixture() { const multiAssetFactory = await ethers.getContractFactory('RMRKMultiAssetMock'); + const multiAssetPremintFactory = await ethers.getContractFactory('RMRKMultiAssetImplPreMint'); const nestableFactory = await ethers.getContractFactory('RMRKNestableMock'); const nestableSoulboundFactory = await ethers.getContractFactory('RMRKSoulboundNestableMock'); const nestableMultiAssetFactory = await ethers.getContractFactory('RMRKNestableMultiAssetMock'); @@ -83,6 +86,17 @@ async function extendedNftRenderUtilsFixture() { const multiAsset = await multiAssetFactory.deploy('MultiAsset', 'MA'); await multiAsset.deployed(); + const multiAssetPremint = ( + await multiAssetPremintFactory.deploy( + 'MultiAssetPreMint', + 'MApM', + 'ipfs://collection/collection', + 'ipfs://collection/token', + [ethers.constants.AddressZero, false, ethers.constants.AddressZero, 0, 10_000, 1_000_000_000], + ) + ); + await multiAssetPremint.deployed; + const nestable = await nestableFactory.deploy('Nestable', 'Ne'); await nestable.deployed(); @@ -107,6 +121,7 @@ async function extendedNftRenderUtilsFixture() { return { multiAsset, + multiAssetPremint, nestable, nestableSoulbound, nestableMultiAsset, @@ -401,6 +416,7 @@ describe('Extended NFT render utils', function () { let issuer: SignerWithAddress; let rootOwner: SignerWithAddress; let multiAsset: RMRKMultiAssetMock; + let multiAssetPremint: RMRKMultiAssetImplPreMint; let nestable: RMRKNestableMock; let nestableSoulbound: RMRKSoulboundNestableMock; let nestableMultiAsset: RMRKNestableMultiAssetMock; @@ -412,8 +428,16 @@ describe('Extended NFT render utils', function () { const supply = bn(10000); beforeEach(async function () { - ({ multiAsset, nestable, nestableSoulbound, nestableMultiAsset, catalog, equip, renderUtils } = - await loadFixture(extendedNftRenderUtilsFixture)); + ({ + multiAsset, + multiAssetPremint, + nestable, + nestableSoulbound, + nestableMultiAsset, + catalog, + equip, + renderUtils, + } = await loadFixture(extendedNftRenderUtilsFixture)); [issuer, rootOwner] = await ethers.getSigners(); }); @@ -434,11 +458,15 @@ describe('Extended NFT render utils', function () { const data = await renderUtils.getExtendedNft(tokenId, multiAsset.address); + expect(data.tokenMetadataUri).to.eql(''); expect(data.directOwner).to.eql(rootOwner.address); expect(data.rootOwner).to.eql(rootOwner.address); expect(data.activeAssetCount).to.eql(bn(2)); expect(data.pendingAssetCount).to.eql(bn(2)); expect(data.priorities).to.eql([10, 42]); + expect(data.maxSupply).to.eql(bn(0)); + expect(data.totalSupply).to.eql(bn(0)); + expect(data.issuer).to.eql(ethers.constants.AddressZero); expect(data.name).to.eql('MultiAsset'); expect(data.symbol).to.eql('MA'); expect(data.activeChildrenNumber).to.eql(bn(0)); @@ -448,6 +476,40 @@ describe('Extended NFT render utils', function () { expect(data.hasEquippableInterface).to.be.false; }); + it('renders correct data for MultiAsset premint', async function () { + const tokenId = await mintFromMock(multiAssetPremint, rootOwner.address); + await multiAssetPremint.addAssetEntry(metaURI); + await multiAssetPremint.addAssetEntry(metaURI); + await multiAssetPremint.addAssetEntry(metaURI); + await multiAssetPremint.addAssetEntry(metaURI); + await multiAssetPremint.addAssetToToken(tokenId, 1, 0); + await multiAssetPremint.addAssetToToken(tokenId, 2, 0); + await multiAssetPremint.addAssetToToken(tokenId, 3, 0); + await multiAssetPremint.addAssetToToken(tokenId, 4, 0); + await multiAssetPremint.connect(rootOwner).acceptAsset(tokenId, 0, 1); + await multiAssetPremint.connect(rootOwner).acceptAsset(tokenId, 1, 2); + await multiAssetPremint.connect(rootOwner).setPriority(tokenId, [10, 42]); + + const data = await renderUtils.getExtendedNft(tokenId, multiAssetPremint.address); + + expect(data.tokenMetadataUri).to.eql('ipfs://collection/token'); + expect(data.directOwner).to.eql(rootOwner.address); + expect(data.rootOwner).to.eql(rootOwner.address); + expect(data.activeAssetCount).to.eql(bn(2)); + expect(data.pendingAssetCount).to.eql(bn(2)); + expect(data.priorities).to.eql([10, 42]); + expect(data.maxSupply).to.eql(bn(10000)); + expect(data.totalSupply).to.eql(bn(tokenId)); + expect(data.issuer).to.eql(issuer.address); + expect(data.name).to.eql('MultiAssetPreMint'); + expect(data.symbol).to.eql('MApM'); + expect(data.activeChildrenNumber).to.eql(bn(0)); + expect(data.isSoulbound).to.be.false; + expect(data.hasMultiAssetInterface).to.be.true; + expect(data.hasNestingInterface).to.be.false; + expect(data.hasEquippableInterface).to.be.false; + }); + it('renders correct data for Nestable', async function () { const parentId = await mintFromMock(nestable, rootOwner.address); const tokenId = await mintFromMock(nestable, rootOwner.address); @@ -463,11 +525,15 @@ describe('Extended NFT render utils', function () { const data = await renderUtils.getExtendedNft(tokenId, nestable.address); + expect(data.tokenMetadataUri).to.eql(''); expect(data.directOwner).to.eql(nestable.address); expect(data.rootOwner).to.eql(rootOwner.address); expect(data.activeAssetCount).to.eql(bn(0)); expect(data.pendingAssetCount).to.eql(bn(0)); expect(data.priorities).to.eql([]); + expect(data.maxSupply).to.eql(bn(0)); + expect(data.totalSupply).to.eql(bn(0)); + expect(data.issuer).to.eql(ethers.constants.AddressZero); expect(data.name).to.eql('Nestable'); expect(data.symbol).to.eql('Ne'); expect(data.activeChildrenNumber).to.eql(bn(2)); @@ -491,11 +557,15 @@ describe('Extended NFT render utils', function () { const data = await renderUtils.getExtendedNft(tokenId, nestableSoulbound.address); + expect(data.tokenMetadataUri).to.eql(''); expect(data.directOwner).to.eql(rootOwner.address); expect(data.rootOwner).to.eql(rootOwner.address); expect(data.activeAssetCount).to.eql(bn(0)); expect(data.pendingAssetCount).to.eql(bn(0)); expect(data.priorities).to.eql([]); + expect(data.maxSupply).to.eql(bn(0)); + expect(data.totalSupply).to.eql(bn(0)); + expect(data.issuer).to.eql(ethers.constants.AddressZero); expect(data.name).to.eql('NestableSoulbound'); expect(data.symbol).to.eql('NS'); expect(data.activeChildrenNumber).to.eql(bn(2)); @@ -537,11 +607,15 @@ describe('Extended NFT render utils', function () { const data = await renderUtils.getExtendedNft(tokenId, nestableMultiAsset.address); + expect(data.tokenMetadataUri).to.eql(''); expect(data.directOwner).to.eql(nestableMultiAsset.address); expect(data.rootOwner).to.eql(rootOwner.address); expect(data.activeAssetCount).to.eql(bn(2)); expect(data.pendingAssetCount).to.eql(bn(2)); expect(data.priorities).to.eql([10, 42]); + expect(data.maxSupply).to.eql(bn(0)); + expect(data.totalSupply).to.eql(bn(0)); + expect(data.issuer).to.eql(ethers.constants.AddressZero); expect(data.name).to.eql('NestableMultiAsset'); expect(data.symbol).to.eql('NMA'); expect(data.activeChildrenNumber).to.eql(bn(2)); @@ -577,11 +651,15 @@ describe('Extended NFT render utils', function () { const data = await renderUtils.getExtendedNft(tokenId, equip.address); + expect(data.tokenMetadataUri).to.eql(''); expect(data.directOwner).to.eql(equip.address); expect(data.rootOwner).to.eql(rootOwner.address); expect(data.activeAssetCount).to.eql(bn(2)); expect(data.pendingAssetCount).to.eql(bn(2)); expect(data.priorities).to.eql([10, 42]); + expect(data.maxSupply).to.eql(bn(0)); + expect(data.totalSupply).to.eql(bn(0)); + expect(data.issuer).to.eql(ethers.constants.AddressZero); expect(data.name).to.eql('Equippable'); expect(data.symbol).to.eql('EQ'); expect(data.activeChildrenNumber).to.eql(bn(2)); From 1aa59f3c5aeeb202102e947a6ac992f7e590b740 Mon Sep 17 00:00:00 2001 From: Steven Pineda Date: Fri, 20 Jan 2023 09:59:23 -0500 Subject: [PATCH 23/26] Apply suggestions from code review Co-authored-by: Jan Turk --- contracts/RMRK/utils/RMRKNestableRenderUtils.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/RMRK/utils/RMRKNestableRenderUtils.sol b/contracts/RMRK/utils/RMRKNestableRenderUtils.sol index acfb394a..5195de17 100644 --- a/contracts/RMRK/utils/RMRKNestableRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKNestableRenderUtils.sol @@ -37,10 +37,10 @@ contract RMRKNestableRenderUtils { * @notice Check if the child is owned by the expected parent. * @dev Reverts if child token is not owned by an NFT. * @dev Reverts if child token is not owned by the expected parent. - * @param childAddress Address of the child contract. - * @param childId ID of the child token. - * @param expectedParent Address of the expected parent contract. - * @param expectedParentId ID of the expected parent token. + * @param childAddress Address of the child contract + * @param childId ID of the child token + * @param expectedParent Address of the expected parent contract + * @param expectedParentId ID of the expected parent token */ function checkExpectedParent( address childAddress, From f631953baa42db0a0d175438761207675002351d Mon Sep 17 00:00:00 2001 From: steven2308 Date: Fri, 20 Jan 2023 10:02:53 -0500 Subject: [PATCH 24/26] Updates docs. --- docs/RMRK/utils/RMRKEquipRenderUtils.md | 8 ++++---- docs/RMRK/utils/RMRKNestableRenderUtils.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/RMRK/utils/RMRKEquipRenderUtils.md b/docs/RMRK/utils/RMRKEquipRenderUtils.md index 7f0ef9fd..fccbd310 100644 --- a/docs/RMRK/utils/RMRKEquipRenderUtils.md +++ b/docs/RMRK/utils/RMRKEquipRenderUtils.md @@ -48,10 +48,10 @@ Check if the child is owned by the expected parent. | Name | Type | Description | |---|---|---| -| childAddress | address | Address of the child contract. | -| childId | uint256 | ID of the child token. | -| expectedParent | address | Address of the expected parent contract. | -| expectedParentId | uint256 | ID of the expected parent token. | +| childAddress | address | Address of the child contract | +| childId | uint256 | ID of the child token | +| expectedParent | address | Address of the expected parent contract | +| expectedParentId | uint256 | ID of the expected parent token | ### composeEquippables diff --git a/docs/RMRK/utils/RMRKNestableRenderUtils.md b/docs/RMRK/utils/RMRKNestableRenderUtils.md index 97f20631..3ad5c041 100644 --- a/docs/RMRK/utils/RMRKNestableRenderUtils.md +++ b/docs/RMRK/utils/RMRKNestableRenderUtils.md @@ -24,10 +24,10 @@ Check if the child is owned by the expected parent. | Name | Type | Description | |---|---|---| -| childAddress | address | Address of the child contract. | -| childId | uint256 | ID of the child token. | -| expectedParent | address | Address of the expected parent contract. | -| expectedParentId | uint256 | ID of the expected parent token. | +| childAddress | address | Address of the child contract | +| childId | uint256 | ID of the child token | +| expectedParent | address | Address of the expected parent contract | +| expectedParentId | uint256 | ID of the expected parent token | ### getChildIndex From e68963214c29c88bc1ae8d7135a036fb87d47a4b Mon Sep 17 00:00:00 2001 From: Steven Pineda Date: Fri, 20 Jan 2023 10:15:52 -0500 Subject: [PATCH 25/26] Apply suggestions from code review Co-authored-by: Jan Turk --- contracts/RMRK/utils/RMRKEquipRenderUtils.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol index 192dbe15..2068fde8 100644 --- a/contracts/RMRK/utils/RMRKEquipRenderUtils.sol +++ b/contracts/RMRK/utils/RMRKEquipRenderUtils.sol @@ -386,6 +386,8 @@ contract RMRKEquipRenderUtils is * ] * @param targetChild Address of the smart contract of the given token * @param childId ID of the child token whose assets will be matched against parent's slot parts + * @param expectedParent Address of the collection smart contract of the expected parent token + * @param expectedParentId ID of the expected parent token * @param parentAssetId ID of the target parent asset to use to equip the child * @return childIndex Index of the child in the parent's list of active children * @return assetsWithSlots An array of `AssetWithSlot` structs containing info about the equippable child assets and their corresponding slot parts @@ -544,8 +546,8 @@ contract RMRKEquipRenderUtils is * @param targetChild Address of the collection smart contract of the given token * @param childId ID of the child token * @param parentAssetId ID of the parent asset from which to get the slot parts - * @param expectedParent ID of the parent asset from which to get the slot parts - * @param expectedParentId ID of the parent asset from which to get the slot parts + * @param expectedParent Address of the collection smart contract of the expected parent token + * @param expectedParentId ID of the expected parent token * @return parentSlotPartIds Array of slot part IDs of the parent token's asset */ function _getParentAndSlotParts( From 909066437ae7c39d49dc421dca9992d8ed5567db Mon Sep 17 00:00:00 2001 From: steven2308 Date: Fri, 20 Jan 2023 10:17:53 -0500 Subject: [PATCH 26/26] Updates docs after minor change. --- docs/RMRK/utils/RMRKEquipRenderUtils.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/RMRK/utils/RMRKEquipRenderUtils.md b/docs/RMRK/utils/RMRKEquipRenderUtils.md index fccbd310..687b0dac 100644 --- a/docs/RMRK/utils/RMRKEquipRenderUtils.md +++ b/docs/RMRK/utils/RMRKEquipRenderUtils.md @@ -170,8 +170,8 @@ Used to get the child's assets and slot parts pairs, identifying parts the s |---|---|---| | targetChild | address | Address of the smart contract of the given token | | childId | uint256 | ID of the child token whose assets will be matched against parent's slot parts | -| expectedParent | address | undefined | -| expectedParentId | uint256 | undefined | +| expectedParent | address | Address of the collection smart contract of the expected parent token | +| expectedParentId | uint256 | ID of the expected parent token | | parentAssetId | uint64 | ID of the target parent asset to use to equip the child | #### Returns