npm package: @prime-vaults/web3
Description: Token SDK for Web3 DApps - Components, hooks, and utilities for token/chain management
Latest version: 0.1.8

# @prime-vaults/web3

Web3 SDK for DApps - Components, hooks, and utilities for token/chain management.

## Features

- Token components with auto-fetch from API
- Chain utilities with viem/chains integration
- Token balance & approval hooks
- Transaction mutation with React Query
- Format utilities for amounts, USD, numbers
- TypeScript first, zero styling (BYO styles)

## Installation

```bash
pnpm add @prime-vaults/web3
```

### Peer Dependencies

```bash
pnpm add react wagmi viem @tanstack/react-query
```

## Quick Start

```tsx
import { TokenProvider } from '@prime-vaults/web3'

function App() {
  return (
    <TokenProvider apiUrl="https://api.example.com/api">
      <YourApp />
    </TokenProvider>
  )
}
```

---

## Components

### Token Components

```tsx
import { TokenIcon, TokenSymbol, TokenName, TokenAmount, TokenBalance } from '@prime-vaults/web3'

// Token icon with auto-fetch
<TokenIcon address="0x..." chainId={80094} className="w-8 h-8" />

// Token symbol/name
<TokenSymbol address="0x..." chainId={80094} />  // "HONEY"
<TokenName address="0x..." chainId={80094} />    // "Honey Token"

// Token amount (raw bigint)
<TokenAmount address="0x..." chainId={80094} rawAmount={1000000000000000000n} />

// Token amount (decimalized)
<TokenAmount address="0x..." chainId={80094} amount={1.5} />

// As USD value
<TokenAmount address="0x..." chainId={80094} rawAmount={1000000000000000000n} usd />

// Token balance (connected wallet)
<TokenBalance address="0x..." chainId={80094} />
<TokenBalance address="0x..." chainId={80094} usd />
```

### Chain Components

```tsx
import { ChainIcon, ChainName, ChainSymbol } from '@prime-vaults/web3'

<ChainIcon chainId={80094} className="w-6 h-6" />
<ChainName chainId={80094} />    // "Berachain"
<ChainSymbol chainId={80094} />  // "BERA"
```

---

## Hooks

### Token Hooks

```tsx
import {
  useToken,
  useTokensByChain,
  useTokenAmount,
  useTokenBalance,
} from '@prime-vaults/web3'

// Get single token (auto-fetches if not in store)
const token = useToken(80094, '0x...')
// { symbol, name, decimals, priceUSD, logoURI, ... }

// Get all tokens for a chain
const tokens = useTokensByChain(80094)

// Format token amount
const { formatted, usd, symbol } = useTokenAmount(80094, '0x...', rawAmount)

// Get token balance for connected wallet
const { balance, formatted, usd, isLoading } = useTokenBalance({
  chainId: 80094,
  address: '0x...',
})
```

### Approval Hook

```tsx
import {
  useApproveToken,
  useAllowance,
  useIsApproved,
} from '@prime-vaults/web3'

// Full approval flow
const { approve, isApproved, allowance, isLoading } = useApproveToken({
  chainId: 80094,
  tokenAddress: '0x...',
  spenderAddress: '0x...',
  amount: parseUnits('100', 18),
})

// Just check allowance
const { allowance, refetch } = useAllowance({
  chainId: 80094,
  tokenAddress: '0x...',
  spenderAddress: '0x...',
})

// Just check if approved
const isApproved = useIsApproved({
  chainId: 80094,
  tokenAddress: '0x...',
  spenderAddress: '0x...',
  amount: parseUnits('100', 18),
})
```

### Transaction Mutation

Built on React Query's `useMutation` - returns standard mutation result.

```tsx
import { useTxMutation } from '@prime-vaults/web3'

const tx = useTxMutation({
  address: '0x...',
  abi: vaultAbi,
  functionName: 'deposit',
  chainId: 80094, // optional, defaults to connected chain
  confirmations: 1, // optional, default 1
  callbacks: {
    onSigning: () => toast.loading('Please sign in wallet...'),
    onPending: (hash) => toast.loading(`Tx submitted: ${hash.slice(0, 10)}...`),
    onSuccess: (hash, receipt) => toast.success('Transaction confirmed!'),
    onError: (error, isUserRejection) => {
      if (isUserRejection) toast.warn('Transaction cancelled')
      else toast.error(error.message)
    },
  },
  invalidateKeys: ['my-custom-query'], // optional, extra query keys to invalidate
})

// Execute transaction
await tx.mutateAsync({ args: [amount, receiver], value: parseEther('0.1') })

// Or without args (for no-param functions)
await tx.mutateAsync({})

// Returns UseMutationResult from React Query
tx.isPending // loading state
tx.isSuccess // success state
tx.isError // error state
tx.data // TransactionReceipt
tx.error // Error object
tx.mutate() // fire-and-forget version
tx.reset() // reset mutation state
```

**Auto-invalidates queries:** `readContract`, `readContracts`, `balance`, `token`, `blockNumber`, `web3-sdk`

**Note:** Returns standard `UseMutationResult` from React Query - all mutation properties available.

---

## Utilities

### Format Utilities

```tsx
import {
  formatTokenAmount,
  formatNumber,
  formatUsdValue,
  formatPrice,
  formatTimeAgo,
  calculateUsdValue,
  formatBps,
  parseBps,
} from '@prime-vaults/web3'

// Format bigint to display string
formatTokenAmount(1000000000000000000n, 18) // "1.00"
formatTokenAmount(1234567890000000000000n, 18) // "1,234.57"
formatTokenAmount(10000000000000n, 18) // "0.00001"
formatTokenAmount(123n, 18) // "0.0₁₃123" (subscript)

// Format numbers (auto trim trailing zeros for small numbers)
formatNumber(1234567) // "1.23M"
formatNumber(1234.56) // "1,234.56"
formatNumber(12.3) // "12.30"
formatNumber(0.123) // "0.123"
formatNumber(0.001) // "0.001"
formatNumber(0.0000001) // "0.0₆1" (subscript for very small)

// Format USD
formatUsdValue(1234567) // "$1.23M"
formatUsdValue(1234.56) // "$1,234.56"
formatUsdValue(0.005) // "<$0.01"

// Format price (supports very small prices like meme coins)
formatPrice(1234.56) // "$1,234.56"
formatPrice(0.001) // "$0.001"
formatPrice(0.00000123) // "$0.0₅123" (subscript)

// Calculate USD value
calculateUsdValue(1000000000000000000n, 18, 2500) // 2500

// Format relative time
formatTimeAgo(new Date(Date.now() - 30000)) // "30s ago"
formatTimeAgo(new Date(Date.now() - 3600000)) // "1h ago"
formatTimeAgo(new Date(Date.now() - 86400000)) // "1d ago"

// Format basis points to percentage (1 bps = 0.01%)
formatBps(100n) // "1.00%"
formatBps(50n) // "0.50%"
formatBps(10000n) // "100.00%"

// Parse percentage to basis points
parseBps(1) // 100n (1% = 100 bps)
parseBps(0.5) // 50n (0.5% = 50 bps)
```

### Chain Utilities

```tsx
import {
  SUPPORTED_CHAINS,
  SUPPORTED_VIEM_CHAINS,
  VIEM_CHAINS,
  getChainConfig,
  getSupportedChainIds,
  getExplorerAddressUrl,
  getExplorerTxUrl,
  getExplorerTokenUrl,
  getChainLogoUrl,
  getChainName,
  getChainShortName,
  getChainColor,
  getNativeSymbol,
  getViemChain,
  getPublicClient,
} from '@prime-vaults/web3'

// Get chain config
const config = getChainConfig(80094)
// { chain, chainId, name, shortName, explorerUrl, nativeSymbol, logoUrl, color }

// Access raw viem Chain for wagmi config
const viemChains = SUPPORTED_VIEM_CHAINS // Chain[]
const berachain = VIEM_CHAINS.berachain // Chain

// Explorer URLs
getExplorerAddressUrl('0x...', 80094) // "https://berascan.com/address/0x..."
getExplorerTxUrl('0x...', 80094) // "https://berascan.com/tx/0x..."
getExplorerTokenUrl('0x...', 80094) // "https://berascan.com/token/0x..."

// Chain info
getChainName(80094) // "Berachain"
getChainShortName(80094) // "BERA"
getChainColor(80094) // "#FF6B00"
getNativeSymbol(80094) // "BERA"
getChainLogoUrl(80094) // "https://icons.llamao.fi/icons/chains/rsz_berachain.jpg"
getSupportedChainIds() // [1, 56, 42161, 1116, 80094]

// Get raw viem Chain object
const chain = getViemChain(80094)

// Create PublicClient for reading blockchain data
const client = getPublicClient(80094)
const balance = await client.getBalance({ address: '0x...' })
```

---

## Supported Chains

| Chain     | ID    | Short | Color   |
| --------- | ----- | ----- | ------- |
| Ethereum  | 1     | ETH   | #627EEA |
| BNB Chain | 56    | BNB   | #F3BA2F |
| Arbitrum  | 42161 | ARB   | #28A0F0 |
| Core DAO  | 1116  | CORE  | #FF9500 |
| Berachain | 80094 | BERA  | #FF6B00 |

---

## Address Utilities

```tsx
import { isEqualAddress, normalizeAddress } from '@prime-vaults/web3'

// Compare addresses (case-insensitive, null-safe)
isEqualAddress('0xABC...', '0xabc...') // true
isEqualAddress(undefined, '0xABC...') // false

// Normalize address to lowercase
normalizeAddress('0xAbC...DeF') // '0xabc...def'
normalizeAddress('invalid') // undefined
```

---

## Types

```tsx
import type {
  Token,
  TokenKey,
  FormattedAmount,
  ChainConfig,
  TxCallbacks,
  TxArgs,
  UseTxMutationOptions,
} from '@prime-vaults/web3'

interface Token {
  _id: string
  chainId: number
  address: `0x${string}`
  symbol: string
  name: string
  decimals: number
  priceUSD?: number
  logoURI?: string
  // ...
}

interface ChainConfig {
  chain: Chain // raw viem Chain
  chainId: number
  name: string
  shortName: string
  explorerUrl: string
  nativeSymbol: string
  logoUrl?: string
  color: string
}
```

---

## API Requirements

The SDK expects the following API endpoint:

```
GET /analytics/tokens
```

Response format:

```json
{
  "success": true,
  "data": {
    "tokens": [
      {
        "address": "0x...",
        "chainId": 80094,
        "symbol": "HONEY",
        "name": "Honey",
        "decimals": 18,
        "logoURI": "https://...",
        "priceUSD": 1.0
      }
    ]
  }
}
```

---

## Advanced Usage

### Custom Query Invalidation

Use `TX_MUTATION_KEY` to invalidate your custom queries on transaction success:

```tsx
import { TX_MUTATION_KEY } from '@prime-vaults/web3'
import { useQuery } from '@tanstack/react-query'

// Your custom query
const { data } = useQuery({
  queryKey: [TX_MUTATION_KEY, 'my-data'],
  queryFn: fetchMyData,
})

// Will be auto-invalidated after any useTxMutation success
```

### Using viem Chains for Wagmi Config

```tsx
import { SUPPORTED_VIEM_CHAINS } from '@prime-vaults/web3'
import { createConfig, http } from 'wagmi'

const config = createConfig({
  chains: SUPPORTED_VIEM_CHAINS,
  transports: {
    [1]: http(),
    [56]: http(),
    [42161]: http(),
    [1116]: http(),
    [80094]: http(),
  },
})
```

---

## Prime Auth

Wallet authentication with JWT for Prime platform. Tokens are stored per wallet address - switching wallets loads existing tokens without re-signing.

### Setup

```tsx
import { WagmiProvider } from 'wagmi'
import { PrimeAuthProvider } from '@prime-vaults/web3'

function App() {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <PrimeAuthProvider
          apiUrl="https://api.prime.com"
          autoReSign={true}
          callbacks={{
            onLoginSuccess: (user) =>
              toast.success(`Welcome ${user.fullname}!`),
            onTokenExpired: () => toast.warning('Session expired'),
            onLogout: () => toast.info('Logged out'),
            onAccountChanged: (address) => console.log('Switched to', address),
          }}
        >
          <YourApp />
        </PrimeAuthProvider>
      </QueryClientProvider>
    </WagmiProvider>
  )
}
```

### Hooks

```tsx
import {
  usePrimeAuth,
  usePrimeUser,
  usePrimeToken,
  useIsAuthenticated,
  useAuthApi,
} from '@prime-vaults/web3'

// Main hook - state + actions
function LoginButton() {
  const { login, logout, isAuthenticated, isAuthenticating } = usePrimeAuth()

  if (isAuthenticated) {
    return <button onClick={logout}>Logout</button>
  }

  return (
    <button onClick={login} disabled={isAuthenticating}>
      {isAuthenticating ? 'Signing...' : 'Login'}
    </button>
  )
}

// User hook
function UserProfile() {
  const user = usePrimeUser()
  const isAuth = useIsAuthenticated()

  if (!isAuth) return <p>Not logged in</p>
  return <p>Welcome, {user?.fullname}</p>
}

// useAuthApi - axios instance with auto Authorization header
function ApiCall() {
  const api = useAuthApi() // Uses apiUrl from provider
  // or: const api = useAuthApi('https://custom-api.com')

  const fetchData = async () => {
    // Authorization header automatically added
    // 401 errors automatically clear token
    const { data } = await api.get('/users/api/v1/data')
    return data
  }
}

// Manual token access (if needed)
function ManualApiCall() {
  const jwtToken = usePrimeToken()

  const fetchData = () => {
    return axios.get('/api/data', {
      headers: { Authorization: `Bearer ${jwtToken}` },
    })
  }
}
```

### Features

- **Per-wallet token storage** - Switch wallet A → B → A, token for A is loaded without re-signing
- **Auto re-sign on expiry** - 401 response triggers re-authentication (if autoReSign enabled)
- **Auto logout on disconnect** - Wallet disconnect keeps tokens persisted for later
- **useAuthApi hook** - Axios instance with Authorization header, auto-handles 401
- **JWT persistence** - Stored in localStorage per wallet address

### Store Access

```tsx
import {
  usePrimeAuthStore,
  useCurrentAccount,
  useCurrentToken,
  useCurrentUser,
} from '@prime-vaults/web3'

// Hook selectors
const account = useCurrentAccount() // { jwtToken, user } | null
const token = useCurrentToken() // string | null
const user = useCurrentUser() // PrimeUser | null

// Full store access
const {
  accounts, // Record<address, { jwtToken, user }>
  setAccount, // (address, token, user?) => void
  getAccount, // (address) => AccountData | null
  clearAccount, // (address) => void
  clearAll, // () => void
} = usePrimeAuthStore()
```

### Types

```tsx
import type {
  PrimeUser,
  PrimeAuthConfig,
  PrimeAuthCallbacks,
} from '@prime-vaults/web3'

interface PrimeUser {
  id: string
  avatar: string
  fullname: string
  address: `0x${string}`
  status: 'active' | 'inactive'
  shared_key: string
  created_at: Date
  updated_at: Date
}

interface PrimeAuthCallbacks {
  onLoginStart?: () => void
  onLoginSuccess?: (user: PrimeUser) => void
  onLoginError?: (error: Error) => void
  onLogout?: () => void
  onTokenExpired?: () => void
  onAccountChanged?: (newAddress: `0x${string}`) => void
}
```

---

## License

MIT