A frontend for the rps chain

Published 7th March 2022.

This is a continuation of the previous rps article, to follow along either go through the previous articles or acquire v0.2 of the repository. Here we add a frontend that connects to Keplr with cosmjs.

Start with commenting out the upgrade registration in the end block hook in app/app.go.

Make a new directory in the rps folder and create a index.html containing a button that calls the following function:

async function connectKeplr(){
	await window.keplr.experimentalSuggestChain({
		chainId: "rps",
		chainName: "Rock paper scissors",
		rpc: "http://0.0.0.0:26657",
		rest: "http://0.0.0.0:1317",
		bip44: {
			coinType: 118,
		},
		bech32Config: {
			bech32PrefixAccAddr: "rps",
			bech32PrefixAccPub: "rps" + "pub",
			bech32PrefixValAddr: "rps" + "valoper",
			bech32PrefixValPub: "rps" + "valoperpub",
			bech32PrefixConsAddr: "rps" + "valcons",
			bech32PrefixConsPub: "rps" + "valconspub",
		},
		currencies: [ 
			{ 
				coinDenom: "MSTAKE", 
				coinMinimalDenom: "stake", 
				coinDecimals: 6, 
			}, 
			{
				coinDenom: "win",
				coinMinimalDenom: "win",
				coinDecimals: 0,
			},
			{
				coinDenom: "draw",
				coinMinimalDenom: "draw",
				coinDecimals: 0,
			},
		],
		feeCurrencies: [
			{
				coinDenom: "MSTAKE",
				coinMinimalDenom: "stake",
				coinDecimals: 6,
			},
		],
		stakeCurrency: {
			coinDenom: "MSTAKE",
			coinMinimalDenom: "stake",
			coinDecimals: 6,
		},
		coinType: 118,
		gasPriceStep: {
			low: 0,
			average: 0.001,
			high: 0.003,
		},
	});
	window.keplr.enable("rps");
}

Unfortunately Keplr is not injected when we view this as a file, so we have to serve it, for example through node with npx http-server

After pressing the button you will be able to send tokens through the Keplr extension.

Next run npm init and npm install ts-proto --save-dev which is needed for protoc, create tx.ts with the following command:

protoc \
	--plugin="./node_modules/.bin/protoc-gen-ts_proto" \
	--ts_proto_out="./" \
	--proto_path="../proto/rps" \
	--ts_proto_opt="esModuleInterop=true,forceLong=long,useOptionals=messages" \
	"../proto/rps/tx.proto"

Install the tsc typescript compiler with npm install -g typescript and run it: tsc tx.ts

It gives an error that protobufjs/minimal has no default export, so remove all instances of ["default"] in tx.js with a search and replace. The other errors regarding missing fields can apparently be ignored.

Next install the stargete cosmjs package with npm install @cosmjs/stargate

We have to register the correct path for our transactions, it can be found in the init function of x/rps/types/tx.pb.go: proto.RegisterType((*MsgJoinQueue)(nil), "marcus.appelros.rps.rps.MsgJoinQueue")

Create a index.js file and fill it with the following:

import { Registry } from "@cosmjs/proto-signing";
import {
	defaultRegistryTypes as defaultStargateTypes,
	SigningStargateClient,
} from "@cosmjs/stargate";
import { MsgJoinQueue } from "./tx.js";

var rps = {};
window.rps = rps;

const customRegistry = new Registry(defaultStargateTypes);
customRegistry.register("/marcus.appelros.rps.rps.MsgJoinQueue", MsgJoinQueue);

async function getClient() {
	await window.keplr.enable("rps");
	const offlineSigner = window.keplr.getOfflineSigner("rps");
	const client = await SigningStargateClient.connectWithSigner(
		"http://0.0.0.0:26657",
		offlineSigner,
		{ registry: customRegistry },
	);
	return client;
}
rps.getClient = getClient;

async function joinQueue() {
	const client = await getClient();
	const accounts = await client.signer.getAccounts();
	const message = {
		typeUrl: "/marcus.appelros.rps.rps.MsgJoinQueue",
		value: MsgJoinQueue.fromPartial({
			creator: accounts[0].address,
		}),
	};
	const fee = {
		amount: [
			{
				denom: "stake",
				amount: "100",
			},
		],
		gas: "100000",
	};
	const response = await client.signAndBroadcast(accounts[0].address, [message], fee);
	return response;
}
rps.joinQueue = joinQueue;

Next we assemble the code for importing in index.html with webpack, using webpack-5 is a little finicky so I will use version 4: npm install -g webpack@4.46.0 webpack-cli

Create webpack.config.js:

module.exports = {
  entry: './index.js',
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js',
  },
};

Then run webpack and import dist/bundle.js in index.html, refresh the served file with ctrl-shift-r and try rps.joinQueue() in the console.

Since in production one would likely use Vue or another framework we simply use the console here.

Our code gets a new client before sending a transaction, if you want a persistent client and/or display data it is helpful to have it update automatically when a user changes their Keplr account: window.addEventListener("keplr_keystorechange", updateInfo)

After starting a match we want to be able to check this games id through the player map so we can submit moves correctly, in proto/rps/query.proto you can see the built in paths to accomplish this: option (google.api.http).get = "/marcus.appelros/rps/rps/player/{index}";

So we can simply visit http://0.0.0.0:1317/marcus.appelros/rps/rps/player/rps1xyz or http://0.0.0.0:1317/marcus.appelros/rps/rps/player to view all players. If you want this functionality through cosmjs check out this guide.

Now it is time to add functionality for submitting moves. In index.js import MsgSubmitMove from tx.js and register it with the correct path, then add the following:

async function submitMove(matchId, moveId) {
	const client = await getClient();
	const accounts = await client.signer.getAccounts();
	const message = {
		typeUrl: "/marcus.appelros.rps.rps.MsgSubmitMove",
		value: MsgSubmitMove.fromPartial({
			creator: accounts[0].address,
			matchId: matchId,
			moveId: moveId,
		}),
	};
	const fee = {
		amount: [
			{
				denom: "stake",
				amount: "100",
			},
		],
		gas: "100000",
	};
	const response = await client.signAndBroadcast(accounts[0].address, [message], fee);
	return response;
}
rps.submitMove = submitMove;

Recompile with webpack, refresh and try rps.submitMove(0,1) with appropriate numbers.

That's it! For the complete code check out the frontend folder in the repository.