Skip to content

Commit

Permalink
Init wagmi and connect wallet functionality (#17)
Browse files Browse the repository at this point in the history
## Why
Wordle3 didn't have the proper wallet connection logic to enable players
to connect and interact with the game.
## How
- By adding Wagmi and TanStack Query to enable wallet interaction
wrapping App.jsx on the respective providers.
- Also creates a custom ConnectWallet component that passes on the
respective address for smart contract interaction. Style to be added
later.

Also adds new publicclients for future usage.
  • Loading branch information
rccsousa authored Nov 7, 2024
1 parent 4225686 commit 098fa60
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 46 deletions.
4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.59.20",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"wagmi": "^2.12.27"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
Expand Down
72 changes: 39 additions & 33 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import {useEffect, useState} from 'react'
import './App.css'

import {local_accounts} from './clients/testClient'
// import wordle hooks
import {
getAlphabet,
getWDTBalance,
getPlayerGuesses,
getHitmap,
canPlay,
useFaucet,
initAttempts,
tryGuess
tryGuess,
} from './hooks/WordleHooks'

// import components
import {Alphabet} from './components/Alphabet/Alphabet'
import { GameSpace } from './components/GameSpace/GameSpace'
import { ApprovalModal } from './components/ApprovalModal/ApprovalModal'
import { LowFunds } from './components/LowFunds/LowFunds'
import { AlreadyPlayed } from './components/AlreadyPlayed/AlreadyPlayed'

// wagmi connection
import { ConnectWallet } from './clients/walletClient/ConnectWallet'
import { wagmiConfig } from '../wagmi.config'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function App() {
// Current account and respetive variables
const [currentAcc, setCurrentAcc] = useState(local_accounts[0])
const [currentAddress, setCurrentAddress] = useState(undefined)
const [alphabet, setAlphabet] = useState(undefined)

// TODO: remove after testing and prototyping
Expand All @@ -39,29 +44,35 @@ function App() {
// Gets the current account's WDT balance, eligibility to play, player attempts, guesses and alphabet.
// this is constantly pinging the network, so i'm unsure if this is the right way to go
useEffect(() => {
if (currentAcc) {
getWDTBalance(currentAcc).then(setWdtBalance)

getAlphabet(currentAcc).then(res => {
if (res.length === 0) setIsPlaying(false)
else setIsPlaying(true)
setAlphabet(res)
})
getPlayerGuesses(currentAcc).then(setPlayerGuesses)

const getParams = async (address) => {
const balance = await getWDTBalance(address)
const alphabet = await getAlphabet(address)
const playerGuesses = await getPlayerGuesses(address)
if (alphabet.length > 0) {setIsPlaying(true)}
if (balance) setWdtBalance(balance)
if (alphabet) setAlphabet(alphabet)
if (playerGuesses) setPlayerGuesses(playerGuesses)
}

if (currentAddress) {
getParams(currentAddress)
}
}, [currentAcc, playerGuesses])

}, [currentAddress])

// handle the guess input
const handleGuessChange = (e) => setGuess(e.target.value)

// handle new guess
const handleNewGuess = (account, guess) => {
tryGuess(account, guess)
setTimeout(() => getPlayerGuesses(account).then(setPlayerGuesses), 500)
}

const handleInitAttempts = async (account) => {
try {
const res = await initAttempts(account)
await initAttempts(account)
} catch (err) {
// placeholder for token faucet user flow
if (err.message.includes("You don't have enough tokens to play")) {
Expand All @@ -81,38 +92,33 @@ function App() {
}

return (
<>
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={new QueryClient()}>
<div>

<header>
<ConnectWallet onAddressChange={setCurrentAddress} />
</header>
{/* Modals for approving transactions and messages */}
{approvalModal && <ApprovalModal account={currentAcc} close={() => setApprovalModal(false)}/>}
{lowFundsWarn && <LowFunds account={currentAcc} close={() => setLowFundsWarn(false)}/>}
{approvalModal && <ApprovalModal account={currentAddress} close={() => setApprovalModal(false)}/>}
{lowFundsWarn && <LowFunds account={currentAddress} close={() => setLowFundsWarn(false)}/>}
{playedToday && <AlreadyPlayed close={() => setPlayedToday(false)}/>}


{/* Playground: list accounts to experiment */}
<h4>Here are some addresses:</h4>
<select onChange={(e) => setCurrentAcc(local_accounts.find(a => a.address === e.target.value))}>
<option value="">Select an address</option>
{local_accounts.map((address) => (
<option key={address.address} value={address.address}>{address.address}</option>
))}
</select>
<p>WDT Balance: {wdtBalance ? `${wdtBalance.toString()} WDT` : '???'}</p>

{/* Game space */}
{!isPlaying ?
(<div className={"game-buttons"}>
<button onClick={() => handleInitAttempts(currentAcc)}>Play</button>
<button onClick={() => handleInitAttempts(currentAddress)}>Play</button>
</div>) : (
<div>
<input placeholder='Enter guess' onChange={handleGuessChange}/>
<button onClick={() => handleNewGuess(currentAcc, guess)}>Submit</button>
<button onClick={() => handleNewGuess(currentAddress, guess)}>Submit</button>
<GameSpace playerGuesses={playerGuesses}/>
<Alphabet playerAlphabet={alphabet}/>
</div>)}
</div>
</>
</QueryClientProvider>
</WagmiProvider>
)
}

Expand Down
24 changes: 24 additions & 0 deletions frontend/src/clients/publicClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createPublicClient, http, publicActions, walletActions, getContract } from 'viem';
import { foundry } from 'viem/chains';
import WordleABI from '../abis/WordleABI.json';
import ERC20ABI from '../abis/ERC20ABI.json';

// Public client
export const publicClient = createPublicClient({
chain: foundry,
transport: http(),
});

// Wordle3 Contract
export const publicWordle = getContract({
address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0',
abi: WordleABI,
client: publicClient,
});

// Wordle Token contract
export const publicWdt = getContract({
address: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
abi: ERC20ABI,
client: publicClient,
});
50 changes: 50 additions & 0 deletions frontend/src/clients/walletClient/ConnectWallet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react'
import { useConnect } from 'wagmi'
import { useAccount, useDisconnect, useEnsAvatar, useEnsName } from 'wagmi'


/**
* Account component that displays connected account and passes on
* the address to the parent component for interaction.
*/
function Account({onAddressChange}) {
const { address } = useAccount()
const { disconnect } = useDisconnect()
const { data: ensName } = useEnsName({ address })
const { data: ensAvatar } = useEnsAvatar({ name: ensName! })

if (address) onAddressChange(address);

return (
<div>
{ensAvatar && <img alt="ENS Avatar" src={ensAvatar} />}
{address && <div>{ensName ? `${ensName} (${address})` : address}</div>}
<button onClick={() => disconnect()}>Disconnect</button>
</div>
)
}

/**
* Account component that displays available wallet connectors
* Connectors are also pulled from ../../wagmi.config.ts
*/
function WalletOptions() {
const { connectors, connect } = useConnect()

return connectors.map((connector) => (
<button key={connector.uid} onClick={() => connect({ connector })}>
{connector.name}
</button>
))
}

/**
* Wrapper for the login user flow.
* Receives onAddressChange from parent component to lift the address
* for smart contract operations
*/
export function ConnectWallet({onAddressChange}) {
const { isConnected } = useAccount()
if (isConnected) return <Account onAddressChange={onAddressChange} />
return <WalletOptions />
}
24 changes: 12 additions & 12 deletions frontend/src/hooks/WordleHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,36 @@ import ERC20ABI from '../abis/ERC20ABI.json'

// Get number of remaining attempts a player still has
export async function getAttempts(account) {
const attempts = await wordle.read.getPlayerAttempts([account.address])
return attempts.toString()
const attempts = await wordle.read.getPlayerAttempts([account])
return formatEther(attempts)
}

// Get the current state of the player's alphabet hitmap
export async function getAlphabet(account) {
return await wordle.read.getAlphabet([account.address])
return await wordle.read.getAlphabet([account])
}

// Get the current player hitmap state
export async function getHitmap(account) {
return await wordle.read.getHiddenWord([account.address])
return await wordle.read.getHiddenWord([account])
}

// Gets the WDT balance of a player
export async function getWDTBalance(account) {
const res = await wdt.read.balanceOf([account.address])
const res = await wdt.read.balanceOf([account])
return formatEther(res)
}

// Gets player used guesses array
export async function getPlayerGuesses(account) {
const res = await wordle.read.getPlayerGuesses([account.address])
const res = await wordle.read.getPlayerGuesses([account])
return res
}


// Checks if a player is eligible to play
export async function canPlay(account) {
return await wordle.read.canPlay([account.address])
return await wordle.read.canPlay([account])
}

// Give a player some WDT
Expand All @@ -44,7 +44,7 @@ export async function useFaucet(account) {
address: wordle.address,
abi: WordleABI,
functionName: 'tokenFaucet',
args: [account.address],
args: [account],
})
await client.writeContract(request)
}
Expand All @@ -60,12 +60,12 @@ export async function approveGame(account) {
args: [wordle.address, 200 * decimals],
})
await client.writeContract(request)
const allowance = await wdt.read.allowance([account.address, wordle.address])
const allowance = await wdt.read.allowance([account, wordle.address])
}

// Initialize the player's attempts
export async function initAttempts(account) {
const allowance = await wdt.read.allowance([account.address, wordle.address])
const allowance = await wdt.read.allowance([account, wordle.address])

// simulate/validate the contract to ensure contract exists
// and is valid (+info: https://viem.sh/docs/contract/writeContract)
Expand All @@ -74,7 +74,7 @@ export async function initAttempts(account) {
address: wordle.address,
abi: WordleABI,
functionName: 'initAttempts',
args: [account.address],
args: [account],
})

if (request) await client.writeContract(request)
Expand All @@ -92,7 +92,7 @@ export async function tryGuess(account, guess) {
address: wordle.address,
abi: WordleABI,
functionName: 'tryGuess',
args: [guess, account.address],
args: [guess, account],
})

if (request) await client.writeContract(request)
Expand Down
15 changes: 15 additions & 0 deletions frontend/wagmi.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { http, createConfig } from 'wagmi'
import { anvil } from 'wagmi/chains'
import { injected, metaMask, safe } from 'wagmi/connectors'

export const wagmiConfig = createConfig({
chains: [anvil],
transports: {
[anvil.id]: http(),
},
connectors: [
injected(),
metaMask(),
safe(),
]
})

0 comments on commit 098fa60

Please sign in to comment.