# Integrate CosmJS and Keplr
Make sure you have all you need before proceeding:
- You understand the concepts of CosmJS.
- You have the checkers CosmJS codebase up to the external GUI. If not, follow the previous steps or go ahead and clone and checkout this branch (opens new window) to get the version needed for this tutorial.
In the previous sections:
- You created the objects, and the messages and clients that allow you to interface any GUI with your checkers blockchain.
- You learned how to start a running checkers blockchain, in Docker or on your local computer.
- You imported an external checkers GUI to use.
Now, you must integrate the two together:
- Adjust the React app to be able to package CosmJS.
- Work on the CosmJS integration.
For the CosmJS integration, you will:
- Work with the GUI's data structures.
- Fetch all games from the blockchain and display them (without pagination).
- Integrate with Keplr for browser-based players.
- Create a new game.
- Fetch a single game to be played.
- Play on the game, making single moves and double moves.
This exercise assumes that:
You are running your checkers blockchain with:
And have the following in
.env
:
# Prepare the integration with the checkers blockchain
As always, when moving code to a browser, adjustments are needed.
# Prepare Webpack
Your GUI uses React v18, which uses Webpack v5. Therefore you need to adjust Webpack's configuration (opens new window) to handle some elements (see also the CosmJS documentation). To modify the Webpack configuration in a non-ejected React app, use react-app-rewired
(opens new window) as explained here (opens new window):
Install the new package:
Add a new
config-overrides.js
:You can also pass along the
RPC_URL
as an environment variable, as seen in the code block above.Change the run targets to use
react-app-rewired
inpackage.json
:Be careful not to put
react-app-rewired
in the"eject"
command.
See a previous section for how to set RPC_URL
in process.env.RPC_URL
. It also assumes that you have an RPC endpoint that runs the checkers blockchain, as explained in the previous section.
# GUI data structures
The checkers GUI uses different data structures, which you must understand to convert them correctly and with less effort.
The
IPlayerInfo
has aname: string
(opens new window) field which can be used as the player's address.The
IGameInfo
has aboard: number[][] | null
(opens new window) field. You must do a conversion fromb*b*...
to this type. And at some point, you must ensure that the alignments are correct.The
IGameInfo
has aturn: number
(opens new window), which can be mapped to"b"
or"r"
.The
IGameInfo
lacks anindex
orid
field. Instead the GUI developer identified games by their index in an array, which is not adequate for this case. You must add anindex
field toIGameInfo
. This is saved as astring
in the blockchain but in practice is a number, which the GUI code expects as game index. Add:To avoid compilation errors on
index:
elsewhere, temporarily addindex?: number
. Remember to come back to it and remove the?
.
# Board converter
In order to correctly convert a blockchain board into a GUI board, you want to see how a GUI board is coded. In components/Game/Board/Board.tsx
, add a console.log(props.squares)
(opens new window). Let React recompile, open the browser console, and create a game. You should see printed:
Where 1
represents player 1, i.e. black pieces. Compare that to how a game is saved in the blockchain:
To convert the board, create a new file src/types/checkers/board.ts
and code as follows:
# Game converter
Next you convert a StoredGame
into the IGameInfo
of the GUI:
Here:
- You use Cosmos addresses instead of names.
- You put today in the creation date because it is not stored in
StoredGame
. - You set the
last
played date to deadline minus expiry duration (an adequate solution). - The possibility of "AI" is not important.
Could an AI player and the blockchain mix together? If the AI has access to a private key that lets it send transactions, it would be a bona fide player. This could be implemented with backend scripts running on a server. See checkers backend scripts for an example of backend scripts for a different use-case.
# Obtain a client
For your React components to communicate with the blockchain they need to have access to a StargateClient
, and at some point to a SigningStargateClient
too. Among the information they need is the RPC URL.
# Pass RPC parameters
The RPC URL is already saved into process.env.RPC_URL
thanks to react-app-rewired
. However, it is better to reduce your components' reliance on process.env
. A simple way to give the RPC URL to the components is for them to:
- Receive an
rpcUrl: string
in the properties. - Keep a
StargateClient
in the component's state that would be instantiated lazily.
With this setup, only index.tsx
would use process.env.RPC_URL
.
For instance, prepare access to rpcUrl
for MenuContainer.tsx
by adding it to the following:
The properties of
src/components/Menu/MenuContainer.tsx
:The properties of the component call stack, first in
src/components/App.tsx
:You have to create the missing
AppProps
entirely.Finally, to
src/index.tsx
:If your compiler still believes that
.RPC_URL
isstring | undefined
, you may append an!
to force it tostring
.
Whenever another component needs the rpcUrl
in this tutorial, adapt and reproduce these steps as necessary.
# Create the StargateClient
Whenever you need a StargateClient
in a component, you can repeat the following chain of actions for the component in question. For instance, to prepare the client in the state of src/components/Menu/MenuContainer.tsx
:
Add the field in the state:
Initialize it to
undefined
in the constructor:Add a method that implements the conditional instantiation:
This creates it only once by saving the client in the state.
Instantiating a CheckersSigningStargateClient
is more involved, as you will see later on.
First, make use of what you already prepared. Why choose to start with MenuContainer
? Because that is where all the games are listed.
# Show all games
To show all games, you only need the read-only StargateClient
. There is no need to worry about private keys for now. Where do you query for the games?
# All games container
Look into src/components/Menu/MenuContainer.tsx
for the componentDidMount
method, which accesses the browser storage for saved games. You can replace the storage logic with the following steps:
- Obtain a
StargateClient
with thegetStargateClient
method. - Obtain the blockchain games (without pagination).
- Convert the blockchain games to the format the component expects.
- Save them in
saved: IGameInfo[]
of the component's state.
Before adding lines of code in the component, a good next step is to add a new function to CheckersStargateClient
to handle the call and the conversion, or to add a helper function that takes a client as a parameter.
# A specific GUI method
In the future you may want to reuse the CheckersStargateClient
code in another GUI or for backend scripts. To avoid polluting the code, and to avoid a less-elegant helper where you need to pass a Stargate client as a parameter, add an extension method (opens new window) to CheckersStargateClient
.
In a new src/types/checkers/extensions-gui.ts
:
Declare the new extension method that extends your
CheckersStargateClient
module:Add its implementation:
Note this does not care about pagination or games beyond the first 20. This purely-GUI detail is left as an exercise.
# All games call
Next, in MenuContainer.tsx
:
Add an empty
import
for the extension method file, otherwise the method is not known nor compiled:Change
componentDidMount
toasync
and replace the storage code with a call to the new method:
Restart npm start
and you should see the list of games in your running chain.
Remember that this exercise assumes that you are running a checkers chain as described at the beginning of this section.
If your list is empty, you can quickly create a game with:
# Individual game menu container
Each game is now listed as a src/components/Menu/MenuItem.tsx
(opens new window). However, in src/components/Menu/Menu.tsx
the index
of the game is taken from its index in the games array, which is not what you want here. The list needs to use the proper index
of each game. In Menu.tsx
make this change:
If the compiler complains, temporarily add !
(as in game.index!
) if you previously added the IGameInfo.index
as index?: number
.
# Show one game
What happens when you click on a game's Resume Game? It opens a page of the form http://localhost:3000/play/1
. The component is src/components/Game/GameContainer.tsx
, from which you have to pick the game information from the blockchain. This process begins as with MenuContainer
:
- Pass a
rpcUrl
(opens new window) along the chain toGameContainerWrapper
(opens new window) then theGameContainer
(opens new window). - Add a
client: CheckersStargateClient | undefined
(opens new window) state field, initialize it (opens new window), and add a lazy instantiation method (opens new window).
Looking into GameContainer
, you see that componentDidMount
gets all the games from storage then sets the state with the information. Instead, as you did for all games, you have to take the game from the blockchain.
In
extensions-gui.tsx
declare another extension method toCheckersStargateClient
dedicated to getting the game as expected by the GUI:Now define
getGuiGame
:In
GameContainer.tsx
add an empty import to the extension methods:Make the
componentDidMount
methodasync
:Split the
componentDidMount
method, and move the game loading lines to their ownloadGame
function:In
loadGame
replace the storage actions with a fetch from the blockchain:Adjust the
setState
call so thatisSaved
is alwaystrue
:Here:
- You force
isSaved
totrue
since it is always saved in the blockchain. - Saving
game.index
to state is unimportant because it is actually passed inIGameContainerProps.index
. - By having a separate
loadGame
, you can call it again if you know the game has changed. - It is important that this component fetches the game from the blockchain on its own, because the game page
http://.../play/1
could be opened out of nowhere and may not have access to the list of games. Moreover, it may be entirely missing from the list.
- You force
Restart npm start
and you should now see your game.
To quickly make a move on your created game with an id of 1
, you can run:
Refresh the page to confirm the change.
# Integrate with Keplr
So far you have only made it possible to show the state of games and of the blockchain. This allows your users to poke around without unnecessarily asking them to connect their wallet and thereby disclose their address. However, to create a game or play in one, users need to make transactions. This is where you need to make integration with the Keplr wallet possible.
Install the necessary packages:
# Identify the checkers blockchain
Keplr will need information to differentiate your checkers blockchain from the other chains. You are also going to inform it about your REST API.
Adjust your
.env
file:Adjust the associated type declaration:
Adjust its passing through Webpack:
With this, prepare your checkers blockchain info in a new src/types/checkers/chain.ts
file:
Note the use of process.env.RPC_URL
again.
The chainId
value has to match exactly that returned by client.getChainId()
, or the transaction signer will balk. The ChainInfo
object is copied from the one you used for Theta in the first steps with Keplr section.
# Prepare a signing client
Just as components that need a CheckersStargateClient
keep one in their state, components that need a SigningCheckersStargateClient
keep one in their state too. Some components may have both.
A component may have both CheckersStargateClient
and SigningCheckersStargateClient
. That is not a problem. In fact it may benefit your project, because with a simple CheckersStargateClient
you can let your users poke around first, and build their trust, before asking them to connect their Keplr account.
The steps to take are the same for each such component.
For instance, in src/components/Menu/NewGameModal/NewGameModal.tsx
:
Add an
rpcUrl: string
(opens new window) to the props, passed alongMenu.tsx
(opens new window) which also needs it as a new prop (opens new window), in turn passed fromMenuContainer.tsx
(opens new window).Add a signing client to the component's state, as well as the potential address of the Keplr user:
Do not forget to initialize them to nothing in the constructor:
The address obtained from Keplr is saved in
creator
, because it is accessible from theOfflineSigner
but not from theSigningStargateClient
.Add a tuple type to return both of them:
This is done because the
setState
function does not ensure the state is updated immediately after it has been called, so the lazy instantiation method has to return both.Inform TypeScript of the Keplr
window
object:Add a function that obtains the signing client and signer's address (a.k.a. future transaction
creator
) by setting up Keplr and connecting to it:Setting up Keplr is idempotent (opens new window), so repeating these operations more than once is harmless. You may want to separate these actions into more defined methods at a later optimization stage.
Note too that a default gas price is passed in, so that you can use"auto"
when sending a transaction.
Your component is now ready to send transactions to the blockchain. Why choose NewGameModal.tsx
as the example? Because this is where a new game is created.
# Create a new game
This modal window pops up when you click on New Game:
The player names look ready-made to take the player's blockchain addresses.
Examine the code, and focus on src/components/Menu/NewGameModal/NewGameModal.tsx
. Thanks to the previous preparation with CheckersSigningStargateClient
it is ready to send transactions:
In
extensions-gui.ts
declare an extension method to yourCheckersSigningStargateClient
that encapsulates knowledge about how to get the newly created game index out of the events:Now define it:
Keep in mind:
- For the sake of simplicity, a possible wager is completely omitted.
- The
getCreatedGameId
is defined in a previous section.
In
NewGameModal.tsx
add an empty import of the extension method so that it is compiled in:Since you are going to paste addresses into the name field, make sure that the GUI does not truncate them. In
src/components/Menu/NewGameModal/PlayerNameInput.tsx
:Back in
NewGameModal.tsx
, change the declaration ofhandleSubmit
and make itasync
:In
handleSubmit
, avert the need to save to local storage and call your create game extension method:Next send the player directly to the newly created game:
In
render()
change the React link around theButton
to a regulardiv
so that window redirection appears smooth:
You have now added game creation to your GUI.
If you do not yet know your Keplr address on the checkers network, you will have to test in two passes. To test properly, you need to:
Run the initialization code by pretending to create a game. This makes Keplr prompt you to accept adding the
checkers
network and accessing your account. Accept both, but optionally reject the prompt to accept a transaction if your balance is zero.Select Checkers in Keplr. Make a note of your address, for instance
cosmos17excjd99u45c4fkzljwlx8eqyn5dplcujkwag8
.Use the faucet you started at the beginning of this section to put enough tokens in your Keplr Checkers account.
"1000000stake"
will satisfy by a 10x margin.Now start again to actually create a game. Accept the transaction, and you are redirected to the game page. This time the content of the game is from the blockchain.
# Play a move
In the GameContainer.tsx
there are makeMove
and saveGame
functions. In a blockchain context, saveGame
is irrelevant, as on each move done with a transaction the game will be automatically saved in the blockchain. So add index: -1
in the game to be saved to get past the compilation error.
Add a console.log
to makeMove
to demonstrate the format it uses:
Now play a move with the current interface. Move your first black piece:
In the blockchain code, this is fromX: 1, fromY: 2, toX: 2, toY: 3
. However, the GUI prints:
Evidently for each position X and Y are flipped. You can encapsulate this knowledge in a helper function:
This is likely a familiar situation. Go back to the board and do as if your piece was moving twice. This time, the GUI prints an array with three positions:
This may not be a legal move but it proves that the GUI lets you make more than one move per turn. You can use this feature to send a transaction with more than one move.
Do some more preparation:
In
extensions-gui.ts
declare an extension method toCheckersStargateClient
to check whether the move is valid, with parameters as they are given in the GUI components:Now define it:
Note that due to the limits of the
canPlayGuiMove
function, you can only test the first move of a multi-move turn. That is, it uses onlypositions[0]
andpositions[1]
of a potentially longer move. You cannot just test the next moves anyway because it will be rejected for sure. For it to be passed, the board would have to be updated first in the blockchain state.Declare another extension method, this time for
CheckersSigningStargateClient
, to actually make the move with parameters as they are given in the GUI components:It returns the captured pieces. Now define it:
Note how it maps from the positions array except for the last position. This is to take the moves out of the positions.
With this done:
Repeat in
GameContainer
what you did inNewGameModal
:- Keep a signing client and
creator
(opens new window) in the component's state. - Initialize (opens new window) it in the constructor.
- Add a tuple (opens new window).
- Add a method to lazily instantiate it (opens new window).
- Keep a signing client and
Change the declaration of
makeMove
to make itasync
:In
makeMove
make sure that the move is likely to be accepted immediately after the existing code that extracts the move. It uses the read-onlyStargateClient
, which allows players to look around without being asked to disclose their address:The move test is made with the read-only client. This is so that your users can poke around without connecting for as long as possible.
With this partial assurance, you can make an actual move:
The assurance is partial because what was tested with
canPlayGuiMove
is whether a player of a certain color can make a move or not. When making the move, it takes the Keplr address without cross-checking whether this is indeed the address of the colored player that tested a move. This could be improved either at the cost of another call to get the game, or better by adding the black and red addresses in the component's status.The remaining code of the
makeMove
method can be deleted.Finish with a reload of the game to show its new state:
There is a potentially hard-to-reproduce-in-production race condition bug here. The
loadGame
is done immediately after the transaction has completed. However, depending on the implementation of the RPC endpoint, theplayGuiMoves
andloadGame
calls may hit two different servers on the backend. In some instances, the server that answers yourloadGame
may not have fully updated its store and may in fact serve you the old version of your game.
As your GUI matures, you may want to show the expected state of the game before you eventually show its finalized state. Sometimes you may want to show the expected state of the game even before the transaction has completed, and add visual cues hinting at the fact that it is a provisional state.
The same can happen when creating a game, where the second server may returnnull
if it has not been updated yet.
Now you can play test in the GUI. Make sure to put your Keplr address as the black player, so that you start with this one. You will need a second account on Keplr with which to play red, otherwise you must play red from the command line. Alternatively, put the same Keplr address for both black and red.
Either way, it is now possible to play the game from the GUI. Congratulations!
If you started the chain in Docker, when you are done you can stop the containers with:
# Further exercise ideas
- Add pagination to the game list.
- Implement typical GUI features, like disabling buttons when their action should be unavailable, or adding a countdown to the forfeit deadline.
- Implement a Web socket to listen to changes. That would be useful when there are two players who cannot communicate otherwise (instead of polling).
To summarize, this section has explored:
- How to prepare for and then integrate CosmJS and Keplr into the GUI of your checkers blockchain, including how to adjust the React app to be able to package CosmJS.
- How to integrate CosmJS, including working with the GUI's data structures, fetching games from the blockchain and displaying them, integrating with Keplr for browser-based players, creating a new game, and fetching a single game to be played.