Create & Evolve

Creation and Evolution queries

Creating and evolving Living Assets are queries that require the signature of the owner of the corresponding Universe where the affected assets live.

Generate your Universe

When on-boarded to the Living Assets platform, clients are asked to provide a web3 address, and a new Universe will be created, controlled by such web3 address.

  • The initial Universe will be created first in the L2 testnet; when ready to go live, a new Universe will be created in the L2 mainnet.

  • The provided web3 address can be controlled by whatever means desired, from a simple web3 wallet under one person's control, to a sophisticated multisig approach that implements certain business logic and governance. All options are identical from the point of view of the Living Assets platform: all complexity is encapsulated in the provided signature.

Straight-to-Code Examples

To get your hands on working code quickly, please visit the Examples repository, and go straight to the asset_create and asset_evolve scripts inside the nodejs folder.

As shown in the examples, the main class that helps build the query parameters easily is AtomicAssetOps, which can be found in the API Signer NPM package.

import { AtomicAssetOps } from 'freeverse-apisigner-js';

The Execute Mutation

Creating and evolving assets in a given universe is done via a GraphQL mutation which requires three input parameters:

  • ops: a string, in JSON format, with the modification operation wished to be executed in the corresponding Universe.

  • signature: the result of signing the operations string with your account.

  • universe: your universe id.

The mutation will fail if the passed signature does not match with the owner of the universe.

mutation ($opsString: JSON!, $signature: String!)
{ 
    execute(
        input: { 
            ops: [$opsString], 
            signature: $signature,
            universe: universe_id,
        })
    {
        results
    }
}

The state of the newly created/updated assets can be readily queried. The following query returns both the already synchronized (settled) state, as well as the newest (possible pending) state; they both will coincide when the verse returned in the response's receipt is reached.

query {
  assetById(id: "$id") {
      props
      propsSettled
  }
}

Building the mutation with AtomicAssetOps

Instantiate the class AtomicAssetOps to build the three parameters required by the mutation. The following example code build a create_asset operation (via the createAssetOp method), inserts it in the mutation (via the push method), and signs the operation.

const { createAssetOp, AtomicAssetOps } = require('freeverse-apisigner-js');
const assetOps = new AtomicAssetOps({ universeId: 0 });
const operation = createAssetOp({
  nonce,
  ownerId: owner,
  props: asset_props,
  metadata: asset_metadata,
});
assetOps.push({ op: operation });
const sig = assetOps.sign({ web3Account: universe_owner_web3account });
const mutation = assetOps.mutation({ signature: sig });

The content of mutation is to be sent to the GraphQL endpoint. In the example, it looks as follows:

mutation {
    execute(
      input: {
        ops: ["{\"type\":\"create_asset\",\"msg\":{\"nonce\":0,\"owner_id\":\"0x983c1A8FCf74CAF648fD51fd7A6d322a502ae789\",\"props\":\"{\\\"name\\\":\\\"Supercool Dragon\\\",\\\"description\\\":\\\"Legendary creature that loves fire.\\\",\\\"image\\\":\\\"ipfs://QmPCHHeL1i6ZCUnQ1RdvQ5G3qccsjgQF8GkJrWAm54kdtB\\\",\\\"animation_url\\\":\\\"ipfs://QmefzYXCtUXudCy9LYjU4biapHJiP26EGYS8hQjpei472j\\\",\\\"attributes\\\":[{\\\"trait_type\\\":\\\"Rarity\\\",\\\"value\\\":\\\"Scarce\\\"},{\\\"trait_type\\\":\\\"Level\\\",\\\"value\\\":5},{\\\"trait_type\\\":\\\"Weight\\\",\\\"value\\\":123.5}]}\",\"metadata\":\"{\\\"private_data\\\":\\\"that only I will see\\\"}\"}}"],
        signature: "93024b05dcc822776fa84f3dc4070d5e16b9f032b11f4699bf6dbf017fac7fa8587ee9af9c41223c4a7f7fe66d873be1cd6141f0f9efccc13aac402c7e327b2d1b",
        universe: 0,
      }
    )
    {
      results
    }
  }

Signing the Mutation

Universe Owners can use their own web3-compatible method to sign the mutation, instead of using the sign() method provided by the AtomicAssetOps class. Just use the digest() method to get the exact string that needs to be signed:

const digest = assetOps.digest()

Details on the Mutation Parameters

Here are all the details on the ops field required by the Execute Mutation, as well as the parameters it requires.

ops [JSON]

msg [JSON]

props [JSON]

The props string parameter is a JSON object that contains any public property wished to be assigned to the asset. We advice to follow a de-facto standard, as detailed in Asset Properties Standard. If you wish to follow the standard, the typical fields would include, at least, the following:

Once an asset is created, the number of attributes does not need to remain constant. New attributes can be added as part of the set_asset_props mutation. Examples: adding the charisma attribute to assets that were created without it, or adding a specific license field making explicit the rights transferred to asset owners.

Images, videos, and media in general, can be in any format that the applications/marketplaces that will show the assets are compatible with. Media can be uploaded separately to IPFS and linked in the corresponding image/animation_url fields. The Customizable Marketplace currently supports these formats: jpeg, png, gif, webp, tiff.

metadata [JSON]

The metadata JSON object is intended to expand the asset properties with data that will not be stored in the blockchain. Specific applications or marketplaces may parse this field to implement client-side business logic.

nonce [uint]

All users and assets within a universe are assigned a Number-Used-Once (NOnce) value. Creation and evolution queries require the correct nonce value as parameter, otherwise the mutation will fail. The nonce value prevents replay attacks, whereby the same signed operation is held by an attacker, and relayed multiple times.

  • create_asset operations require passing the user nonce, in a given universe, corresponding to the initial owner to whom the created asset will be assigned.

  • set_asset_propsoperations require passing the asset nonce.

Applications can either keep track of user and asset nonces themselves, or query them when needed before building Execute Mutations, as detailed in these examples:

The Response to the Execute Mutation

The API responses to execute mutations contain a Receipt Json object, which includes the signature of the L2 relayer, confirming that the changes will be applied at the current verse.

If the mutation contains one or several create_asset operations, the receipt contains the assetId of the corresponding created assets.

This is the anatomy of a Receipt:

Here's an example of the return of a query that created an asset, and evolved another asset, in one single atomic call:

{
  "data": {
    "execute": {
      "ops": [
        "{\"type\":\"create_asset\",\"msg\":{\"nonce\":227,\"owner_id\":\"0x0A0b39de3E704e8fB1C8E2A8C92c25A1A5f01930\",\"props\":\"{\\\"name\\\":\\\"Supercool Dragon\\\",\\\"description\\\":\\\"Legendary creature that loves ice.\\\",\\\"image\\\":\\\"ipfs://QmPCHHeL1i6ZCUnQ1RdvQ5G3qccsjgQF8GkJrWAm54kdtB\\\",\\\"animation_url\\\":\\\"ipfs://QmefzYXCtUXudCy9LYjU4biapHJiP26EGYS8hQjpei472j\\\",\\\"attributes\\\":[{\\\"trait_type\\\":\\\"Rarity\\\",\\\"value\\\":\\\"Common\\\"},{\\\"trait_type\\\":\\\"Level\\\",\\\"value\\\":10},{\\\"trait_type\\\":\\\"Weight\\\",\\\"value\\\":223.5}]}\",\"metadata\":\"{\\\"private_data\\\":\\\"that has been updated\\\"}\"}}",
        "{\"type\":\"set_asset_props\",\"msg\":{\"nonce\":5,\"id\":\"9997220295222263808910038093126797702019127927349276\",\"props\":\"{\\\"name\\\":\\\"My First Living Asset\\\",\\\"description\\\":\\\"Freeverse living asset. The on-chain properties of this NFT can evolve.\\\",\\\"image\\\":\\\"ipfs://QmUn4gzAAY8vDFNnA25MopB1RWtMmwWdQCEMUe67XiPW8x\\\",\\\"animation_url\\\":\\\"ipfs://QmefzYXCtUXudCy9LYjU4biapHJiP26EGYS8hQjpei472j\\\",\\\"attributes\\\":[{\\\"trait_type\\\":\\\"Level\\\",\\\"value\\\":\\\"1\\\"},{\\\"trait_type\\\":\\\"Shape\\\",\\\"value\\\":\\\"Cilinder\\\"},{\\\"trait_type\\\":\\\"Creation date\\\",\\\"value\\\":\\\"2022-07-13 10:50:01\\\"}]}\",\"metadata\":\"{\\\"private_data\\\":\\\"that has been updated\\\"}\"}}"
      ],
      "signature": "5746cdd53449f8c9d693125fdc44213c95c94904b2c547e7268c7f5028427138489ec2044d0737530671b2f2917d2163781f688718e2c613d05a124627eff64b1c",
      "universe": 0,
      "verse": 62,
      "results": [
        "{\"id\":\"116101747409823859223166001815771088694834615066564912\"}",
        "{\"result\":\"success\"}"
      ],
      "receiptSignature": "c0337ea5f1a1ff35346fc589b59bc55f705dfdb2409d8e0717371832d8343e54460e733e63a4df7b035cf36b8b044befdf73a3087974eeb10197ec2dc95aa6851c"
    }
  }
}

Here's a code example showing how to verify the signer of the receipt signature.

Last updated

freeverse.io