Basic Living Assets Server

Create a brand new server application with endpoints to mint and evolve Living Assets

In this tutorial, we are going to create a simple server application from scratch, that offers two endpoints - one to create a new Living Asset (a dynamic NFT) and assign it to a user, and the other to evolve the properties of an existing asset. The server will use GraphQL to interact with the Living Assets API.

Why a server-side application?

Creating and evolving Living Assets requires proof of authority - for example, for an asset that is going to be used in a game, only the game developer should have the authority to create that asset and verify its utility in the game. For this reason, all Freeverse clients get created for them their own 'universe' - a dedicated area within our Layer-2 where they have control. To demonstrate authority, any transaction that modifies the content of the universe must be cryptographically signed by the universe owner's private key.

As the creation or evolution of Living Assets requires signing with the game developer's private key, these actions must be executed within a server application which stores that key. Doing it from the client would require storing the private key within the game binary, thus handing it to every app installation, with all the attack vectors associated to it.

The figure below shows an example of how the server this tutorial will start to build will fit into a typical game.

Requirements

To follow this tutorial you should have:

  • Basic knowledge of development in NodeJS, and basic knowledge of how POST operations work in RESTful applications. Knowledge of GraphQL is useful, but not mandatory; an introduction can be found here.

  • A development machine with the latest version of NodeJS and Node Package Manager installed.

  • A sandbox universe (and associated private key) within the Living Assets API. If you do not have one of these, please contact us.

  • A basic knowledge of the Create & Evolve functions described here.

While there are many different server application frameworks, in many different languages, for this tutorial we will ExpressJS. While this framework has perhaps gone out of fashion recently, it serves the purpose of a quick way of creating a server with different routes for endpoints. Feel free to adapt to your favourite framework! The rest of the code (creating mutations, signing etc.) is framework agnostic.

All identification in the Living Assets API is done by Ethereum compatible wallet addresses. Each of your users must be identified via a public Ethereum compatible wallet address - like the ones created in this tutorial.

Setup

Create a new NodeJS application from the console, and install the packages as shown below. We will need Express for server routing, GraphQL-Request for connecting to the Living Assets API, and two of Freeverse's packages to simplify signing transactions. Navigate to a new folder in your terminal and run the following commands, one at a time:

npm init
npm install express graphql-request freeverse-crypto-js freeverse-apisigner-js

On project initialization, give your project a name and accept all of the defaults.

Now create a starting javascript file called index.js, add a simple console.log("hello, world!"); and test it by running

node index.js

Creating the basic server

The very simple server in this tutorial only needs to expose two endpoints, both of which will accept JSON data via POST. One will use this data to create (mint) a new Living Asset, the other will use the data to evolve (update) the properties of an existing asset.

Overwrite the content of index.js with the following code:

// index.js
const express = require('express');
const app = express();
app.use(express.json()); // requires Express v4.16+

app.post('/create', (request, response) => {
	response.send(request.body); // echo the result back
});

app.post('/evolve', (request, response) => {
	response.send(request.body); // echo the result back
});

app.listen(3000);

Here, there is a simple Express server which accepts two POST endpoints, both of which simply echo the received JSON back as a response.

Run the example from the terminal as above, then test it with the following CURL command from the terminal (or by sending the JSON object via a REST testing application such as Postman):

curl -d '{"owner":"0x123", "props":"{}", "metadata":"{}"}' -H "Content-Type: application/json" -X POST http://localhost:3000/create

You should see this response in the Terminal:

{"user":"0x123","props":"{}","metadata":"{}"}

Congratulations, you have a server up and running!

Creating an Asset

Now we have our server endpoints, let's create and sign a mutation that creates an asset for a user. For this example, we'll simply create an asset and assigns it to the same account as the universe owner (using the public address as the identifier).

Getting the User Nonce

The Living Asset API makes heavy use of the nonce concept to reduce the chance of repeat requests. To create a new asset, if the nonce of the mutation is not equal to that of the owner who is receiving the asset, the mutation will fail. More information about nonce here.

Modify the first half of index.js (up to the end of the '/create/' route, with the following code, which queries the Living Assets API to get the nonce value of a specific user.

const express = require('express');
const { GraphQLClient, gql } = require('graphql-request');
const app = express();
app.use(express.json());

// constants for use in tutorial
const endpoint = '<paste endpoint provided by Freeverse';
const owner = '<paste_universe_owner_public_address>';

// the GraphQL Client, used to send and receive data
const graphQLClient = new GraphQLClient(endpoint, {
	jsonSerializer: {
	parse: JSON.parse,
	stringify: JSON.stringify,
	},
});

// create asset route
app.post('/create/', async (request, response) => {
	// request the nonce from the Living Assets API
	const data = await graphQLClient.request(
		// query
		gql`
		query( $userId: String! ) {
			usersUniverseByUserIdAndUniverseId(
				universeId: 0,
				userId: $userId
			) {
				nonce
			}
		}`,
		// variables
		{
			userId: owner,
		},
	);
	// if user is not found, query returns null
	// so we protect against this by setting default value to 0
	let ownerNonce = 0;
  	if (data.usersUniverseByUserIdAndUniverseId && data.usersUniverseByUserIdAndUniverseId.nonce) {
	    ownerNonce = data.usersUniverseByUserIdAndUniverseId.nonce
	}
	response.send(ownerNonce.toString()); // echo the result
});

// remainder of file...

Run this example with the same CURL command as above, and you should see the user nonce printed in the terminal. We will use this nonce when building the mutation to create the asset.

Signing and Creating the Create Asset Mutation

We will use two Freeverse NPM packages to sign transactions and build the mutation strings (the repositories for these packages are freely available here).

Create a new file in your project directory called freeverse.js and type the following code:

// freeverse.js
const identity = require('freeverse-crypto-js');
const { createAssetOp, AtomicAssetOps } = require('freeverse-apisigner-js');

const UNIVERSE_ID = ''; // PASTE YOUR UNIVERSE ID HERE
const PVK = ''; // PASTE YOUR PVK HERE

const universeOwnerAccount = identity.accountFromPrivateKey(PVK);

exports.createAsset = (owner, ownerNonce, assetProps, assetMetadata) => {
	const assetOps = new AtomicAssetOps({ universeId: UNIVERSE_ID });
	const operation = createAssetOp({
		nonce: ownerNonce,
		ownerId: owner,
		props: assetProps,
		metadata: assetMetadata,
	});
	
	assetOps.push({ op: operation });
	const sig = assetOps.sign({ web3Account: universeOwnerAccount });
	const mutation = assetOps.mutation({ signature: sig });
	return mutation;
};

Make sure to paste your universe ID and private key in the relevant variables.

This function accepts as arguments the four parameters that we must supply to the create_asset transaction in the Freeverse API.

The first two commands of the function use these passed parameters to create an "operations string" with the correct format (if you want, you can output the operation variable to the console to see what it looks like).

The sign command takes that operations string and signs it using the supplied private key.

Finally, the mutation command parses the operations string (including escape characters where required), combines it with the signature, and outputs the final GraphQL mutation.

Now, return to index.js and add this newly created file to the list of dependencies at the beginning.

// index.js
const freeverse = require('./freeverse');

Then, in the route for '/create/', replace the final response.send() command with the following code:

const mutation = freeverse.createAsset(owner, ownerNonce, {}, {});
response.send(mutation);

As you can see, while we are creating a new asset for owner, the properties and metadata are simply empty objects, for the time being.

Upon calling the same CURL function, the response should be a string of text which represents the mutation that must be sent to the API to create the asset.

You can test this mutation directly now in the playground below (for staging scenarios only). Paste the output code into the left-hand window and press the play button. The right-hand window should show a successful result, with the ID of the newly created asset, as illustrated in the image below.

If you try to run this mutation again (by pressing the Play button again) you will see it returns an error! This is because the nonce for this user has changed as a result of the previous transaction. This is why we need to query the nonce each time before minting an asset.

Sending the mutation to the Living Assets API

All that remains for us to do is to automatically send this mutation to the Living Assets API. We have already created a GraphQLClient object to request the asset nonce, so we can reuse that to send the mutation. Replace the final lines of the '/create/' route to be:

let ownerNonce = 0;
if (data.usersUniverseByUserIdAndUniverseId && data.usersUniverseByUserIdAndUniverseId.nonce) {
    ownerNonce = data.usersUniverseByUserIdAndUniverseId.nonce
}
const mutation = freeverse.createAsset(owner, ownerNonce, {}, {});
const mutationResponse = await graphQLClient.request(mutation);
response.send(mutationResponse);

Upon executing the CURL request again, this time the successful result of the mutation will be returned, which contains the newly created asset ID.

You can parse this response to extract the newly created asset ID to store elsewhere in your server application (for example, for tracking which user owns which assets).

Including POSTed data in the mutation

Currently, the user ID for whom we are minting the asset is hardcoded, as are the asset properties and metadata. Let's change that to feed this data from the POST request into the mutation. To avoid long multiline CURL requests, we'll put our JSON data into a file, and pass the filename to the CURL request. Remember to call the CURL request from the same directory as the JSON file!

curl -d @testCreate.json -H "Content-Type: application/json" -X POST http://localhost:3000/create

While the asset properties can be anything you want, for full compatibility with the other elements of the API (Living Assets Scan app, marketplace etc.) Freeverse recommends the use of a standard format for NFT properties, demonstrated below. The metadata, which is not viewable by the end user, can be any data you want.

testCreate.json

{
	"owner":"<PASTE_OWNER_WEB3_ADDRESS>",
	"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":{
		"info": "which is private"
	}
}

To pipe this data into the mutation, we must change the first lines of the '/create/' route to the following:

// create asset route
app.post('/create/', async (request, response) => {
	// get the asset data from the POST request body
	const { owner, props, metadata } = request.body;
	...
	// and below change the call to createAsset
	const mutation = freeverse.createAsset(owner, ownerNonce, props, metadata);
	...

You can now use the returned asset ID to see the asset details in the Sandbox Living Assets Scan app. In the production Living Assets Scan app, from here you can also verify ownership and properties on Polygon.

Evolving an Asset

The steps to change the properties of an existing asset are very similar to those for creating a new one. The principal difference is that the nonce required is associated with the asset, rather than with the user. However, almost everything else is the same.

Note that when you update the asset properties you are overwriting the old ones i.e. you must pass the entire asset properties JSON.

First, we need to add a new function to freeverse.js, which creates and signs the mutation to update the asset properties. This code is almost identical to that which creates and signs the 'create' mutation, only changing the user ID for asset ID, and changing the operation type:

// freeverse.js
// add updateAssetOp in the requirements
const { createAssetOp, updateAssetOp, AtomicAssetOps } = require('freeverse-apisigner-js');

// ...

exports.evolveAsset = (asset, assetNonce, assetProps, assetMetadata) => {
	const assetOps = new AtomicAssetOps({ universeId: UNIVERSE_ID });
	const operation = updateAssetOp({
		nonce: assetNonce,
		assetId: asset,
		props: assetProps,
		metadata: assetMetadata,
	});
	assetOps.push({ op: operation });
	const sig = assetOps.sign({ web3Account: universeOwnerAccount });
	const mutation = assetOps.mutation({ signature: sig });
	return mutation;
};

We then modify the '/evolve/' route - again, very similar to the '/create/', but changing the nonce query and only a few key variables:

// index.js
// create asset route
app.post('/evolve/', async (request, response) => {
	// get the asset ID from the POST request body
	const { asset, props, metadata } = request.body;
	// request the nonce from the Living Assets API
	const data = await graphQLClient.request(
		// query
		gql`
		query( $assetId: String! ) {
			assetById(
				id: $assetId
			) {
				nonce
			}
		}`,
		// variables
		{
			assetId: asset,
		},
	);
	const assetNonce = data.assetById.nonce;
	const mutation = freeverse.evolveAsset(asset, assetNonce, props, metadata);
	const mutationResponse = await graphQLClient.request(mutation);
	response.send(mutationResponse);
});

Finally, we create a new JSON file, changing the 'owner' property for 'asset', and pasting the assetID that was returned in the terminal from the response to the 'create' call.

{
	asset":"2659135364631074528293600266911586821553740380548290008617451061479",
	"props":{
		"name": "Supercool Dragon",
...

Feel free to change any of the properties to see how they are updated!

Let's call our new route, making sure to specify the new JSON filename:

curl -d @testEvolve.json -H "Content-Type: application/json" -X POST http://localhost:3000/evolve

And then check out the updated asset in the Sandbox Living Assets Scan!

Wrapping up & next steps

In this tutorial we created a basic server that allows you to mint and evolve Living Assets. For practical use, several steps are now available:

  • add authentication to the endpoints (in our demo, anybody with access to the endpoint can mint as many Living Assets as they want, for any owner!). In the next tutorial, we'll learn how to add a JSON Web Token for authentication, and also add a second layer of encryption to the payload requests of both the '/create'/ and '/evolve/' routes

  • create functionality to add various other queries to the Living Assets API to the application - a comprehensive set of examples is available here.

  • connect to a database to track ownership independently on the Living Assets API

The full code for this tutorial is available here.

Thanks for following along!

Last updated

freeverse.io