Skip to content

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 a 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:

mutation ($opsString: JSON!, $signature: String!)
{ 
    execute(
        input: { 
            ops: [$opsString], 
            signature: $signature,
            universe: universe_id,
        })
    {
        results
    }
}
  • 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.

Warning

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

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({ 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]

Property Values
type "create_asset"
"set_asset_props"
msg JSON object. See message options below.

msg [JSON]

Property Values
nonce Target owner's user nonce for create_asset
Asset nonce for set_asset_props
id The ID of the asset (mandatory for set_asset_props)
owner_id The web3 address of the asset owner (mandatory for create_asset)
props A string containing a JSON object with the public properties of this asset. See Asset Properties Standard for reference.
metadata A string containing JSON object with the private metadata of this asset

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:

    Property           Values
name The name of the asset
description The description of the asset
image A link to the asset image; an ipfs:// route is recommended for decentralization
attributes a JSON array with each entry containing trait_type and value
animation_url A link to asset media: video, sound, etc.; an ipfs:// route is recommended for decentralization

Note

Once an asset is created, the number of attributes do 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.

Note

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. Alternatively, images can also be uploaded via the image upload API (see this example.) In the latter case, we currently support 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. Nonce value prevent 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:

{
    assetById(id:"107839786668602559179520105617439374088259616617004211901616313864846") {
        nonce
    }
}
{
    usersUniverseByUserIdAndUniverseId(universeId: 0, userId: "0xE67EdD9Dddb965Cb43B1f070D7e2b16EfE9668a2") {
        nonce
    }
}

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:

           Property             Values
ops an array with the operations applied
results an array, with entries aligned with ops, containing the unique Ids of the assets created for each create_asset, and a bool confirming the update for each set_asset_props
universe the universe Id where the ops are applied
signature the signature provided as input to the query
verse the verse at which ops will be synchronized with the Layer 1 (= currentVerse + 1)
receiptSignature the Layer 2 signature committing to the rest of returned parameters

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.