First P2E game on Tableland (2)
Tags: tableland, nft, metadata, dynamic
Published: 2023-02-12 13:34:51

It's been a while since I posted a blog about the first P2E game on Tableland, in order to give you an idea about how to manipulate metadata on-chain, just like normal relational database manipulation.

Previously, I talked about the databases on Tableland for Kart and Asset collections, how to create tables on the Tableland, and how to return URI based on that.
If you missed the previous one, please check it before you move forward.
https://dev.to/ponyjackal/first-p2e-game-on-tableland-1-2p8p

Today, I will deep dive into AutobodyShop contract, where you will eventually apply many assets to the kart, which leads to metadata updates on the Kart collection.

So we have the applyAssets function which will take assets data for the kart, and update the metadata table on Tableland for the kart.

It will be easy since we will just fetch assets metadata, and update the kart table.

But here is the thing, we are not able to get results from tableland on-chain, we can only do that off-chain.
Furthermore, our assets have multiple attributes, do you remember?
This means, we have to do some sub-queries, which is also not possible in the current tableland.

So I ended up using the hybrid method, we will fetch the results from the assets table and do some sub-queries to finalize the attributes we will apply to the kart off-chain, which is node js api.
Once user hits apply assets button on UI, it will hit this api, not the smart contract function directly, and from that api, we make some voucher which contains (BE signature, query strings to run on tableland tables), call the smart contract on UI.

Hope you will find it easy to understand, or this blog will help you.
https://dev.to/ponyjackal/signature-verification-on-hybrid-method-2349

Here is the voucher for applyAssets function on Autobodyshop.

```
struct AutoBodyShopVoucher {
    address owner;
    uint256 kartId;
    uint256[] assetIds;
    string resetQuery;
    string applyQuery;
    string updateImageQuery;
    uint256 nonce;
    uint256 expiry;
    bytes signature;
}
```

Now that I'd love to share applyAssets function.

```
  /**
   * @dev Apply assets attributes to a kart
   * @param _voucher The AutoBodyShopVoucher
   */
  function applyAssets(AutoBodyShopVoucher calldata _voucher) external nonContract nonReentrant {
    address signer = _verify(_voucher);
    require(signer == gameServer && msg.sender == _voucher.owner, "AutoBodyShop: invalid call");
    require(_voucher.nonce == nonces[_voucher.owner], "AutoBodyShop: invalid nonce");
    require(_voucher.expiry == 0 || block.timestamp <= _voucher.expiry, "AutoBodyShop: signature is expired");
    require(kittyKartGoKart.ownerOf(_voucher.kartId) == msg.sender, "AutoBodyShop: not a kart owner");

    nonces[_voucher.owner]++;
    for (uint256 i = 0; i < _voucher.assetIds.length; i++) {
      require(kittyKartAsset.ownerOf(_voucher.assetIds[i]) == msg.sender, "AutoBodyShop: not an asset owner");
      kittyKartAsset.safeTransferFrom(msg.sender, address(this), _voucher.assetIds[i]);
    }

    // update in_use for previously applied asset
    tableland.runSQL(address(this), kittyKartAssetAttributeTableId, _voucher.resetQuery);
    // set kart_id in asset attribute table
    tableland.runSQL(address(this), kittyKartAssetAttributeTableId, _voucher.applyQuery);
    // update image url for kart
    tableland.runSQL(address(this), kittyKartGoKartTableId, _voucher.updateImageQuery);

    emit ApplyAssets(_voucher.kartId, _voucher.assetIds);
  }
```

Looks pretty much straight forward, doesn't it?

First there are some validation checks including ownership and voucher.
And then transfer assets to Autobodyshop contract, 

Finally, update tableland tables,

Note that we need 3 things to update.
- update in_use column in asset table
- update kart_id column in asset table
- update image_url column in kart table

Oh, did I tell you about tableland write permissions?

Okay, no worries, let me explain now.

Each tableland table has privileges so that onlyowners can write into the table, though it's public to read.
And table owner here is the table creator obivously, in our case, for assets table, owner is Asset contract, for kart table, owner is Kart contract.
For more details, https://docs.tableland.xyz/table-writes-updates

As you notice, we are going to update assets and kart tables on Autobodyshop contract, which is not the owner.
In order to make this work, we have to grant write permission to Autobodyshop contract, and only owner of the table can do this.


That's why we have grantAccess and revokeAccess functions in Kart and Asset contracts.

```
 /**
   * @dev Grant access of table to EOA
   * @param _to The address to grant access
   */
  function grantAccess(address _to) external onlyOwner {
    require(metadataTableId != 0, "KittyKartGoKart: table is not created yet");
    tableland.runSQL(
      address(this),
      metadataTableId,
      string.concat(
        "GRANT INSERT, UPDATE, DELETE ON ",
        metadataTable,
        " TO ",
        "'",
        StringsUpgradeable.toHexString(_to),
        "'",
        ";"
      )
    );
    emit AccessGranted(_to);
  }

  /**
   * @dev Revoke access of table to EOA
   * @param _to The address to grant access
   */
  function revokeAccess(address _to) external onlyOwner {
    require(metadataTableId != 0, "KittyKartGoKart: table is not created yet");
    tableland.runSQL(
      address(this),
      metadataTableId,
      string.concat(
        "REVOKE INSERT, UPDATE, DELETE ON ",
        metadataTable,
        " FROM ",
        "'",
        StringsUpgradeable.toHexString(_to),
        "'",
        ";"
      )
    );
    emit AccessRevoked(_to);
  }
```

Once the write permission is granted to the Autobodyshop contract, we are able to run SQL queries to update assets and the kart table on Autobodyshop contract.

Damn, I think I went through all the things you need to get familiar with the app.

Here are 2 things I want to emphasize, that shaped our current design.
- There is no way to get query results on-chain
- For now, it's not possible to run sub-queries in the tableland

As you know, it will give a sense of building on-chain metadata in NFT space with tableland.

Hope you enjoyed this,
Thanks