From atum-stack-backend
Web3 frontend and off-chain integration pattern library — wallet connection (RainbowKit, Web3Modal v3, ConnectKit, Privy for embedded wallets, Dynamic, Coinbase Smart Wallet for Account Abstraction), wagmi v2 + viem (the modern TypeScript stack replacing ethers v6 in 2026), reading contracts (useReadContract, watch, multicall batching), writing transactions (useWriteContract, useWaitForTransactionReceipt, simulateContract for pre-flight checks), event listening (useWatchContractEvent, watchContractEvent), wallet state management (account, chain, balance hooks), chain switching, ENS resolution, IPFS metadata uploads (Pinata, web3.storage, Lighthouse), The Graph subgraph queries with @graphprotocol/graph-cli, Ethers.js v6 patterns for Node.js backends, JSON-RPC providers (Alchemy, Infura, QuickNode, public RPCs), private key management (NEVER in frontend, KMS / Vault / hardware wallet for signing), signed message verification (EIP-712 typed data, EIP-191 personal sign, SIWE Sign-In with Ethereum), and Account Abstraction ERC-4337 with Pimlico / Stackup bundlers + paymasters for gasless UX. Use when integrating wallet connect into a Next.js / Remix / Vue app, building a backend that interacts with smart contracts, indexing on-chain events, or implementing wallet-based authentication. Differentiates from generic frontend-patterns by Web3-specific UX (wallet rejection, gas estimation, chain switching, transaction pending states).
npx claudepluginhub arnwaldn/atum-plugins-collection --plugin atum-stack-backendThis skill uses the workspace's default tool permissions.
Ce skill couvre l'intégration **off-chain** : frontend wallet connect, backend Node.js/Python qui parle aux contracts, indexing, et auth Web3.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides implementation of event-driven hooks in Claude Code plugins using prompt-based validation and bash commands for PreToolUse, Stop, and session events.
Ce skill couvre l'intégration off-chain : frontend wallet connect, backend Node.js/Python qui parle aux contracts, indexing, et auth Web3.
Stack canonique 2026 : wagmi v2 + viem côté frontend React, viem ou ethers v6 côté backend, RainbowKit pour la modale de connexion.
npm install wagmi viem @rainbow-me/rainbowkit @tanstack/react-query
// app/providers.tsx
'use client'
import { WagmiProvider, createConfig, http } from 'wagmi'
import { mainnet, base, arbitrum, polygon } from 'wagmi/chains'
import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import '@rainbow-me/rainbowkit/styles.css'
const config = getDefaultConfig({
appName: 'My App',
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
chains: [mainnet, base, arbitrum, polygon],
ssr: true,
})
const queryClient = new QueryClient()
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>{children}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
)
}
// app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body><Providers>{children}</Providers></body>
</html>
)
}
// app/page.tsx
'use client'
import { ConnectButton } from '@rainbow-me/rainbowkit'
import { useAccount, useBalance } from 'wagmi'
export default function Home() {
const { address, isConnected } = useAccount()
const { data: balance } = useBalance({ address })
return (
<main>
<ConnectButton />
{isConnected && <p>Balance: {balance?.formatted} {balance?.symbol}</p>}
</main>
)
}
import { useReadContract } from 'wagmi'
import { erc20Abi } from 'viem'
function TokenBalance({ tokenAddress, userAddress }: { tokenAddress: `0x${string}`, userAddress: `0x${string}` }) {
const { data, isLoading, error } = useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
})
if (isLoading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
return <p>Balance: {data?.toString()}</p>
}
import { useReadContracts } from 'wagmi'
const { data } = useReadContracts({
contracts: [
{ address: token1, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: token2, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: token3, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
],
})
3 reads en 1 RPC call grâce à Multicall3 (deployed sur toutes les chaines majeures).
import { useWriteContract, useSimulateContract, useWaitForTransactionReceipt } from 'wagmi'
function MintButton() {
// 1. Pre-flight check (simule sans envoyer)
const { data: simulation } = useSimulateContract({
address: contractAddress,
abi: myContractAbi,
functionName: 'mint',
args: [],
value: parseEther('0.01'),
})
// 2. Hook d'écriture
const { writeContract, data: hash, isPending, error } = useWriteContract()
// 3. Wait for receipt
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
return (
<button
disabled={!simulation?.request || isPending || isConfirming}
onClick={() => writeContract(simulation!.request)}
>
{isPending ? 'Confirm in wallet...' : isConfirming ? 'Confirming...' : isSuccess ? 'Minted!' : 'Mint'}
</button>
)
}
Important : useSimulateContract détecte les reverts AVANT d'envoyer la tx → meilleure UX.
import { useWatchContractEvent } from 'wagmi'
useWatchContractEvent({
address: contractAddress,
abi: myContractAbi,
eventName: 'Transfer',
onLogs: (logs) => {
console.log('New transfer:', logs)
},
})
// Backend (Next.js API route)
import { SiweMessage, generateNonce } from 'siwe'
export async function POST(req: Request) {
const { message, signature } = await req.json()
const siwe = new SiweMessage(message)
try {
await siwe.verify({ signature, nonce: req.cookies.get('siwe-nonce')?.value })
} catch {
return new Response('Invalid', { status: 401 })
}
// Crée une session JWT pour l'address
const token = jwt.sign({ address: siwe.address }, process.env.JWT_SECRET!)
return Response.json({ token })
}
// Frontend
import { useAccount, useSignMessage } from 'wagmi'
import { SiweMessage } from 'siwe'
const { address, chainId } = useAccount()
const { signMessageAsync } = useSignMessage()
async function signIn() {
const nonce = await fetch('/api/auth/nonce').then(r => r.text())
const message = new SiweMessage({
domain: window.location.host,
address,
statement: 'Sign in to My App',
uri: window.location.origin,
version: '1',
chainId,
nonce,
}).prepareMessage()
const signature = await signMessageAsync({ message })
await fetch('/api/auth/verify', {
method: 'POST',
body: JSON.stringify({ message, signature }),
})
}
Avantage SIWE : auth sans password, sans email, sans server-side state cryptographique. Standard EIP-4361.
import { createPublicClient, createWalletClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
// Read-only client
const publicClient = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
})
const blockNumber = await publicClient.getBlockNumber()
// Write client (server-side, NEVER in frontend)
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
})
const hash = await walletClient.writeContract({
address: contractAddress,
abi: myContractAbi,
functionName: 'mint',
args: [recipient, amount],
})
const receipt = await publicClient.waitForTransactionReceipt({ hash })
Sécurité critique : la PRIVATE_KEY ne doit JAMAIS être exposée. Utiliser :
npm install -g @graphprotocol/graph-cli
graph init --product hosted-service my-org/my-subgraph
# subgraph.yaml
specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: MyContract
network: mainnet
source:
address: "0x..."
abi: MyContract
startBlock: 18000000
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
file: ./src/mapping.ts
entities:
- Transfer
abis:
- name: MyContract
file: ./abis/MyContract.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
// src/mapping.ts
import { Transfer as TransferEvent } from '../generated/MyContract/MyContract'
import { Transfer } from '../generated/schema'
export function handleTransfer(event: TransferEvent): void {
const id = event.transaction.hash.toHex() + '-' + event.logIndex.toString()
const transfer = new Transfer(id)
transfer.from = event.params.from
transfer.to = event.params.to
transfer.amount = event.params.value
transfer.timestamp = event.block.timestamp
transfer.save()
}
graph build
graph deploy --product hosted-service my-org/my-subgraph
Query le subgraph depuis le frontend :
const query = `
query GetTransfers($user: String!) {
transfers(where: { from: $user }, orderBy: timestamp, orderDirection: desc, first: 20) {
id
to
amount
timestamp
}
}
`
const res = await fetch(SUBGRAPH_URL, {
method: 'POST',
body: JSON.stringify({ query, variables: { user: address.toLowerCase() } }),
})
import { PinataSDK } from 'pinata'
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
})
// Upload image
const file = new File([imageBlob], 'image.png', { type: 'image/png' })
const imageUpload = await pinata.upload.file(file)
const imageCID = imageUpload.IpfsHash
// Upload metadata JSON
const metadata = {
name: 'My NFT #1',
description: 'Cool NFT',
image: `ipfs://${imageCID}`,
attributes: [
{ trait_type: 'Color', value: 'Blue' },
],
}
const metaUpload = await pinata.upload.json(metadata)
console.log(`tokenURI: ipfs://${metaUpload.IpfsHash}`)
ERC-4337 permet des wallets smart contract avec :
import { createSmartAccountClient } from 'permissionless'
import { signerToSafeSmartAccount } from 'permissionless/accounts'
import { createPimlicoBundlerClient } from 'permissionless/clients/pimlico'
// Embedded wallet via Privy ou Coinbase Smart Wallet
const account = await signerToSafeSmartAccount(publicClient, {
signer,
safeVersion: '1.4.1',
})
const bundlerClient = createPimlicoBundlerClient({
transport: http(`https://api.pimlico.io/v2/${chain.id}/rpc?apikey=${process.env.PIMLICO_KEY}`),
})
const smartAccountClient = createSmartAccountClient({
account,
chain,
bundlerTransport: http(`https://api.pimlico.io/v2/${chain.id}/rpc?apikey=${process.env.PIMLICO_KEY}`),
middleware: { sponsorUserOperation: paymaster.sponsorUserOperation },
})
const txHash = await smartAccountClient.sendTransaction({
to: contractAddress,
data: encodeFunctionData({ abi, functionName: 'mint', args: [] }),
})
window.ethereum direct au lieu de wagmi → casse avec WalletConnect, Coinbase Wallet, embedded walletswriteContract → casse aux EIP-1559 changesuseSimulateContract avant chaque writesolidity-patterns (ce plugin)smart-contract-security (ce plugin)frontend-design (atum-stack-web)