返回 Skill 列表
extension
分类: 开发与工程无需 API Key

Frontend Web3 for BAP-578

构建一个Next.js前端,以与BNB Chain上的BAP-578非同质化代理合约进行交互。涵盖钱包连接、链设置、ABI集成、用于铸造和管理的React钩子以及代理身份的UI模式。

person作者: jakexiaohubgithub

Frontend Web3 for BAP-578

Use this skill to integrate BAP-578 smart contract reads and writes into a Next.js/React frontend. This skill covers everything from wallet connection through to building production-quality agent management UIs.


When to use this skill

  • Adding Web3 wallet connectivity for BNB Chain or BSC Testnet to a Next.js app.
  • Building mint flows, agent dashboards, or identity cards for BAP-578.
  • Creating React hooks that wrap contract reads and writes.
  • Integrating wagmi, viem, and RainbowKit with the BAP-578 ABI.
  • Rendering the four identity questions as UI components.

The Four Identity Questions (Frontend View)

1) Who are you?

The UI renders the agent's on-chain identity by reading getAgentState and getAgentMetadata. Display the token ID, owner address, persona (parsed from JSON), experience text, and logic address. Show active/inactive status with a visual indicator (green dot for active, grey for inactive). If a logic contract is bound, display the address with an indicator that behavior is externally defined.

2) What do you remember?

The frontend reads structured metadata from the contract and optionally fetches extended memory from vaultURI. To verify vault integrity, the frontend should:

  1. Fetch the content at vaultURI.
  2. Compute keccak256 of the raw content using viem's keccak256 utility.
  3. Compare the result to the on-chain vaultHash.
  4. Show a "Verified" badge if they match, or "Unverified" warning if not.

Display on-chain event history by querying past events filtered by token ID: AgentCreated, AgentFunded, AgentWithdraw, AgentStatusChanged, MetadataUpdated.

3) What can you do?

The UI exposes these agent capabilities as interactive forms and buttons:

  • Mint agent — form with persona, experience, metadata URI, and optional logic address
  • Fund agent — input for BNB amount, calls fundAgent
  • Withdraw — input for amount, calls withdrawFromAgent
  • Toggle status — switch to call setAgentStatus
  • Update metadata — form for new persona, experience, vault URI
  • Bind logic — address input for setLogicAddress

4) How can I trust it?

The frontend only signs transactions through the user's own wallet (MetaMask, WalletConnect, etc.). There is no backend mutation path. Every write goes directly to the on-chain contract. The UI should:

  • Show the contract address prominently so users can verify on a block explorer.
  • Display transaction hashes after each write for independent verification.
  • Never request or store private keys.
  • Treat off-chain data as untrusted unless vault hash matches.

Tech Stack

  • Next.js 14+ (App Router recommended)
  • wagmi v2 — React hooks for Ethereum
  • viem — low-level typed EVM client
  • @tanstack/react-query — caching layer for wagmi
  • @rainbow-me/rainbowkit — wallet connection modal
  • TypeScript — strongly typed throughout

Step-by-Step Integration

Step 1: Install dependencies

npm install wagmi viem @tanstack/react-query @rainbow-me/rainbowkit

Step 2: Create Web3 config

Create config/web3.ts:

import { http } from "wagmi";
import { bsc, bscTestnet } from "wagmi/chains";
import { getDefaultConfig } from "@rainbow-me/rainbowkit";

export const wagmiConfig = getDefaultConfig({
  appName: "NFA",
  projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "",
  chains: [bsc, bscTestnet],
  transports: {
    [bsc.id]: http(process.env.NEXT_PUBLIC_BSC_RPC_URL),
    [bscTestnet.id]: http(process.env.NEXT_PUBLIC_BSC_TESTNET_RPC_URL),
  },
  ssr: true,
});

Required env vars in .env.local:

NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id
NEXT_PUBLIC_BSC_RPC_URL=your_bsc_rpc
NEXT_PUBLIC_BSC_TESTNET_RPC_URL=your_bsc_testnet_rpc
NEXT_PUBLIC_BAP578_ADDRESS=0xYourDeployedAddress

Step 3: Create providers wrapper

Create components/Web3Provider.tsx:

"use client";

import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { wagmiConfig } from "@/config/web3";
import "@rainbow-me/rainbowkit/styles.css";

const queryClient = new QueryClient();

export function Web3Provider({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        <RainbowKitProvider>{children}</RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}

Add <Web3Provider> to your root layout wrapping {children}.

Step 4: Define ABI and contract config

Create config/bap578.ts:

export const BAP578_ADDRESS = process.env.NEXT_PUBLIC_BAP578_ADDRESS as `0x${string}`;

export const BAP578_ABI = [
  // Minting
  {
    name: "createAgent",
    type: "function",
    stateMutability: "payable",
    inputs: [
      { name: "to", type: "address" },
      { name: "logicAddress", type: "address" },
      { name: "metadataURI", type: "string" },
      {
        name: "metadata",
        type: "tuple",
        components: [
          { name: "persona", type: "string" },
          { name: "experience", type: "string" },
          { name: "voiceHash", type: "string" },
          { name: "animationURI", type: "string" },
          { name: "vaultURI", type: "string" },
          { name: "vaultHash", type: "bytes32" },
        ],
      },
    ],
    outputs: [{ name: "tokenId", type: "uint256" }],
  },
  // Funding
  {
    name: "fundAgent",
    type: "function",
    stateMutability: "payable",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [],
  },
  // Withdrawal
  {
    name: "withdrawFromAgent",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "tokenId", type: "uint256" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [],
  },
  // Status
  {
    name: "setAgentStatus",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "tokenId", type: "uint256" },
      { name: "active", type: "bool" },
    ],
    outputs: [],
  },
  // Logic binding
  {
    name: "setLogicAddress",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "tokenId", type: "uint256" },
      { name: "logicAddress", type: "address" },
    ],
    outputs: [],
  },
  // Metadata update
  {
    name: "updateAgentMetadata",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "tokenId", type: "uint256" },
      { name: "newURI", type: "string" },
      {
        name: "newMetadata",
        type: "tuple",
        components: [
          { name: "persona", type: "string" },
          { name: "experience", type: "string" },
          { name: "voiceHash", type: "string" },
          { name: "animationURI", type: "string" },
          { name: "vaultURI", type: "string" },
          { name: "vaultHash", type: "bytes32" },
        ],
      },
    ],
    outputs: [],
  },
  // Views
  {
    name: "getAgentState",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [
      {
        name: "",
        type: "tuple",
        components: [
          { name: "balance", type: "uint256" },
          { name: "active", type: "bool" },
          { name: "logicAddress", type: "address" },
          { name: "createdAt", type: "uint256" },
          { name: "owner", type: "address" },
        ],
      },
    ],
  },
  {
    name: "getAgentMetadata",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [
      {
        name: "",
        type: "tuple",
        components: [
          { name: "persona", type: "string" },
          { name: "experience", type: "string" },
          { name: "voiceHash", type: "string" },
          { name: "animationURI", type: "string" },
          { name: "vaultURI", type: "string" },
          { name: "vaultHash", type: "bytes32" },
        ],
      },
    ],
  },
  {
    name: "tokensOfOwner",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "owner", type: "address" }],
    outputs: [{ name: "", type: "uint256[]" }],
  },
  {
    name: "getTotalSupply",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "getFreeMints",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "user", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "isFreeMint",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [{ name: "", type: "bool" }],
  },
] as const;

Step 5: Build React hooks

Create hooks/useBap578.ts:

import { useReadContract, useWriteContract, useAccount } from "wagmi";
import { parseEther, zeroAddress, zeroHash } from "viem";
import { BAP578_ADDRESS, BAP578_ABI } from "@/config/bap578";

export function useAgentState(tokenId: bigint) {
  return useReadContract({
    address: BAP578_ADDRESS,
    abi: BAP578_ABI,
    functionName: "getAgentState",
    args: [tokenId],
  });
}

export function useAgentMetadata(tokenId: bigint) {
  return useReadContract({
    address: BAP578_ADDRESS,
    abi: BAP578_ABI,
    functionName: "getAgentMetadata",
    args: [tokenId],
  });
}

export function useOwnedTokens() {
  const { address } = useAccount();
  return useReadContract({
    address: BAP578_ADDRESS,
    abi: BAP578_ABI,
    functionName: "tokensOfOwner",
    args: address ? [address] : undefined,
    query: { enabled: !!address },
  });
}

export function useFreeMints() {
  const { address } = useAccount();
  return useReadContract({
    address: BAP578_ADDRESS,
    abi: BAP578_ABI,
    functionName: "getFreeMints",
    args: address ? [address] : undefined,
    query: { enabled: !!address },
  });
}

export function useMintAgent() {
  const { writeContract, ...rest } = useWriteContract();
  const mint = (params: {
    to: `0x${string}`;
    persona: string;
    experience: string;
    metadataURI: string;
    value?: bigint;
  }) => {
    writeContract({
      address: BAP578_ADDRESS,
      abi: BAP578_ABI,
      functionName: "createAgent",
      args: [
        params.to,
        zeroAddress,
        params.metadataURI,
        {
          persona: params.persona,
          experience: params.experience,
          voiceHash: "",
          animationURI: "",
          vaultURI: "",
          vaultHash: zeroHash,
        },
      ],
      value: params.value ?? 0n,
    });
  };
  return { mint, ...rest };
}

export function useFundAgent() {
  const { writeContract, ...rest } = useWriteContract();
  const fund = (tokenId: bigint, amountBnb: string) => {
    writeContract({
      address: BAP578_ADDRESS,
      abi: BAP578_ABI,
      functionName: "fundAgent",
      args: [tokenId],
      value: parseEther(amountBnb),
    });
  };
  return { fund, ...rest };
}

export function useWithdrawFromAgent() {
  const { writeContract, ...rest } = useWriteContract();
  const withdraw = (tokenId: bigint, amountBnb: string) => {
    writeContract({
      address: BAP578_ADDRESS,
      abi: BAP578_ABI,
      functionName: "withdrawFromAgent",
      args: [tokenId, parseEther(amountBnb)],
    });
  };
  return { withdraw, ...rest };
}

Step 6: Build UI components

Recommended component set:

  • WalletButton — uses <ConnectButton /> from RainbowKit
  • MintAgentForm — collects persona, experience, URI; calls useMintAgent
  • AgentDashboard — lists owned tokens via useOwnedTokens, renders cards
  • AgentIdentityCard — displays one agent's four-question profile
  • FundAgentForm — BNB amount input, calls useFundAgent
  • WithdrawAgentForm — amount input, calls useWithdrawFromAgent
  • AgentStatusToggle — switch that calls setAgentStatus
  • VaultVerifier — fetches vault URI, hashes content, compares to on-chain hash

Vault Verification Pattern

import { keccak256, toHex } from "viem";

async function verifyVault(vaultURI: string, onChainHash: `0x${string}`) {
  const res = await fetch(vaultURI);
  const content = await res.text();
  const hash = keccak256(toHex(content));
  return hash === onChainHash;
}

Display a green "Verified" badge if true, or a red "Unverified — data may have been tampered with" warning if false. If vaultHash is the zero hash, display "No vault data registered".


Event History Display

Use viem's getContractEvents to fetch historical events for a token:

import { publicClient } from "@/config/web3";
import { BAP578_ADDRESS, BAP578_ABI } from "@/config/bap578";

async function getAgentHistory(tokenId: bigint) {
  const events = await publicClient.getContractEvents({
    address: BAP578_ADDRESS,
    abi: BAP578_ABI,
    fromBlock: 0n,
    toBlock: "latest",
  });
  return events.filter((e) => {
    const args = e.args as Record<string, unknown>;
    return args.tokenId === tokenId;
  });
}

Render these as a timeline component showing creation, funding, withdrawals, status changes, and metadata updates with timestamps and transaction hashes.


Constraints and Safety Rules

  • Free mints must go to self. The UI should auto-set to = connected address when free mints remain.
  • Send exact fee. After free mints are exhausted, send the exact mint fee. Read the fee from the contract or config.
  • No private keys. Never ask for or store private keys. All signing happens in the wallet.
  • Untrusted off-chain data. Always verify vault content against vaultHash before displaying.
  • Chain switching. Prompt users to switch to BNB Chain if they are on the wrong network.
  • Error handling. Catch and display user-friendly messages for common revert reasons.

Common Frontend Errors and Fixes

  • "User rejected transaction" — user declined in wallet; no action needed.
  • "Insufficient funds" — not enough BNB for gas + value; show balance warning.
  • "Execution reverted: incorrect fee" — free mints exhausted; recalculate fee.
  • "Execution reverted: not token owner" — wrong wallet connected.
  • "Execution reverted: contract is paused" — admin has paused; show notice.

Output Format

When asked for frontend help, respond with:

  1. Components/files to create or modify
  2. Complete code snippets (not fragments)
  3. Env vars required
  4. How to test the flow (connect wallet → perform action → verify result)
  5. Error handling notes

Mobile Considerations

Responsive design

The BAP-578 frontend should work on mobile devices where most Web3 users interact:

  • Use responsive layouts (CSS Grid/Flexbox) for agent cards
  • Make wallet connect buttons large and touch-friendly
  • Simplify forms on mobile (collapsible metadata fields)
  • Test with mobile wallets (MetaMask Mobile, Trust Wallet)

Deep linking

Support deep links to specific agents for sharing:

https://app.example.com/agent/17
https://app.example.com/owner/0xABC...

Progressive Web App (PWA)

Consider PWA support for mobile users who want an app-like experience:

  • Offline agent profile caching
  • Push notifications for funding and status events
  • Home screen installation

Accessibility

  • Use semantic HTML for agent cards and forms
  • Provide alt text for agent animation assets
  • Ensure wallet address displays are screen-reader friendly (e.g., "Address starting with 0 x A B C")
  • Support keyboard navigation for all interactive elements
  • Use ARIA labels on custom components (status toggles, fund buttons)
  • Ensure color contrast meets WCAG 2.1 AA standards

Related Skills

  • bap578
  • bap578-scanner
  • bap578-metadata-design