#sidechain

By Gauthier Sebille

In the first part of our tutorial, we have seen how to interact with the VM state of our sidechain Deku-P.

The goal of this article is to create a front web application using this state!

Prerequisites

       1. Read and do the first blog article
       2. Having an up & running Deku-P Cluster (explained in the first blog article)
       3. Having a working front application

Steps

We assume you have front web development skills or experiences. Hence, we will only focus on the mandatory tasks to interact with Deku-P from the outside world.

You already interacted with the VM state using deku-cli in the previous blog post. On a front web application, we have to do some actions before submitting:

       1. Retrieve the actual state of the Deku VM
       2. Retrieve the current height
       3. Create the operation we want to submit
       4. Submit the operation

We will explain in detail how to perform each of these actions.

A naive working front

Before going deeper into how to create and submit our operation, we create a simple front web with several buttons, one per action:

       - One button to mint cookies
       - One button to mint cursor
       - One button to mint grandma

We will also display several useful information saved in our state, like:

       - Current amount of cookies
       - Current “cookies per second”
       - Current amount of cursors
       - Current amount of grandma
       - Next cost for Cursor
       - Next cost for Grandma

Here is the current front we have on Decookies:

I think you guessed it:

       - if we click on the cookie, it mints a cookie.
       - If we click on the cursor, it will mint a cursor.
       - And if we click on the beautiful grandma… It will mint a Grandma!

Of course, you need sufficient funds to buy Cursors, Grandmas and the other buildings.

Defining our state

The first thing to do is to have an equivalent of the State we defined in our VM.

Remember how it is in the end of the first blog article:


export type cookieBaker = {
    cookies: bigint,
    cursors: bigint,
    grandmas: bigint,

    cursorCost: bigint,
    grandmaCost: bigint,

    cursorCps: bigint,
    grandmaCps: bigint
}

Let’s define the exact same on our front web application.

In our case, we decided to use the reducer pattern. Hence, the cookieBaker type will be wrapped in a state type, containing other useful information.

Minting your first cookie

Now that we have defined our state and created our reducer to perform the correct actions regarding the clicked button, our reducer kinda looks like:


export const reducer = (state: state, action: action): state => {
   switch (action.type) {
       case "FULL_UPDATE_COOKIE_BAKER": {
           return { ...state, cookieBaker: action.payload }
       }
    }
}

You can have a look at the defined actions, this is not the purpose of this article, and I won’t explain the details. You are free not to use the reducer pattern.

Create wallet

For security and gameplay purposes, we are not going to ask the private key of the gamer. Hence, we generate a new one, based on the chosen nickname.

This is where we are going to use the Beacon SDK. We will sign the user nickname, then generate a Key pair (public and private key) from this signed nickname.

Why?

       1. Because using Beacon SDK to sign every transaction, would make the user signing every transaction to mint a cookie causing the gameplay to be far away from a cookie-clicker game.
       2. Because the VM-state of Deku is public we cannot store anything important in it (like your random secret or a way to retrieve it), moreover, in Deku, the state is saved for a public address. Hence, we need to remember who you are.

Using the Beacon SDK to sign your nickname, then using this signed-nickname as a seed to generate a KeyPair, is the best/quickest way to do so.

As soon as the user has fulfilled the nickname and node URI fields, we can use the Beacon SDK to sign the nickname when you click on the Beacon Wallet button.


const handleBeaconConnection = async () => {
    nodeUri = nodeUriRef.current?.value || "";
    nickName = nicknameRef.current?.value || "";

    if (nodeUri && nickName) {
        dispatch(saveConfig(nodeUri, nickName));
        const createWallet = (): BeaconWallet => {
            const Tezos = new TezosToolkit("https://mainnet.tezos.marigold.dev/");
            // creates a wallet instance if not exists
            const myWallet = new BeaconWallet({
                name: "decookies",
                preferredNetwork: NetworkType.CUSTOM,
            });

            // regarding the documentation this step is necessary
            Tezos.setWalletProvider(myWallet);
            return myWallet;
        }
        try {
            const wallet = latestState.current.wallet ? latestState.current.wallet : createWallet();
            await wallet.requestPermissions({
                network: {
                    type: NetworkType.CUSTOM,
                    rpcUrl: "https://mainnet.tezos.marigold.dev/"
                },
                // Only neede to sign the chosen nickname
                scopes: [PermissionScope.SIGN]
            });

            // sign the chosen nickname
            const seed = await wallet.client.requestSignPayload({
                signingType: SigningType.RAW, payload: stringToHex(nickName)
            }).then(val => val.signature);

            // get keyPair
            const rawKeyPair = await human.getKeyPairFromSeed(seed.toString(), "ed25519");
            const keyPair = getKeyPair(rawKeyPair);
            // save them in state to use them at each needed action
            dispatch(saveGeneratedKeyPair(keyPair))
            dispatch(saveWallet(wallet));
        } catch (err) {
            const error_msg = (typeof err === 'string') ? err : (err as Error).message;
            dispatch(addError(error_msg));
            throw new Error(error_msg);
        }
    } else
        dispatch(addError("Need to fulfill Nickname and Node URI"));
}

We don’t only store the cookieBaker in our state. We also dispatch some other useful information for our case (the generated keyPair and the Wallet)

Mint your first cookie!

Let’s define the business function!

We want to make the VM state to be successfully updated when our user clicks on the cookie. The action to perform is to add one cookie to the current amount of this user.

This function will create the correct payload to submit our operation with. Here is its signature:


export const mint = async (action: vmOperation, nodeUri: string, keyPair: keyPair | null): Promise => {
}

vmOperation is our type wrapping the “cookie” payload of the first tutorial:


{"type":"mint","operation":"cookie"}

On the Deku side, every payload must be signed.

We initialize an InMemorySigner from taquito with the keyPair we have generated before. The first step is to create a new one, given our private key:


export const mint = async (action: vmOperation, nodeUri: string, keyPair: keyPair | null): Promise => {
    const signer = new InMemorySigner(keyPair.privateKey)
}

Submit Operation using the Deku Toolkit (recommended)

Using the Deku toolkit is the easiest way to submit operations to Deku. It is available here

There are four steps:

       1. Create a InMemorySigner   
       2. Create a DekuSigner from the previous InMemorySigner   
       3. Create a DekuToolkit with the DekuSigner and the Node URI   
       4. Submit the operation


export const mint = async (action: vmOperation, nodeUri: string, keyPair: keyPair | null): Promise => {

    const inMemorySigner = new InMemorySigner(keyPair.privateKey)
    const dekuSigner = deku.fromMemorySigner(inMemorySigner);
    const dekuToolkit =
        new deku.DekuToolkit(
            {
                dekuRpc: latestState.current.nodeUri,
                dekuSigner: dekuSigner
            }
        );
    const hash =
        await dekuToolkit.submitVmOperation(JSON.stringify(action, stringifyReplacer));
}

If you choose to use the Deku Toolkit, you can directly go to the “Voilà” section. Because you already successfully submitted your first operation to Deku, and can get your first cookie!

Submit operation using fetch (not recommended)

Construct the payload

The payload we must provide to Deku is a bit complex, here is its structure in a JSON format:


{
 "key": "edpkuN8cpRSywEYBg2HvnCqvuLRPQat3EDKjFu8a6YReotpevbAvbe",
 "signature": "edsigtahhW6epxRPijrMLrRw39eDFKh8X3M64rMbcp8YXDmZJH1LnssZPvMC1ntU6Efke7qwRP5wS53gHFXiy8FWNgf4xzgn4pv",
 "operation": {
   "level": "9",
   "nonce": "2074395974",
   "source": "tz1fZvBpK632jM4ExhqrVzntbV99C6zHxsEw",
   "content": [
     "Vm_transaction",
     {
       "operation": "{\"type\":\"mint\",\"operation\":\"cookie\"}",
       "tickets": []
     }
   ]
 }
}

I start by explaining the operation nested object:

- level is the block-level you want your operation to be included (must be +15 actual blockchain level or -15) (see level section)

- nonce a random integer value lower than maximum int64 in OCaml (see nonce section)

- source is the public address we created in the first blog article using deku-cli create-wallet command

- content is a polymorphic array, whose first element is always the string "Vm_transaction" and second element is a JSON record where operation is the payload needed by our application (a simple string in our case). This is the last argument of deku-cli create-custom-transaction command in the previous blog post. In our case, the correct payload for this action is “cookie” which is a valid JSON object since it is a string.

To generate such a payload from our web front application, we can do something like:


const signer = new InMemorySigner(keyPair.privateKey)

const address = await signer.publicKeyHash();
const key = await signer.publicKey();
const content = ["Vm_transaction", {
    operation: JSON.stringify(action, stringifyReplacer),
    tickets: []
}];
const source = address;
let nonce = createNonce().toString();
const operation = {
    level,
    nonce,
    source,
    content
};

Nonce

On the Deku side, the nonce is just a random integer, we can easily do the same on our web front application using the following function:


export const createNonce = (): number => {
    const maxInt32 = 2147483647;
    const nonce = Math.floor(Math.random() * maxInt32);
    return nonce;
}

Level

This value must come from the Deku-P side. Hence, we need to fetch it from our Deku cluster!

Once you have successfully run your Deku Cluster (see in the first article on “How to run Deku cluster”)

You can simply copy/paste the following code, which requests the good endpoint:


export const requestBlockLevel = async (nodeUri: string): Promise => {
   const blockRequest = await fetch(nodeUri + "/api/v1/chain/level",
       {
           method: "GET"
       });
   const blockResponse = await blockRequest.json();
   return blockResponse.level;
}

Sign

And finally, we sign the operation, to get its signature:


const signature = await signer.sign(stringToHex(JSON.stringify(operation))).then((val) => val.prefixSig);

Submit the operation

Next and final step: submit this operation to the /api/v1/operations endpoint of one of our Deku nodes!

You can access the Node URI on localhost:8080 and perform the following:


const hash = await fetch(nodeUri + "/api/v1/operations",
    {
        method: "POST",
        body: fullPayload
    });

It returns the hash of the operation. This can be useful in some cases (not our cookie clicker game) to fetch another endpoint of Deku to get the list of included operations. Fetching it, will allow you to ensure your operation has been included!

🎉 Voilà!! 👏

You successfully submitted your first operation to Deku!!!!

Now, we can fetch the actual state of our VM to verify it has been successfully modified!

/api/v1/state/unix/

This is the endpoint we are going to fetch to get the famous cookieBaker we have defined!

To do so, you can simply copy/paste the following code snippet, which is simply getting data from one of the Deku nodes, and parsing it to only return the state corresponding to the user address:


export const getActualPlayerState = async (nodeUri: string, keyPair: keyPair | null): Promise => {
   if (keyPair) {
       const signer = new InMemorySigner(keyPair.privateKey)
       const userAddress = await signer.publicKeyHash();
       // TODO: migrate to "/api/v1/state/unix/" + useraddress
       const stateRequest = await fetch(nodeUri + "/api/v1/state/unix/",
           {
               method: "GET"
           });
       const stateResponse = JSON.parse(await stateRequest.text(), parseReviver);
       if (stateResponse) {
           const cookieBaker = stateResponse[userAddress];
           if (!cookieBaker) {
               return initialState;
           } else {
               return cookieBaker;
           }
       } else {
           console.log("no value in state");
           return initialState;
       }
   } else {
       throw new Error("NO PRIVATE KEY");
   }
}

And call it wherever it makes sense in your application. In the near future, you will be able to fetch directly on /api/v1/state/unix/tz1xxxx and avoid doing this parsing!

From now, we already were able to:

       - Call several HTTP endpoints of our Deku nodes
       - Update our VM state by minting a cookie
       - Retrieve the updated state

Mint your first cursor

Since we already provide the action (payload of deku-cli) we can easily mint our first cursor now, by defining the correct action in our reducer and simply calling the mint function with a different action!


mint("{\"type\":\"mint\",\"operation\":\"cookie\"}");

The mint function is simply wrapping the correct payload we give it, to create the corresponding operation record type!

🎉 Voilà!! 👏

You guessed it, it will be easy to add the action to mint a Grandma!

We successfully created a front web application interacting with the VM State we had defined in the first blog article!

Here is the complete mint function without the Deku toolkit:


export const mint = async (action: vmOperation, nodeUri: string, keyPair: keyPair | null): Promise => {
   if (keyPair) {
       try {
           const signer = new InMemorySigner(keyPair.privateKey)
 
           const address = await signer.publicKeyHash();
           const key = await signer.publicKey();
 
           const level = await requestBlockLevel(nodeUri);
           const payload = action;
           const content = ["Vm_transaction", {
               operation: JSON.stringify(payload, stringifyReplacer),
               tickets: []
           }];
           const source = address;
           let nonce = createNonce().toString();
           const operation = {
               level,
               nonce,
               source,
               content
           };
           const signature = await signer.sign(stringToHex(JSON.stringify(operation))).then((val) => val.prefixSig);
           const fullPayload = JSON.stringify({
               key,
               signature,
               operation
           }, stringifyReplacer);
 
           const hash = await fetch(nodeUri + "/api/v1/operations",
               {
                   method: "POST",
                   body: fullPayload
               });
           return hash.text();
       } catch (err) {
           const error_msg = (typeof err === 'string') ? err : (err as Error).message;
           throw new Error(error_msg);;
       }
   } else {
       throw new Error("No private key in local storage")
   }
}

You can find the whole code of our front application directly on our GitHub!

If you want to know more about Marigold, please follow us on social media (Twitter, Reddit, Linkedin)!

Scroll to top