13 min read
Overview
If you have made it this far, congratulations! Take a moment to pat yourself on the back and reflect on how far you have come. Fortunately, some of our earlier design decisions should make today's lesson feel a little too easy. We will use the SendTransactionTemplate that we created in Lesson 2 and the Create Instruction methods we created in our client library to create a button component for each program instruction. All of our components today will more or less follow this model:
import { TransactionInstruction } from "@solana/web3.js";
import { SendTransactionTemplate } from "./SendTransactionTemplate";
import { useGame } from "@/utils/game/useGame";
import { useState, useEffect } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
interface Props {
// Some instruction-specific props
onSuccess?: () => void;
}
const ExampleButton = ({ onSuccess }: Props) => {
const [instructions, setInstructions] = useState<TransactionInstruction[]>([]);
const { gameClient } = useGame();
const { publicKey: player } = useWallet();
const { connection } = useConnection();
useEffect(() => {
if (!player) return;
gameClient.getInstructionMethod(player)
.then((instruction) => {
setInstructions([instruction]);
})
.catch((err) => {
console.log(err);
});
}, [player, connection, gameClient]);
return (
<SendTransactionTemplate
transactionInstructions={instructions}
buttonLabel="Example Button Text"
onSuccess={onSuccess}
/>
);
}
export default ExampleButton;
We will generate some instruction-specific props. Then, we will use the useGame, useWallet, and useConnection hooks to get relevant information for our instruction. We will use our client to run one of our instruction methods and set the output to an instructions state variable. Finally, we will pass our instructions state variable to the SendTransactionTemplate component along with a button label and an optional onSuccess callback.
Let's jump right in!
Create New Player Button
The first button we will create is the "Create New Player" button. This button will utilize the output of the createNewPlayerInstruction method that we made in our client library.
Inside your ./components/SendTransactionButtons/ directory, create a new file, CreatePlayer.tsx. Add the following code:
import { TransactionInstruction } from "@solana/web3.js";
import { SendTransactionTemplate } from "./SendTransactionTemplate";
import { useGame } from "@/utils/game/useGame";
import { useState, useEffect } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
interface Props {
onSuccess?: () => void;
}
const CreatePlayerButton = ({ onSuccess }: Props) => {
const [instructions, setInstructions] = useState<TransactionInstruction[]>([]);
const { gameClient } = useGame();
const { publicKey: player } = useWallet();
const { connection } = useConnection();
useEffect(() => {
if (!player) return;
gameClient.createNewPlayerInstruction(player)
.then((instruction) => {
setInstructions([instruction]);
})
.catch((err) => {
console.log(err);
});
}, [player, connection, gameClient]);
return (
<SendTransactionTemplate
transactionInstructions={instructions}
buttonLabel="Create New Player"
onSuccess={onSuccess}
/>
);
}
export default CreatePlayerButton;
This component is almost identical to the example we provided, except we use the createNewPlayerInstruction method, which returns a TransactionInstruction that we can pass to the SendTransactionTemplate component.
We will be able to import this in our Game.tsx component and render it in our Game page (we will do this tomorrow).
Create New Game Button
The next button we are going to create is the "Create New Game" button. This button will utilize the output of the createNewGameInstruction method that we created in our client library. Create a new file in your ./components/SendTransactionButtons/ directory called CreateGame.tsx. Add the following code:
import { TransactionInstruction } from "@solana/web3.js";
import { SendTransactionTemplate } from "./SendTransactionTemplate";
import { useGame } from "@/utils/game/useGame";
import { useState, useEffect } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
const CreateGameButton = () => {
const [instructions, setInstructions] = useState<TransactionInstruction[]>([]);
const [gamePda, setGamePda] = useState<string>();
const { gameClient, refreshGames, refreshActivePlayer } = useGame();
const { publicKey: player } = useWallet();
const { connection } = useConnection();
useEffect(() => {
if (!player) return;
gameClient.createNewGameInstruction(player)
.then(({ instruction, game }) => {
setGamePda(game);
setInstructions([instruction]);
})
.catch((err) => {
console.log(err);
});
}, [player, connection, gameClient]);
return (
<SendTransactionTemplate
transactionInstructions={instructions}
buttonLabel="Create New Game"
onSuccess={() => {
refreshGames();
refreshActivePlayer();
window.location.replace(`/games/${gamePda}`);
}}
/>
);
}
export default CreateGameButton;
This component uses the createNewGameInstruction method, which returns an object containing both the TransactionInstruction and the publicKey of the newly created game. On success, we will refresh the list of games and the active player and use the game variable to redirect the user to the newly created game page (we will make this page next lesson).
Now that we have done a couple of these, feel free to try and create the JoinGameButton, PlayButton, and ClaimRewardButton on your own. Otherwise, feel free to follow along below.
Join Game Button
The next button we will create is the "Join Game" button. This button will utilize the output of the joinGameInstruction method that we created in our client library. Create a new file in your ./components/SendTransactionButtons/ directory called JoinGame.tsx. Add the following code:
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import { SendTransactionTemplate } from "./SendTransactionTemplate";
import { useGame } from "@/utils/game/useGame";
import { useState, useEffect } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { Game } from "@/utils/game/types";
interface Props {
gameInfo: Game;
gamePda: PublicKey;
}
const JoinGameButton = ({gameInfo, gamePda}:Props) => {
const [instructions, setInstructions] = useState<TransactionInstruction[]>([]);
const { gameClient, refreshGames, refreshActivePlayer } = useGame();
const { publicKey: player } = useWallet();
const { connection } = useConnection();
useEffect(() => {
if (!player) return;
gameClient.joinGameInstruction(gameInfo, gamePda, player)
.then((instruction) => {
setInstructions([instruction]);
})
.catch((err) => {
console.log(err);
});
}, [player, connection, gameClient, gameInfo, gamePda]);
return (
<SendTransactionTemplate
transactionInstructions={instructions}
buttonLabel="Join"
width={24}
onSuccess={()=>{
refreshGames();
refreshActivePlayer();
window.location.replace(`/games/${gamePda.toBase58()}`);
}}
/>
);
}
export default JoinGameButton;
The only notable difference for this component is that we pass some information about the Game the player is joining as props. This will allow us to create a button for each game in our GameList component that the player can join. Similarly, on success, we update the game and player states and redirect the user to the game page.
Play Button
The next button we will create is the "Play" button. The way we will implement this button is that we will have a play button on an open square on the tic-tac-toe board. When the player clicks on the button, we will pass the square's index to the PlayButton component, which will use the playInstruction method to create a transaction instruction to play the square. Create a new file in your ./components/SendTransactionButtons/ directory called Play.tsx. Add the following code:
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import { SendTransactionTemplate } from "./SendTransactionTemplate";
import { useGame } from "@/utils/game/useGame";
import { useState, useEffect } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { Game } from "@/utils/game/types";
interface Props {
gameInfo: Game;
currentPlayer: PublicKey;
gamePda: PublicKey;
row: number;
column: number;
currentMove: 'X' | 'O';
onSuccess?: () => void;
}
const PlayButton = ({ gameInfo, currentPlayer, gamePda, row, column, onSuccess, currentMove }: Props) => {
const [instructions, setInstructions] = useState<TransactionInstruction[]>([]);
const { gameClient } = useGame();
const { publicKey: player } = useWallet();
const { connection } = useConnection();
useEffect(() => {
if (!player) return;
if (!gameClient) return;
gameClient.createPlayInstruction({gameInfo, currentPlayer, gamePda, row, column})
.then((instruction) => {
setInstructions([instruction]);
})
.catch((err) => {
console.log(err);
});
}, [player, connection, gameClient, gameInfo, currentPlayer, gamePda, row, column]);
return (
<SendTransactionTemplate
transactionInstructions={instructions}
buttonLabel={currentMove}
width={5}
invisible={true}
onSuccess={onSuccess}
/>
);
}
export default PlayButton;
This component functions exactly like the others, but we have a few more props that we need to pass to it. We need to pass the gameInfo, currentPlayer, gamePda, row, and column props to satisfy all of the arguments in our createPlayInstruction method. We are also going to do a few styling things that will help our UX:
- We will use the
invisibleprop to hide the button. This will effectively let us use the underlying square's div as the button without adding any new styling. - We need to pass the
currentMoveprop (an 'X' or 'O') so that we can display to the user what they are about to play on the board (this will require a little conditional rendering in ourGameBoardcomponent that we will build tomorrow).
Claim Reward Button
The last button we will create is the "Claim Reward" button. This button will utilize the output of the claimRewardInstruction method that we created in our client library. Create a new file in your ./components/SendTransactionButtons/ directory called ClaimReward.tsx. Add the following code:
import { Keypair, TransactionInstruction } from "@solana/web3.js";
import { SendTransactionTemplate } from "./SendTransactionTemplate";
import { useGame } from "@/utils/game/useGame";
import { useState, useEffect } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
const ClaimRewardButton = () => {
const [instructions, setInstructions] = useState<TransactionInstruction[]>([]);
const [extraSigners, setExtraSigners] = useState<Keypair[]>([]);
const { gameClient, refreshActivePlayer } = useGame();
const { publicKey: player } = useWallet();
const { connection } = useConnection();
useEffect(() => {
if (!player) return;
gameClient.claimRewardInstruction(player)
.then(({instruction, extraSigner}) => {
setInstructions([instruction]);
setExtraSigners([extraSigner]);
})
.catch((err) => {
console.log(err);
});
}, [player, connection, gameClient]);
return (
<SendTransactionTemplate
transactionInstructions={instructions}
buttonLabel="Claim Reward 🎁"
onSuccess={() => {
refreshActivePlayer();
}}
extraSigners={extraSigners}
/>
);
}
export default ClaimRewardButton;
If you recall, the claimRewardInstruction method returns an object containing both the TransactionInstruction and the Keypair of the newly created mint account. We will use the extraSigners prop to pass this keypair to the SendTransactionTemplate component. This will allow us to sign the transaction with the new mint account's private key (needed to initialize the account). On success, we will refresh the player state to update their reward claimed status.
That's a Wrap!
Nice job today! We have created all the buttons we need to interact with our program. Tomorrow, we will make the Game and Board components that will create the UI/UX for our game. See you then!