Creating Tables from Contracts

Creating Tables from Contracts

Smart contracts can call the Tableland registry contract directly to create tables.

Synopsis

Developers can leverage smart contract calls to the TablelandTables registry smart contract for chain-based table creation. There is one major callout to implement.

The creating contract must be able to receive an ERC721 token.

Namely, please be sure to implement a way for the contract to receive the ERC721 token that is minted & sent by the TablelandTables registry contract. Two common paths to achieve this include:

  • Inherit the ERC721Holder contract, provided by OpenZeppelin.
  • Implement the underlying IERC721Receiver and its onERC721Received function.

Without this implementation, your contract will revert when trying to create tables since it is unable receive the corresponding token.

Implementation

For those opting to inherit from the ERC721Holder, please be sure to review the specification on OpenZeppelin for more information. If possible, try for at least solc 0.8.12 since this introduced string concatenation bug fixes, which is used in writing SQL.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@tableland/evm/contracts/ITablelandTables.sol";

contract BasicCreate is ERC721Holder {
    // Create a variable that stores the interface to the `TablelandTables` registry contract
    ITablelandTables private _tableland;

    constructor(address registry) {
				// Pass the `TablelandTables` deployed smart contract address
        _tableland = ITablelandTables(registry);
    }

    // Some logic to call `_tableland` & create a table using `createTable`
		// Note: `onERC721Received` is implemented by `ERC721Holder`
}

Alternatively, you may wish to use IERC721Receiver and implement onERC721Received yourself:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@tableland/evm/contracts/ITablelandTables.sol";

contract BasicCreate is IERC721Receiver {
    // Create a variable that stores the interface to the `TablelandTables` registry contract
    ITablelandTables private _tableland;

    constructor(address registry) {
				// Pass the `TablelandTables` deployed smart contract address
        _tableland = ITablelandTables(registry);
    }

		function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
		    return IERC721Receiver.onERC721Received.selector;
		}

    // Some logic to call `_tableland` & create a table using `createTable`
}

Example

Let’s walk through an example with ERC721Holder since IERC721Receiver is rather similar. This is a simple smart contract that calls the Tableland registry smart contract and mints a table. Upon minting a table, there is a mapping that tracks tableNames to their corresponding tableIds, but this is purely for demonstration purposes. Alternatively, you may want to store the tableName and tableId as storage variables if you anticipate only needing to mint and use a select few tables.

Put differently, the contract below is a table factory that simply mints tables, but your app will likely have a specific set of tables it needs to use, and that’s it. You might want to replace the tables mapping with its components, tableName and tableId, or something similar.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@tableland/evm/contracts/ITablelandTables.sol";

contract BasicCreate is ERC721Holder {
		// A mapping that holds `tableName` and its `tableId`
    mapping(string => uint256) public tables;
		// nterface to the `TablelandTables` registry contract
    ITablelandTables private _tableland;

    constructor(address registry) {
        _tableland = ITablelandTables(registry);
    }

    function create(string memory prefix) public payable {
        uint256 tableId = _tableland.createTable(
            address(this),
            /*
            *  CREATE TABLE {prefix}_{chainId} (
            *    id integer primary key,
            *    message text
            *  );
            */
            string.concat(
                "CREATE TABLE ",
                prefix,
                "_",
                Strings.toString(block.chainid),
                " (id integer primary key, message text);"
            )
        );

        string memory tableName = string.concat(
            prefix,
            "_",
            Strings.toString(block.chainid),
            "_",
            Strings.toString(tableId)
        );

        tables[tableName] = tableId;
    }
}

The contract can now call the Tableland registry and mint tables. Note the usage of address(this). The TablelandTables contract has a createTable function that takes an address called owner as the first parameter. It simply mints a TABLE ERC721 token to this owner — which is the contract since address(this) was passed.

It’s possible that a developer will choose to pass the address of some function caller’s address, like msg.sender, instead of having the contract itself be the owner via address(this). Here, it would eliminate the need for inheriting ERC721Holder or IERC721Receiver since the contract would, thus, be acting as a passthrough to the Tableland registry contract and never actually receives an ERC721 token. It’d be minted right to msg.sender.