Comment on page
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.
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.
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.JavaScript
import { AtomicAssetOps } from 'freeverse-apisigner-js';
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.
GraphQL
Test it
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.GraphQL
Test it
query {
assetById(id: "$id") {
props
propsSettled
}
}
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.NodeJS
Test it
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:GraphQL
Test it
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
}
}
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()
Here are all the details on the
ops
field required by the Execute Mutation, as well as the parameters it requires.Property | Values |
---|---|
type | "create_asset"
"set_asset_props" |
msg | JSON object. See message options below. |
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 |
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 |
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
.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.
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_props
operations 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:
Asset Nonce
User Nonce
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"
}
}
}