Integrating JavaScript with the Casper blockchain

This tutorial will give an introduction on how to integrate an example JavaScript program with the Casper block chain using the Google Chrome plugin called Casper Signer. The contract used is the one created in the previous video:

Creating a smart contract in Rust

And this contract was deployed to the Casper blockchain in this video:

Deploying a smart contract to the test Casper network

Throughout this tutorial series it will be shown how to develop a simple web based game that will interact with the Casper blockchain. The game will be written in JavaScript and the Casper blockchain is used to store the players high score for the game. A contract is created that can store and retrieve the high-score from the blockchain. The contract is then deployed to the Casper network. Now it will be shown how to integrate a Javascript game with the deployed contract.

The final game can be seen here:

Full source code can be found here:

https://github.com/playcasper/snake-casper-game

Pre-requisites:

  • Have a Linux development environment setup which includes Cmake, Cargo and Rust
  • Have some JavaScript programming knowledge
  • Have a contract compiled down to WASM
  • Have a contract deployed to the Casper test network

Game Overview

The game that is covered in this tutorial is the classic game “Snake”. The snake is controlled with the arrow keys and the aim is to eat as many apples as possible without touching either the edge or the snake itself.

At the top of the display is the current highest score which is pulled from the blockchain. The users current score is shown just below. Once the game is over, the user is given the option to save their score. If “Save” is selected then the Casper Signer plugin is automatically launched. If the extension is not already installed then the user will be asked to install it before proceeding. Once logged in to the Casper Signer the user is shown the information that is going to be saved. This needs to be approved by the user in the form of signing this deployment. This is then submitted to the block chain and the program polls the status until a valid response is returned. This can take up to a minute. In the meantime, if the user has played the game before, then the program pulls the user’s own individual highest score back from the block chain. Once complete, the user is able to play the game again.

Snake JavaScript Code

The first file to look at is the landing page for the game. This is the snake.html file. Here the display is broken down in to several divisions. There is the highest score div, the users current score section and then the game container where the main canvas is created.

	<div id="container">
		<img src="https://playcasper.io/wp-content/uploads/2022/09/Monkey7a.png" class="logo">
		<div id="high_score">Highest Score: (retrieving from block chain)</div>
		<div id="score">Current Score: </div>
		<div id="game-container">
			<canvas id="snakeboard"></canvas>
		</div>
		<div id="footer"></div>
	</div>

This division is used to show a countdown, in the center of the display, before the game starts.

        <div class="countdown" id="countdown">
        </div>

Then there is the popup that is shown when the game is over with the button to save the users score.

	<div class="popup" id="popup">
		<a href="#" id="popupClose" title="Close" class="modal-close">X</a>
		<h1>Game Over</h1>
	
		<div class="center" id="saveScoreDiv" style="display:none">
			<p>Click here to save your score</p>
			<button type="button" id="saveScore">Save</button>
			<div id="submission_response"> </div>
			<div id="users_high_score"> </div>
		</div>
		
		<div class="center" id="casperSignerDiv">
			<p>Sign in to Casper Signer to save your score!</p>
			<p><img class="center" style="width:100px" src="/src/casper-signer.png"></p>
			<button type="button" id="signIn">Sign In</button>
			<button type="button" onclick="location.href='https://chrome.google.com/webstore/detail/casper-signer/djhndpllfiibmcdbnmaaahkhchcoijce'">Download Signer</button>
		</div>
	</div>	

The two JavaScript files are loaded:

snake.js which actually controls the game and
main.js which handles all the interfacing to the block chain

        <script src="/src/snake.js"></script>
        <script src="/src/main.mjs"></script>

It is outside the scope of this tutorial to go in to a lot of detail with the game itself but here we describe the basics of the snake.js file. The full code can be found here:

https://github.com/playcasper/snake-casper-game

A tiled PNG image is used to easily store all the images for the snake in its various orientations and the other graphics such as the apple. This allows us to easily access each image through one file.

	// Load graphics for snake and food
	tileimage = new Image();
	tileimage.src = 'src/snake-graphics.png'

Then there is an array to store all of the sections of the snake. The X and Y coordinates for each segment and which direction that particular snake segment is pointing. This array grows as the size of the snake grows.

	// Define array for snake
	// X offset, Y offset and Direction
	let snake = [ {x: 0, y: 0, d: 'Up'} ]

The size of the board is setup to fill the display.

	// Get the canvas element and dynamically set the height and width
	const snakeboard = document.getElementById("snakeboard");
	snakeboard.height = Math.floor(document.getElementById("game-container").clientHeight / cellSize) * cellSize;
	snakeboard.width = Math.floor(document.getElementById("game-container").clientWidth * 0.5 / cellSize) * cellSize;

	// Return a two dimensional drawing context
	const snakeboard_ctx = snakeboard.getContext("2d");

Then there are a number of functions to:

reset the game to the starting status
generate a random location for the apple to appear
clear the board of any graphics
draw the apple in the chosen location
move the snake by one cell in whichever direction its pointing
and start the initial 3 second timers to start the game

An event handler is setup to listen for any key press which fires the function change_direction().

	// Listen for any key presses
	document.addEventListener("keydown", change_direction);

The timer function runs for 3 seconds and then the game starts by calling the main function.

function startTimer()
{
	if(counter === 0)
	{
		document.getElementById("countdown").innerHTML = "";
		main();
	}
	else
	{
		document.getElementById("countdown").innerHTML = counter;
		
		setTimeout(function onTick() {
			counter = counter - 1;
			startTimer(); 
		}, 1000)
	}
}

A check is made to see if any end of game conditions have been met. If not then the board is cleared, the apple is redrawn in the same position, the snake is moved and drawn on the display and then the sequence is started again. The rest of the implementation of each of the functions is relatively simple and self-explanatory with a little basic programming knowledge.

When the game ends, a popup to save the score is made to appear and all the other controls are cancelled.

		// Blur the background and display end of game popup
		document.getElementById("popup").style.display = "block";
		document.getElementById("container").classList.add("blur");
		document.getElementById("submission_response").innerHTML = "";
		document.getElementById("users_high_score").innerHTML = "";

This is where the other JavaScript files, main.mjs and app.js, interact with the Casper block chain.

Node.js is used to run the game and the app.js file is setup to listen on port 9000 in a standard way using the express module. The casper-js-sdk library is ued to allow us to build correctly configured requests and deployments to the block chain. The SDK is set up to listen on the same peer where the contract was deployed.

One major detail should be noted about using node.js. This is not usually a problem when running on a local host but the intention here is to run this game on a live server. This means that the calls to the Casper Blockchain peer through the casper JavaScript SDK would normally return in a CORS error if not run through a proxy server. CORS stands for Cross-Origin Resource Sharing and is a protocol that prevents websites from calling other websites on different domains with JavaScript requests, which could potentially be a security risk. To get around this problem a proxy server is used in our node.js application and the necessary calls from the main.js file have to be posted back to the main domain.

This process can be demonstrated when we first load the JavaScript file when the game loads. The getHighScore() function is called to get the current highest score from the blockchain. This works by posting a web request back to the proxy server and waits for a reply.

// Get the highest score from the blockchain
function getHighScore(){
    // Post a request to get the highscore
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/highscore", true);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send();
    xhr.onload = function() {
        // Update the highest score display
        highestScore = this.responseText;
        document.getElementById("high_score").innerHTML = "Highest Score: " + highestScore;
    }     
}

The proxy server (in app.js)picks up the request and calls the casper-js SDK to get the latest block state and then the current state root hash. This is combined with the account hash for the deployment and the name of the Casper contract. The result is a JSON representation of the query as was shown in the previous video where we made a similar query to the blockchain. This JSON is parsed and then it is possible to loop through the elements to get the information about the highscore. This value is then returned back to the calling JavaScript and the highest score value is displayed.

// Get the highscore from the block chain
app.post('/highscore', async (req, res) => {
    var hash = process.env.ACCOUNT_HASH;
    var highscore = "0";

    // Get the state root hash
    const latestBlock = await casperService.getLatestBlockInfo();
    const stateRootHash = await casperService.getStateRootHash(latestBlock.block.hash);

    // Get the highscore from the contract
    const result = await casperService.getBlockState(
        stateRootHash,
        hash,
        ['highscore']
    );

    // Parse the json response to get the highscore
    var obj = JSON.parse(JSON.stringify(result));

    for (let i = 0; i < obj.Contract.namedKeys.length; i++) {
        if(obj.Contract.namedKeys[i].name == "highest_score") {
            const scoresData = await casperService.getBlockState(
                stateRootHash,
                hash,
                ['highscore',obj.Contract.namedKeys[i].name]
            );
                
            highscore = JSON.parse(JSON.stringify(scoresData)).CLValue;
        }
    } 

    // Return the highscore
    res.status(200).send(highscore);
});

That is basically how data can be easily retrieved from the block chain.

Now we are going to go through an example of saving a score to the blockchain.

When the user clicks on the “Save” button we have an event listener that fires. A basic check is performed to see if the new users score is higher than the highest score. If it is, then the display is updated. Then the SaveScore() function is called.

// Event for saving the highscore
const btnSaveScore = document.getElementById("saveScore");
btnSaveScore.addEventListener("click", async () => {
    // Retrieve the users high score after playing the game
    usersHighestScore = document.getElementById('score').innerHTML;

    // If the users highscore is greater than the previous record then update the display    
    if(usersHighestScore > highestScore)
    {
        document.getElementById("high_score").innerHTML = "Highest Score: " + usersHighestScore;
    }
    
    await SaveScore();
});

The Casper Signer Chrome extension is called. This call pops up the Casper Signer window and then a check is made to see if the extension is connected to the website. If it is connected then the current user’s public key is retrieved. Then the saveHighScore() function is called. A request back to the proxy server is made to set up the deploy request to save the score.

// Save the users score. Create the deploy, sign, make the deploy and check the deploy
async function SaveScore(){
    // Call the Casper Signer app to request a connection
    await window.casperlabsHelper.requestConnection();
    
    // Check that the app is connected
    const isConnected = await window.casperlabsHelper.isConnected()

    retryCount = 24;

    if(isConnected){
        // Use the Casper Signer app to get the users public key
        const publicKey = await window.casperlabsHelper.getActivePublicKey();
        
        // Call function to save the score
        reply = await savetHighScore(publicKey, usersHighestScore);
    
        // Check the status of the reply
        if (reply != "Failed")
        {
            // The reply is good, so use the Casper Signer app to sign the deploy
            const signedDeployJSON = await window.casperlabsHelper.sign(reply, publicKey);
            
            // Post a request to make the deploy
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "/deploy", true);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.send(JSON.stringify(signedDeployJSON));
            xhr.onload = function() {
                if(this.responseText == "Error")
                {
                    // An error occurred during the deploy
                    document.getElementById("submission_response").innerHTML = "Insufficient funds in account.<br>Get some more Casper!<br><br>";
                }
                else
                {
                    // No errors occurred during the deploy
                    document.getElementById("submission_response").innerHTML = "Your score has been submitted to the Casper blockchain.<br>Please wait for a reply....<br>(This can take a minute!)<br><br>";
                    
                    getUsersHighScore(publicKey);

                    // Get the deploy hash and check for the deploy status
                    deployHash = this.responseText;
                    checkDeployStatus();
                }
            }
        }
        else
        {
            // An error occurred during the deploy
            document.getElementById("submission_response").innerHTML = "Saving the score has failed<br><br>";
        }
    }
    else
    {   
        // Capser Signer App is not connected
        alert("Press connect on your Capser Signer App.");
        window.casperlabsHelper.requestConnection();
    }
}

// Function to make the request to create the deploy for signing
async function savetHighScore(publicKey, score){
    // Post a request to create the deploy
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/savehighscore", false);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send(JSON.stringify({
        value1: publicKey,
        value2: score
    }));
    
    if (xhr.status === 200) {
        return JSON.parse(xhr.responseText);
    }
    
    return "Failed";
}

The deployment is made up of three sections, the deploy parameters, session information and a payment amount. The deploy parameter selects whether the test Casper blockchain is being used or the real one. Then the session information defines the contract hash, the contract entry point, and the arguments that are to be sent to the contract.

// Set up the deploy for signing
app.post("/savehighscore", async (req, res) => {
    let publicKey  = req.body.value1;
    let score  = req.body.value2;

    // Set up the deploy params 
    const deployParams = new DeployUtil.DeployParams(CLPublicKey.fromHex(publicKey), "casper-test");
  
    // Set up the arguments for the contract call
    const args = RuntimeArgs.fromMap({
        name: new CLString(publicKey),
        value: new CLI32(score)
    });
     
    // Get the contract hash
    const contractHash = decodeBase16(process.env.CONTRACT_HASH);

    // Configure the deploy with entry point and payment
    const session = DeployUtil.ExecutableDeployItem.newStoredContractByHash(
        contractHash,
        "highscore_set",
        args
    );
      
    let payment = DeployUtil.standardPayment(process.env.SAVE_PAYMENT);
     
    const deploy = DeployUtil.makeDeploy(
        deployParams,
        session,
        payment
    );
    
    const deployJSON = DeployUtil.deployToJson(deploy);

    // Return the deploy for signing
    res.status(200).send(deployJSON);
});

This information is combined with a payment amount in to a deployment and then converted in to JSON. This is then returned back to the main JavaScript file to be given to the Casper Signer and displayed to the user for Signing.

Once the user agrees to the deployment and clicks on “Sign” then the signed deployment is sent back to the proxy server for final deployment.

// Make the deploy to the block chain
app.post("/deploy", async (req, res) => {
    
    let deployHash = "Error";
    
    try {
        // Make the deploy
        const signedDeploy = DeployUtil.deployFromJson(req.body).unwrap();
        const deployResult = await casperService.deploy(signedDeploy);
        
        // Get the deploy hash
        deployHash = JSON.parse(JSON.stringify(deployResult)).deploy_hash;
    } catch (error) {
    }
    
    res.status(200).send(deployHash);
});

Here the signed deployment is sent to the Casper blockchain and we retrieve the deployment hash. This value is used to query the status of the deployment.

If everything works correctly then an asynchronous call is made to retrieve the users current high score in a similar manner to how the overall highest score was previously retrieved.

The current deployment status is checked by setting a timer that runs every 5 seconds. An attempt to get the deploy results is made using the deployment hash. Again, the proxy server is called with a web request passing the deploy hash.

// Check the deploy status on a 5 second timer
function checkDeployStatus() {
    getDeployResultInterval = setInterval(() => {
        getDeployResult();
    }, 5000);
}

// Check the deploy status
async function getDeployResult() {
    try {
        // Post a request to get the deploy status
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "/getdeloystatus", true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.send(JSON.stringify({
            value: deployHash
        }));        
        xhr.onload = function() {
            // Parse the response
            deployInfo = this.responseText;
            // Check the execution results
            if (JSON.parse(deployInfo).execution_results.length == 1) {
                // Execution results are present so stop the timer and check the result
                clearInterval(getDeployResultInterval);
                if (JSON.parse(deployInfo).execution_results[0].result.hasOwnProperty("Success")) {
                    document.getElementById("submission_response").innerHTML = "Your score has been successfully saved on the Casper blockchain!<br><br>";
                } else {
                    document.getElementById("submission_response").innerHTML = "There was an error: " + JSON.stringify(JSON.parse(deployInfo).execution_results[0].result.Failure.error_message);
                }        
            }
        }
    } catch(error) {
        alert('getDeployResult: ' + error.message);
    }
    
    if (retryCount <= 0)
    {
        alert("Deploy checking timed out");
        clearInterval(getDeployResultInterval);
    }
        
    retryCount = retryCount - 1;
}
// Check the status of the deploy status
app.post("/getdeloystatus", async (req, res) => {
    
    let returnMessage = null;
    
    try {
        deployHash = req.body.value;

        const deployInfo = await casperService.getDeployInfo(deployHash);
        returnMessage = deployInfo;
    } catch(error) {
        returnMessage = error.message;
    } 
    
    // Return deploy status
    res.status(200).send(returnMessage);
});

The current deployment status is retrieved and returns the JSON result to be parsed. This is checked to see if there is an answer in the execution_results. If present, then the timer is stopped and a check is made to see if the result is an overall success which shows that everything was saved correctly. If not, then the user is notified of the error.

Running on the Linux Server

To run the game on the Linux server some components need to be installed including node.js, npm, express and casper-js-SDK. Run the following commands:

sudo apt install nodejs
sudo apt install npm
npm install express
npm install casper-js-sdk --save
npm install dotenv --save

Ensure that the Chrome browser is installed on your system and that the Casper Signer extension is also installed.

Start node.js using the command:

node app.js

Then open Chrome and visit the URL:

http://localhost:9000

The game should now be running.

This is how a simple JavaScript game can interact with a Casper contract that is deployed on the Casper blockchain using the Chrome Casper Signer extension.