Child-management
Once you have deployed your Nestable NFT contract, you can dynamically change ownership of NFTs with the following operations.
Writing operations
Proposing a child
IERC7401 follows as propose-accept pattern, to prevent malicious parties from spamming your NFTs. A child must be added via the addChild
method defined in the standard, but this must only be called by the contract of the child NFT being added, since it needs to be aware that the child's owner will be the destination token.
Our core implementations include a nestTransferFrom
method which can be called by the owner of the child NFT or an approved party. The alternative way to do it, is to directly mint the child into the parent. Our core implementations include an internal, non-opinionanted _nestMint
method you can use to build on top, and our ready to use implementations include an external nestMint
which has different behavior according to the type of implementation. Both of these will do the right addChild
calls internally.
Never call addChild
directly on a Nestable contract, our implementations will revert in this case. Only the child contract is supposed to do this call.
nestTransferFrom
and _nestMint
have a bytes
parameter: data
, with no specified format. This parameter is passed to addChild
so you can use it to pass arbitrary information to the parent contract. If you do not need it you can simply send empty bytes. You can also use the data
parameter in the _beforeNestedTokenTransfer
and _afterNestedTokenTransfer
hooks. For more details see the hooks section.
Via Nest Transfer From
You may add a nestTransfer
on top of the nestTransferFrom
method, which uses the msg.sender
as the from
(just as commonly ERC721
implementations do). We did not include it to keep the ready to use implementations minimal in size so you have more space for custom logic.
const childId = 10;
const parentId = 1;
const data = '0x';
await childContract
.nestTransferFrom(user.address, parentContract.address, childId, parentId, data);
Via Nest Mint
Our ready to use implementations include a nestMint
method which has different behavior according to the type of implementation. If you build your custom contract on top of our core or base implementations, you may implement your own nestMint
method. The method is fully opinionanted and it is not mandatory, you can also have a regular mint
and later do a nest transfer.
The _prepareMint
method is available on the base implementations as a helper to assign next available tokenIds
. For more details on the implementation levels see the implementation section. Here is an example of a bulk nest mint method:
function nestMint(
address to,
uint256 numToMint,
uint256 destinationId
) public payable virtual returns (uint256 firstTokenId) {
(uint256 nextToken, uint256 totalSupplyOffset) = _prepareMint(
numToMint
);
_chargeMints(numToMint); // This depends on the charging method, and it is omitted on premint versions.
for (uint256 i = nextToken; i < totalSupplyOffset; ) {
// This method does not use the data parameter, but you can modify to your needs
_nestMint(to, i, destinationId, "");
unchecked {
++i;
}
}
return nextToken;
}
Accepting a child
In order to become part of the array of active children, a proposed child must be accepted first. This should be done by either the owner of the parent token, or an approved party.
You may implement your own auto accept mechanism. The ready to use implementations by default auto accept the first asset, or assets added by the owner of the token. On RMRK's wizard (opens in a new tab), you can also create contracts with a method to define and auto-accept children from certain collections.
The childIndex
parameter is an annoying detail, but it prevents the contract from having to do gas expensive operations, either:
- Iterate over the list of pending children to find the index.
- Keep track of the index for each child.
const parentId = 1;
const childId = 10;
// Find childIndex on parent's pending children
const childrenIds = (await parentContract.pendingChildrenOf(parentId)).map(
(child) => child.tokenId.toNumber()
)
const childIndex = childrenIds.indexOf(childId)
await parentContract.acceptChild(parentId, childIndex, childContract.address, childId);
Rejecting all children
Although there is a way to reject a child at a time, see rejecting a child, at times you might want to remove all of your NFTs children. The method includes a maxRejections
parameter, to prevent you from unwillingly rejecting a child which arrives just before the call is executed. You may set it to the total number of pending children and have the user confirm before doing the call.
const parentId = 1;
const pendingChildren = await parentContract.pendingChildrenOf(parentId);
const maxRejections = pendingChildren.length;
// Confirm the number is what user expects.
await parentContract.rejectAllChildren(parentId, maxRejections);
Transferring a child
There are 4 actions that can be achieved through the transferChild
method. To fully understand them, we have to look at the available parameters passed to transferChild
.
If the to
parameter is address zero, which is the case of abandoning or rejecting a child, the parent contract does not call the child. This a safety measure to be able to remove malicious children without interacting with them.
If the destinationId
is 0
, the destination must be an EOA or a contract implementing IERC721Receiver
. In this case the parent contract will call safeTransferFrom
on the child. On the other hand, if there is a destination token, the parent will call nestTransferFrom
on the child.
In both cases the data
parameter will be passed so you can use it to pass arbitrary information to the child contract. You may also use it on the _beforeTransferChild
and _afterTransferChild
hooks. For more details see the hooks section.
The childIndex
parameter is an annoying detail, but it prevents the contract from having to do gas expensive operations, either:
- Iterate over the list of pending children to find the index.
- Keep track of the index for each child.
/**
* @notice Used to transfer a child token from a given parent token.
* @dev When transferring a child token, the owner of the token is set to `to`, or is not updated in the event of
* `to` being the `0x0` address.
* @param tokenId ID of the parent token from which the child token is being transferred
* @param to Address to which to transfer the token to
* @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token)
* @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or
* pending array)
* @param childAddress Address of the child token's collection smart contract.
* @param childId ID of the child token in its own collection smart contract.
* @param isPending A boolean value indicating whether the child token being transferred is in the pending array of
* the parent token (`true`) or in the active array (`false`)
* @param data Additional data with no specified format, sent in call to `_to`
*/
function transferChild(
uint256 tokenId,
address to,
uint256 destinationId,
uint256 childIndex,
address childAddress,
uint256 childId,
bool isPending,
bytes memory data
) external;
Based on the desired state transitions, the values of these parameters have to be set accordingly (any parameters not set in the following examples depend on the child token being managed):
Rejecting a child
Removes a child from the pending children array of the parent token. For security reasons, the child token is never called.
Abandoning and rejecting a child are very similar, the only difference is that child is that abandoning removes from active children, and rejecting from pending children.
// These are example values, adjust depending on the tokens:
const tokenId = 1;
const childAddress = childContract.address;
const childId = 10;
// Find childIndex on parent's pending children
const childrenIds = (await parentContract.pendingChildrenOf(parentId)).map(
(child) => child.tokenId.toNumber()
)
const childIndex = childrenIds.indexOf(childId)
// To reject
const to = ethers.constants.AddressZero;
const destinationId = 0;
const isPending = true;
// For custom usage
const data = '0x';
await parentContract
.transferChild(tokenId, to, destinationId, childIndex, childAddress, childId, isPending, data);
Abandoning a child
Removes a child from the active children array of the parent token. For security reasons, the child token is never called.
Abandoning and rejecting a child are very similar, the only difference is that child is that abandoning removes from active children, and rejecting from pending children.
// These are example values, adjust depending on the tokens:
const tokenId = 1;
const childAddress = childContract.address;
const childId = 10;
// Find childIndex on parent's active children
const childrenIds = (await parentContract.childrenOf(parentId)).map(
(child) => child.tokenId.toNumber()
)
const childIndex = childrenIds.indexOf(childId)
// To abandon
const to = ethers.constants.AddressZero;
const destinationId = 0;
const isPending = false;
// For custom usage
const data = '0x';
await parentContract
.transferChild(tokenId, to, destinationId, childIndex, childAddress, childId, isPending, data);
Unnesting a child
Transfers the child token to an EOA or an ERC721Receiver
. The EOA can be the root owner.
// These are example values, adjust depending on the tokens:
const tokenId = 1;
const childAddress = childContract.address;
const childId = 10;
// Find childIndex on parent's actice children
const childrenIds = (await parentContract.childrenOf(parentId)).map(
(child) => child.tokenId.toNumber()
)
const childIndex = childrenIds.indexOf(childId)
const isPending = false; // You can also transfer from pending
// To transfer to EOA or ERC721Receiver
const to = eoaOrErc721Receiver.address;
const destinationId = 0;
// For custom usage
const data = '0x';
await parentContract
.transferChild(tokenId, to, destinationId, childIndex, childAddress, childId, isPending, data);
Transferring the child token into a new parent token
Removes the child from the active children array on the current parent and adds it to the pending children array on the new one. The new parent's root owner can accept or reject it. Acceptance is needed even if the root owner of the new parent token is the same as the root owner of the former parent.
// These are example values, adjust depending on the tokens:
const tokenId = 1;
const childAddress = childContract.address;
const childId = 10;
// Find childIndex on parent's actice children
const childrenIds = (await parentContract.childrenOf(parentId)).map(
(child) => child.tokenId.toNumber()
)
const childIndex = childrenIds.indexOf(childId)
const isPending = false; // You can also transfer from pending
// To transfer to another NFT
const to = newParentContract.address;
const destinationId = 2;
// For custom usage
const data = '0x';
await parentContract
.transferChild(tokenId, to, destinationId, childIndex, childAddress, childId, isPending, data);
Reading operations
Getting root owner
Used to retrieve the root owner of a given token. That is the owner of the token, or recursively the owner of the parent token if owner is an NFT. It will return an EOA or a contract implementing IERC721Receiver
.
const tokenId = 1;
const rootOwner = await parentContract.ownerOf(tokenId);
// const rootOwner = '0x...'
Getting direct owner
Used to retrieve the immediate owner of the given token. It can be an EOA or a contract implementing either IERC7401
or IERC721Receiver
. If the owner is an NFT, the parentId
will be non zero and isNFT
will be true
. If the owner is an EOA or or IERC721Receiver
, the parentId
will be 0
and isNFT
will be false
.
const tokenId = 1;
const [owner, parentId, isNFT] = await parentContract.directOwnerOf(tokenId);
// owner = '0x...'
// parentId = 1
// isNFT = true
Getting active children
Used to retrieve the active children tokens of a given parent token. The returned array consists of Child
structs which contain tokenId
and contractAddress
of the child token. You can also retrieve a specific child given the index.
// All children
const parentId = 1;
const children = await parentContract.childrenOf(parentId);
// children = [
// { tokenId: 10, contractAddress: '0x...' },
// { tokenId: 11, contractAddress: '0x...' },
// { tokenId: 12, contractAddress: '0x...' },
// ]
// A specific child
const childIndex = 0;
const child = await parentContract.childOf(parentId, childIndex);
// child = { tokenId: 10, contractAddress: '0x...' }
Getting pending children
Used to retrieve the pending children tokens of a given parent token. The returned array consists of Child
structs which contain tokenId
and contractAddress
of the child token. You can also retrieve a specific child given the index.
// All children
const parentId = 1;
const children = await parentContract.pendingChildrenOf(parentId);
// children = [
// { tokenId: 10, contractAddress: '0x...' },
// { tokenId: 11, contractAddress: '0x...' },
// { tokenId: 12, contractAddress: '0x...' },
// ]
// A specific child
const childIndex = 0;
const child = await parentContract.pendingChildOf(parentId, childIndex);
// child = { tokenId: 10, contractAddress: '0x...' }