Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: transactionButton component to handle wallet connection and transaction sending #161

Merged
merged 2 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 17 additions & 22 deletions src/hooks/useWeb3Status.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useChainIsSupported } from 'connectkit'
import { Address, Chain } from 'viem'
import {
UseBalanceReturnType,
Expand All @@ -7,20 +6,18 @@ import {
useAccount,
useBalance,
useChainId,
useConnect,
useDisconnect,
usePublicClient,
useSwitchChain,
useWalletClient,
} from 'wagmi'
import { injected } from 'wagmi/connectors'

import { chains, type ChainsIds } from '@/src/lib/networks.config'
import { RequiredNonNull } from '@/src/types/utils'

export type AppWeb3Status = {
appChainId: Chain['id']
readOnlyClient: UsePublicClientReturnType
supportedChains: readonly [Chain, ...Chain[]]
appChainId: ChainsIds
}

export type WalletWeb3Status = {
Expand All @@ -29,13 +26,13 @@ export type WalletWeb3Status = {
connectingWallet: boolean
switchingChain: boolean
isWalletConnected: boolean
isWalletNetworkSupported: boolean | null
walletChainId: number | undefined
walletClient: UseWalletClientReturnType['data']
isWalletSynced: boolean
walletChainId: Chain['id'] | undefined
}

export type Web3Actions = {
switchChain: (chainId: Chain['id']) => void
switchChain: (chainId?: ChainsIds) => void
disconnect: () => void
}

Expand All @@ -53,35 +50,33 @@ export const useWeb3Status = () => {
isConnected: isWalletConnected,
isConnecting: connectingWallet,
} = useAccount()
const appChainId = useChainId()
const isWalletNetworkSupported = useChainIsSupported(walletChainId)
const { chains: supportedChains, isPending: switchingChain, switchChain } = useSwitchChain()
const appChainId = useChainId() as ChainsIds
const { isPending: switchingChain, switchChain } = useSwitchChain()
const readOnlyClient = usePublicClient()
const { data: walletClient } = useWalletClient()
const { data: balance } = useBalance()
const { connect } = useConnect()
const { disconnect } = useDisconnect()

const appWeb3Status = {
supportedChains,
appChainId,
const isWalletSynced = isWalletConnected && walletChainId === appChainId

const appWeb3Status: AppWeb3Status = {
readOnlyClient,
appChainId,
}

const walletWeb3Status = {
const walletWeb3Status: WalletWeb3Status = {
address,
balance,
walletChainId,
isWalletConnected,
isWalletNetworkSupported,
connectingWallet,
switchingChain,
walletClient,
isWalletSynced,
walletChainId,
}

const web3Actions = {
switchChain: (chainId: number) => switchChain({ chainId }),
connect: () => connect({ connector: injected() }),
const web3Actions: Web3Actions = {
switchChain: (chainId: number = chains[0].id) => switchChain({ chainId }), // default to the first chain in the config
disconnect: disconnect,
}

Expand All @@ -96,7 +91,7 @@ export const useWeb3Status = () => {

export const useWeb3StatusConnected = () => {
const context = useWeb3Status()
if (!context.address) {
if (!context.isWalletConnected) {
throw new Error('Use useWeb3StatusConnected only when a wallet is connected')
}
return useWeb3Status() as RequiredNonNull<Web3Status>
Expand Down
98 changes: 53 additions & 45 deletions src/pageComponents/home/Examples/demos/TransactionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,61 @@ import { useSendTransaction, useWriteContract } from 'wagmi'

import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status'
import { TransactionButton } from '@/src/sharedComponents/TransactionButton'
import { withWalletStatusVerifier } from '@/src/sharedComponents/WalletStatusVerifier'

export const TransactionButtonDemo = () => {
const { address } = useWeb3StatusConnected()
const { sendTransactionAsync } = useSendTransaction()
const { writeContractAsync } = useWriteContract()
const TransactionButtonDemo = withWalletStatusVerifier(
() => {
const { address } = useWeb3StatusConnected()
const { sendTransactionAsync } = useSendTransaction()
const { writeContractAsync } = useWriteContract()

const handleOnMined = (receipt: TransactionReceipt) => {
alert(`Transaction completed! 🎉 \n hash: ${receipt.transactionHash}`)
}
const handleOnMined = (receipt: TransactionReceipt) => {
alert(`Transaction completed! 🎉 \n hash: ${receipt.transactionHash}`)
}

const handleSendTransaction = (): Promise<Hash> => {
// Send native token
return sendTransactionAsync({
chainId: sepolia.id,
to: address,
value: parseEther('0.1'),
})
}
const handleSendTransaction = (): Promise<Hash> => {
// Send native token
return sendTransactionAsync({
chainId: sepolia.id,
to: address,
value: parseEther('0.1'),
})
}

const handleWriteContract = (): Promise<Hash> => {
// Send ERC20 token [USDC]
return writeContractAsync({
chainId: sepolia.id,
abi: erc20Abi,
address: '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8', // USDC
functionName: 'transfer',
args: [address, 100000000n], // 100 USDC
})
}
const handleWriteContract = (): Promise<Hash> => {
// Send ERC20 token [USDC]
return writeContractAsync({
chainId: sepolia.id,
abi: erc20Abi,
address: '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8', // USDC
functionName: 'transfer',
args: [address, 100000000n], // 100 USDC
})
}

return (
<>
<TransactionButton
chain={sepolia}
label="Send 100 USDC"
labelSending="Sending 100 USDC..."
onMined={handleOnMined}
transaction={handleWriteContract}
/>
<br />
<TransactionButton
chain={sepolia}
label="Send 0.1 ETH"
labelSending="Sending 0.1 ETH..."
onMined={handleOnMined}
transaction={handleSendTransaction}
/>
</>
)
}
return (
<>
<TransactionButton
chain={sepolia}
label="Send 100 USDC"
labelSending="Sending 100 USDC..."
onMined={handleOnMined}
transaction={handleWriteContract}
/>
<br />
<TransactionButton
chain={sepolia}
label="Send 0.1 ETH"
labelSending="Sending 0.1 ETH..."
onMined={handleOnMined}
transaction={handleSendTransaction}
/>
</>
)
},
{
chainId: sepolia.id, // this DEMO component is for sepolia chain
},
)

export default TransactionButtonDemo
2 changes: 1 addition & 1 deletion src/pageComponents/home/Examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Hash from '@/src/pageComponents/home/Examples/demos/Hash'
import HashInput from '@/src/pageComponents/home/Examples/demos/HashInput'
import TokenDropdownDemo from '@/src/pageComponents/home/Examples/demos/TokenDropdown'
import TokenInput from '@/src/pageComponents/home/Examples/demos/TokenInput'
import { TransactionButtonDemo } from '@/src/pageComponents/home/Examples/demos/TransactionButton'
import TransactionButtonDemo from '@/src/pageComponents/home/Examples/demos/TransactionButton'
import { ConnectWalletButton } from '@/src/providers/Web3Provider'

const Wrapper = styled.section`
Expand Down
47 changes: 14 additions & 33 deletions src/sharedComponents/TransactionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { ReactElement, useEffect, useState } from 'react'
import { useEffect, useState } from 'react'

import { Button } from 'db-ui-toolkit'
import { Chain, Hash, TransactionReceipt } from 'viem'
import { useWaitForTransactionReceipt } from 'wagmi'

import { useWeb3Status } from '@/src/hooks/useWeb3Status'
import { ConnectWalletButton } from '@/src/providers/Web3Provider'
import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status'

interface TransactionButtonProps {
transaction: () => Promise<Hash>
chain?: Chain
onMined?: (receipt: TransactionReceipt) => void
fallback?: ReactElement
disabled?: boolean
label?: string
labelSending?: string
Expand All @@ -24,9 +22,8 @@ interface TransactionButtonProps {
*
* @component
* @param {Function} props.transaction - The function that initiates the transaction.
* @param {Function} props.fallback - The fallback component to be rendered when the wallet is not connected. (default: ConnectButton)
* @param {Function} props.onMined - The callback function to be called when the transaction is mined.
* @param {Chain} props.chain - The chain where the transaction will be sent.
* @param {Function} props.onMined - The callback function to be called when the transaction is mined.
* @param {boolean} props.disabled - The flag to disable the button.
* @param {string} props.label - The label for the button.
* @param {string} props.labelSending - The label for the button when the transaction is pending.
Expand All @@ -35,27 +32,23 @@ interface TransactionButtonProps {
*/

export const TransactionButton = ({
chain,
disabled,
fallback = <ConnectWalletButton />,
label = 'Send Transaction',
labelSending = 'Sending...',
onMined,
transaction,
}: TransactionButtonProps) => {
const {
isWalletConnected,
isWalletNetworkSupported,
supportedChains,
switchChain,
switchingChain,
walletChainId,
} = useWeb3Status()
const { isWalletSynced } = useWeb3StatusConnected()

if (!isWalletSynced) {
throw new Error(
'TransactionButton component must be used inside a WalletStatusVerifier component or withWalletStatusVerifier HoC',
)
}

const [hash, setHash] = useState<Hash>()
const [isPending, setIsPending] = useState<boolean>(false)

const isCorrectChain = chain ? walletChainId === chain.id : isWalletNetworkSupported

const { data: receipt } = useWaitForTransactionReceipt({
hash: hash,
})
Expand All @@ -79,22 +72,10 @@ export const TransactionButton = ({
}
}

if (!isWalletConnected) {
return fallback
}

const inputProps = {
disabled: isPending || switchingChain || disabled,
onClick: isCorrectChain
? handleSendTransaction
: () => switchChain((chain || supportedChains[0]).id),
disabled: isPending || disabled,
onClick: handleSendTransaction,
}

const buttonLabel = isPending
? labelSending
: !isCorrectChain
? `Switch to ${(chain || supportedChains[0]).name}`
: label

return <Button {...inputProps}>{buttonLabel}</Button>
return <Button {...inputProps}>{isPending ? labelSending : label}</Button>
}
Loading
Loading