Basic Usage
Child Management

Child-management

Once you have deployed your Nestable NFT contract, you can dynamically change ownership of NFTs with the following operations.

Writing operations

  1. Proposing a child.
    1. Via nestTransferFrom.
    2. Via _nestMint.
  2. Accepting a child.
  3. Rejecting all children.
  4. Transferring a child.
    1. Rejecting a child.
    2. Abandoning a child.
    3. Unnesting child token to the root owner.
    4. Transferring the child token to an EOA or an ERC721Receiver.
    5. Transferring the child token into a new parent token.

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

  1. Getting root owner
  2. Getting direct owner
  3. Getting active children
  4. Getting pending children

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...' }