npm package: @provable-games/metagame-sdk
Description: Shared types, utilities, and headless hooks for Provable Games metagame SDKs
Author: Provable Games
Latest version: 0.1.7

# @provable-games/metagame-sdk

Shared types, utilities, and headless React hooks for Provable Games metagame UIs — powering [Budokan](https://github.com/Provable-Games/budokan) (tournaments) and [Bokendo](https://github.com/Provable-Games/bokendo) (quests).

## Features

- **Unified types** — `Token`, `Prize`, `EntryFee`, `Participant`, `StatusTimestamps`
- **Utilities** — Address formatting, number display, prize aggregation, status computation, entry fee calculations, qualification evaluation, proof building, extension config parsing
- **Headless hooks** — Pagination, search, token selection, status indicators, score tables, prize tables, entry fee preview, entry/extension qualification
- **RPC utilities** — On-chain extension and fee/prize config reading via Starknet RPC
- **Tree-shakeable** — Main entry has zero React dependency; hooks and RPC are separate entry points
- **ESM + CJS** — Dual build with full TypeScript declarations

## Install

```bash
npm install @provable-games/metagame-sdk
# or
bun add @provable-games/metagame-sdk
```

**Optional peer dependencies** (install only what you use):

```bash
npm install react      # For hooks (/react entry)
npm install starknet   # For RPC utilities (/rpc entry)
```

## Quick Start

### Utilities (no React needed)

```ts
import {
  indexAddress,
  formatNumber,
  formatPrizeAmount,
  computeStatus,
  groupPrizesByToken,
  getOrdinalSuffix,
} from "@provable-games/metagame-sdk";

// Normalize a Starknet address
indexAddress("0x00000abc"); // "0xabc"

// Format numbers with smart suffixes
formatNumber(1500000); // "1.5m"
formatNumber(2500);    // "2.5k"

// Compute tournament/quest status from timestamps
const status = computeStatus({
  registrationStart: 1000,
  registrationEnd: 2000,
  start: 3000,
  end: 4000,
}, Date.now() / 1000);
// { label: "Live", variant: "active", isActive: true, countdown: { ... } }

// Group prizes by token
const grouped = groupPrizesByToken(prizes);
// { "0xabc": { type: "erc20", address: "0xabc", totalAmount: "150" } }

// Ordinal suffixes
getOrdinalSuffix(1);  // "st"
getOrdinalSuffix(22); // "nd"
```

### Entry Fee & Prize Calculations

```ts
import {
  calculateEntryFeeBreakdown,
  distributePool,
  buildEntryFeePrizes,
  calculateTotalPrizeValueUSD,
  calculatePaidPlaces,
} from "@provable-games/metagame-sdk";

// Break down entry fee into creator/protocol/prize shares
const breakdown = calculateEntryFeeBreakdown(entryFee, participantCount);

// Calculate prize pool distribution
const distribution = distributePool(totalPool, positions, weight);
```

### Qualification & Proofs

```ts
import {
  evaluateQualification,
  buildQualificationProof,
  identifyExtensionType,
  parseTournamentValidatorConfig,
} from "@provable-games/metagame-sdk";

// Check if a user qualifies for entry
const result = evaluateQualification(requirement, userState);

// Build proof for on-chain verification
const proof = buildQualificationProof(qualificationData);

// Parse extension configs from on-chain data
const config = parseTournamentValidatorConfig(rawConfig);
```

### React Hooks

```tsx
import { useStatusIndicator, useTokenSelector, usePagination } from "@provable-games/metagame-sdk/react";

// Auto-refreshing status indicator
function StatusBadge({ timestamps }) {
  const status = useStatusIndicator(timestamps); // refreshes every second
  return <span className={status.variant}>{status.label}</span>;
}

// Token selector with search + pagination
function TokenPicker({ tokens }) {
  const { search, setSearch, filteredTokens, select, pagination, getTokenProps } =
    useTokenSelector({ tokens, tokenType: "erc20", pageSize: 10 });

  return (
    <div>
      <input value={search} onChange={(e) => setSearch(e.target.value)} />
      {filteredTokens.map((token) => (
        <div key={token.address} {...getTokenProps(token)}>
          {token.symbol}
        </div>
      ))}
      <button onClick={pagination.prev} disabled={!pagination.hasPrev}>Prev</button>
      <button onClick={pagination.next} disabled={!pagination.hasNext}>Next</button>
    </div>
  );
}

// Entry qualification check
import { useEntryQualification } from "@provable-games/metagame-sdk/react";

function EntryGate({ requirement, userAddress }) {
  const { qualified, reason, loading } = useEntryQualification({ requirement, userAddress });
  if (loading) return <span>Checking...</span>;
  return qualified ? <button>Enter</button> : <span>{reason}</span>;
}
```

### RPC Utilities

```ts
import { readExtensionConfig, readFeeAndPrizeExtensions } from "@provable-games/metagame-sdk/rpc";

// Read extension configuration from on-chain contracts
const config = await readExtensionConfig(provider, extensionAddress);
```

## API Reference

### Types

| Type | Fields |
|------|--------|
| `Token` | `address`, `name`, `symbol`, `tokenType`, `decimals?`, `logoUrl?` |
| `Prize` | `id`, `position`, `tokenAddress`, `tokenType`, `amount`, `sponsorAddress` |
| `EntryFee` | `tokenAddress`, `amount`, `creatorShare?`, `refundShare?` |
| `EntryRequirement` | `requirementType`, `tokenAddress?`, `entryLimit?` |
| `Participant` | `address`, `rank?`, `score?`, `name?`, `entryNumber?` |
| `StatusTimestamps` | `start`, `end`, `registrationStart?`, `registrationEnd?`, `submissionEnd?`, `completed?`, `unlocked?` |
| `StatusResult` | `label`, `variant`, `isActive`, `countdown` |

### Utilities

| Function | Description |
|----------|-------------|
| `indexAddress(address)` | Strip leading zeros from hex address |
| `padAddress(address)` | Pad address to 66 characters |
| `displayAddress(address)` | Truncate to `0x1234...abcd` |
| `bigintToHex(value)` | Convert bigint/number/string to hex |
| `formatNumber(num)` | Smart formatting with k/m suffixes |
| `formatPrizeAmount(num)` | Precision-aware prize display |
| `formatUsdValue(value)` | USD display with `<0.01` handling |
| `formatScore(num)` | Score display formatting |
| `formatTime(seconds)` | Human-readable duration ("2 Days", "3 Hours") |
| `getOrdinalSuffix(n)` | Returns "st", "nd", "rd", "th" |
| `calculatePayouts(places, weight)` | Weighted payout percentages summing to 100 |
| `calculateDistribution(positions, weight, ...)` | Basis-point distribution matching contract logic |
| `groupPrizesByToken(prizes)` | Group prizes by token address |
| `calculatePrizeValue(amount, decimals, price)` | Compute USD value of a prize |
| `computeStatus(timestamps, now?)` | Derive status label, variant, and countdown |
| `isBefore(date1, date2)` | Date comparison |
| `calculateEntryFeeBreakdown(fee, count)` | Break down entry fee into shares |
| `distributePool(pool, positions, weight)` | Distribute prize pool across positions |
| `aggregatePrizesByPosition(prizes)` | Group prizes by leaderboard position |
| `aggregatePrizesBySponsor(prizes)` | Group prizes by sponsor address |
| `filterClaimablePrizes(prizes, ...)` | Filter prizes claimable by a participant |
| `filterZeroPrizes(prizes)` | Remove zero-amount prizes |
| `parseNFTBulkInput(input)` | Parse bulk NFT token ID input |
| `getExtensionAddresses(chain)` | Get known extension contract addresses |
| `identifyExtensionType(address)` | Identify extension type from address |
| `parseTournamentValidatorConfig(config)` | Parse tournament validator config |
| `parseERC20BalanceValidatorConfig(config)` | Parse ERC20 balance validator config |
| `parseOpusTrovesValidatorConfig(config)` | Parse Opus Troves validator config |
| `evaluateQualification(requirement, state)` | Evaluate entry qualification |
| `buildQualificationProof(data)` | Build proof for on-chain verification |
| `buildEntryFeePrizes(config)` | Build prize list from entry fee config |
| `calculateTotalPrizeValueUSD(prizes, ...)` | Calculate total prize pool USD value |
| `calculatePaidPlaces(prizes)` | Count positions with non-zero prizes |
| `buildParticipationMap(registrations)` | Map addresses to participation counts |
| `buildWinMap(leaderboards)` | Map addresses to win counts |
| `resolveTournamentQualifications(input)` | Resolve tournament validator qualifications |
| `calculateOpusTrovesEntries(troves)` | Calculate entries from Opus Troves |
| `findBannableEntries(entries, ...)` | Find entries eligible for banning |

### React Hooks (`/react` entry)

| Hook | Description |
|------|-------------|
| `useDebounce(value, delay)` | Debounce any value |
| `usePagination({ totalItems, pageSize? })` | Page state with next/prev/reset |
| `useSearchFilter({ items, searchFields, debounceMs? })` | Filtered list with debounced search |
| `useTokenSelector({ tokens, tokenType?, pageSize? })` | Token search + selection + pagination |
| `useStatusIndicator(timestamps, refreshMs?)` | Auto-refreshing status computation |
| `useScoreTable({ participants, pageSize?, sortField? })` | Sorted, searchable, paginated score table |
| `usePrizeTable({ prizes, ... })` | Prize display with grouping and pagination |
| `useEntryFeePreview({ entryFee, ... })` | Entry fee breakdown preview |
| `useEntryQualification({ requirement, ... })` | Entry qualification check |
| `useExtensionQualification({ extension, ... })` | Extension-based qualification evaluation |
| `useOpusTrovesBannableEntries({ entries, ... })` | Identify bannable Opus Troves entries |

### RPC Utilities (`/rpc` entry)

| Function | Description |
|----------|-------------|
| `readExtensionConfig(provider, address)` | Read extension config from chain |
| `readFeeAndPrizeExtensions(provider, ...)` | Read fee and prize extension data |

## Development

```bash
npm install
npm run build        # ESM + CJS to dist/
npm run typecheck    # TypeScript validation
npm test             # Unit tests
npm run dev          # Watch mode
```

## Publishing

Automated via GitHub Actions — create a GitHub release to trigger npm publish.

## License

MIT