Chapter 2: Writing the core app
In this chapter, you’ll write the core logic to interact with the Walnut contract by creating an App class. This class will initialize player-specific wallet clients and contracts, and provide easy-to-use functions like hit, shake, reset, and look. Estimated time: ~20 minutes
Now, navigate to packages/cli/src/
and create a file called app.ts
which will contain the core logic for the CLI:
# Assuming you are in packages/cli/lib
cd ../src
touch app.ts
Import required dependencies
Start by importing all the necessary modules and functions at the top of app.ts
:
import {
type ShieldedContract,
type ShieldedWalletClient,
createShieldedWalletClient,
getShieldedContract,
} from 'seismic-viem'
import { Abi, Address, Chain, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { getShieldedContractWithCheck } from '../lib/utils'
Define the app configuration
The AppConfig
interface organizes all settings for the Walnut App, including player info, wallet setup, and contract details. It supports a multiplayer environment, with multiple players having distinct private keys and contract interactions.
interface AppConfig {
players: Array<{
name: string // Name of the player
privateKey: string // Private key for the player’s wallet
}>
wallet: {
chain: Chain // Blockchain network (e.g., Seismic Devnet or Anvil)
rpcUrl: string // RPC URL for blockchain communication
}
contract: {
abi: Abi // The contract's ABI for interaction
address: Address // The contract's deployed address
}
}
Create the App class
The App
class manages player-specific wallet clients and contract instances, providing an easy-to-use interface for multiplayer gameplay.
export class App {
private config: AppConfig // Holds all app configuration
private playerClients: Map<string, ShieldedWalletClient> = new Map() // Maps player names to their wallet clients
private playerContracts: Map<string, ShieldedContract> = new Map() // Maps player names to their contract instances
constructor(config: AppConfig) {
this.config = config
}
}
Add initialization logic to App
The init()
method sets up individual wallet clients and contract instances for each player, enabling multiplayer interactions. Each player gets their own wallet client and a direct connection to the contract.
async init() {
for (const player of this.config.players) {
// Create a wallet client for the player
const walletClient = await createShieldedWalletClient({
chain: this.config.wallet.chain,
transport: http(this.config.wallet.rpcUrl),
account: privateKeyToAccount(player.privateKey as `0x${string}`),
})
this.playerClients.set(player.name, walletClient) // Map the client to the player
// Initialize the player's contract instance and ensure the contract is deployed
const contract = await getShieldedContractWithCheck(
walletClient,
this.config.contract.abi,
this.config.contract.address
)
this.playerContracts.set(player.name, contract) // Map the contract to the player
}
}
Add helper methods to App
These helper methods ensure that the app fetches the correct wallet client or contract instance for a specific player, supporting multiplayer scenarios.
getWalletClient
:
private getWalletClient(playerName: string): ShieldedWalletClient {
const client = this.playerClients.get(playerName)
if (!client) {
throw new Error(`Wallet client for player ${playerName} not found`)
}
return client
}
getPlayerContract
:
private getPlayerContract(playerName: string): ShieldedContract {
const contract = this.playerContracts.get(playerName)
if (!contract) {
throw new Error(`Shielded contract for player ${playerName} not found`)
}
return contract
}
Implement Contract Interaction Methods
reset
Resets the Walnut for the next round. The reset is player-specific and resets the shell and kernel values.
async reset(playerName: string) {
console.log(`- Player ${playerName} writing reset()`)
const contract = this.getPlayerContract(playerName)
const walletClient = this.getWalletClient(playerName)
await walletClient.waitForTransactionReceipt({
hash: await contract.write.reset([], { gas: 100000n })
})
}
shake
Allows a player to shake the Walnut, incrementing the kernel. This supports multiplayer scenarios where each player’s shakes impact the Walnut. Uses signed writes.
async shake(playerName: string, numShakes: number) {
console.log(`- Player ${playerName} writing shake()`)
const contract = this.getPlayerContract(playerName)
const walletClient = this.getWalletClient(playerName)
await contract.write.shake([numShakes], { gas: 50000n }) // signed write
})
}
hit
:
A player can hit the Walnut to reduce the shell’s strength. Each hit is logged for the respective player.
async hit(playerName: string) {
console.log(`- Player ${playerName} writing hit()`)
const contract = this.getPlayerContract(playerName)
const walletClient = this.getWalletClient(playerName)
await contract.write.hit([], { gas: 100000n })
}
look
:
Reveals the kernel for a specific player if they contributed to cracking the shell. This ensures fairness in multiplayer gameplay. Uses signed reads.
async look(playerName: string) {
console.log(`- Player ${playerName} reading look()`)
const contract = this.getPlayerContract(playerName)
const result = await contract.read.look() // signed read
console.log(`- Player ${playerName} sees number:`, result)
}
Last updated