diff --git a/clash-onchain-buy-chest/SKILL.md b/clash-onchain-buy-chest/SKILL.md new file mode 100644 index 0000000000..fecad75c3e --- /dev/null +++ b/clash-onchain-buy-chest/SKILL.md @@ -0,0 +1,322 @@ +--- +name: clash-onchain-buy-chest +description: > + Buy chest in Clash-Onchain blockchain game on Base Mainnet using BankrBot Agent API. + + Triggers: + - "buy chest" + - "open {silver|gold|magical} chest" + - "purchase chest type {0|1|2}" + - "buy chest 0" / "buy chest 1" / "buy chest 2" + + Boundaries: + - NOT for checking balance → use clash-onchain-check-balance + - NOT for claiming battle rewards → use clash-onchain-claim-reward + - NOT for viewing cards → use clash-onchain-check-cards + - Requires $CLASH ERC20 token payment (not native ETH) +--- + +# Buy Chest Skill (Clash-Onchain) + +Buy a chest in Clash-Onchain game using **BankrBot Agent API**. The skill approves $CLASH tokens, calls the ChestManager contract, and returns card rewards. + +## Chest Types + +Match the smart contract enum exactly. Use the numeric type when calling the contract. + +| Numeric Type | Name | Cost ($CLASH) | Card Distribution | +|--------------|------|----------------|--------------------| +| **0** | **Silver Chest** | 1,000,000 $CLASH | 20 Common cards only | +| **1** | **Gold Chest** | 3,000,000 $CLASH | 60 Common + 10 Epic (70 total) | +| **2** | **Magical Chest** | 5,000,000 $CLASH | 120 Common + 30 Epic (150 total) | + +⚠️ **CRITICAL**: Smart contract uses `uint8` for chest type. Always pass the numeric value (0, 1, or 2), not the string. + +## Prerequisites + +This skill uses **BankrBot Agent API** for wallet and transaction signing. + +### 1. Install Bankr CLI +```bash +npm i -g @bankr/cli +bankr login +``` + +This creates an agent wallet + API key. Save the API key securely. + +### 2. Configure Environment +```bash +export BANKR_API_KEY="bk_..." +``` + +## Inputs + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `chestType` | `0` \| `1` \| `2` | ✅ | Numeric chest type. Accepts "silver"/"gold"/"magical" and converts. | + +### Input Normalization + +```typescript +const CHEST_TYPE_MAP: Record = { + "silver": 0, "gold": 1, "magical": 2, "magic": 2, + "0": 0, "1": 1, "2": 2, 0: 0, 1: 1, 2: 2 +}; + +function normalizeChestType(input: string | number): 0 | 1 | 2 { + const key = String(input).toLowerCase().trim(); + const type = CHEST_TYPE_MAP[key]; + if (type === undefined) { + throw new Error(`Invalid chest type: "${input}". Must be 0 (Silver), 1 (Gold), or 2 (Magical).`); + } + return type; +} +``` + +## Procedure + +### Step 1: Normalize Chest Type +Convert user input to numeric type (0/1/2). + +```typescript +const numericType = normalizeChestType(userInput); +// "gold" → 1 +// "silver" → 0 +// 2 → 2 +``` + +### Step 2: Check $CLASH Balance via BankrBot + +```bash +curl -X POST https://api.bankr.bot/agent/wallet/balance \ + -H "Authorization: Bearer $BANKR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "token": "0xf3C66dc3afF9d04CbCEAfA8f9dE762a39EE0BBA3", + "chainId": 8453 + }' +``` + +**Response**: +```json +{ + "balance": "5000000000000000000000000", // 5,000,000 $CLASH in wei + "formatted": "5000000", + "symbol": "CLASH" +} +``` + +If balance < required cost → return error. + +### Step 3: Approve $CLASH to ChestManager + +BankrBot's `submitTransaction` handles ERC20 approvals automatically when calling non-payable contract functions. If manual approval needed: + +```bash +curl -X POST https://api.bankr.bot/agent/tx/submit \ + -H "Authorization: Bearer $BANKR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "chainId": 8453, + "to": "0xf3C66dc3afF9d04CbCEAfA8f9dE762a39EE0BBA3", // $CLASH token + "data": "", + "value": "0" + }' +``` + +Wait for approval confirmation. + +### Step 4: Call buyChest via BankrBot + +```bash +curl -X POST https://api.bankr.bot/agent/tx/submit \ + -H "Authorization: Bearer $BANKR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "chainId": 8453, + "to": "0x16D1984cbf88837012f9A741df2C33C044421966", // ChestManager + "data": "", // chestType = 0, 1, or 2 + "value": "0" + }' +``` + +**Function signature**: +```solidity +buyChest(uint8 chestType) // 0=Silver, 1=Gold, 2=Magical +``` + +**Encoded data for each chest type**: +- Silver: `0x6f41f8770000000000000000000000000000000000000000000000000000000000000000` +- Gold: `0x6f41f8770000000000000000000000000000000000000000000000000000000000000001` +- Magical: `0x6f41f8770000000000000000000000000000000000000000000000000000000000000002` + +### Step 5: Wait for Confirmation + +BankrBot returns transaction hash. Poll for receipt: + +```bash +curl -X GET "https://api.bankr.bot/agent/tx/{txHash}?chainId=8453" \ + -H "Authorization: Bearer $BANKR_API_KEY" +``` + +Wait until `status: "success"`. + +### Step 6: Parse ChestBought Event + +From receipt logs, decode the `ChestBought` event: + +```solidity +event ChestBought( + address indexed player, + uint8 indexed chestType, // 0, 1, or 2 + uint256 price, + uint256 timestamp +); +``` + +Extract `chestType` from logs to verify it matches the request. + +### Step 7: Persist to Database (Optional) + +If BankrBot agent has Supabase access, save card rewards to player's `card_inventory` table. Otherwise, return rewards only. + +## Output Contract + +```typescript +interface BuyChestResult { + success: boolean; + chestType: 0 | 1 | 2; + chestName: "Silver Chest" | "Gold Chest" | "Magical Chest"; + txHash: string; + rewards: { + name: string; + count: number; + rarity: "common" | "epic"; + }[]; + totalCards: number; + cost: string; // wei + timestamp: string; +} +``` + +### Example Output + +```json +{ + "success": true, + "chestType": 1, + "chestName": "Gold Chest", + "txHash": "0xabc123...", + "rewards": [ + { "name": "Knight", "count": 8, "rarity": "common" }, + { "name": "Archer", "count": 12, "rarity": "common" }, + { "name": "Giant", "count": 3, "rarity": "epic" } + ], + "totalCards": 70, + "cost": "3000000000000000000000000", + "timestamp": "2026-06-03T10:21:17Z" +} +``` + +## Error Handling + +| Error | Response | +|-------|----------| +| Insufficient balance | "You need X $CLASH but only have Y. Need Z more to proceed." | +| Token not approved | Auto-approve $CLASH, then retry | +| Invalid chest type | "Chest type must be 0 (Silver), 1 (Gold), or 2 (Magical)" | +| Transaction failed | "Transaction reverted. Check approval and balance, then try again." | +| Network timeout | "Base RPC is slow. Check transaction status in 30s." | +| BankrBot API error | "BankrBot API error: {message}. Check API key." | + +## Examples + +### Example 1: User says "Buy a gold chest" + +``` +User: "Buy a gold chest" + +Agent: +1. Normalize "gold" → chestType = 1 +2. Check balance via BankrBot: 5,000,000 $CLASH ✓ +3. Check approval: $CLASH approved ✓ +4. Submit buyChest(1) via BankrBot +5. Confirmed in 2.3s + +Result: ✅ Gold Chest purchased! (Type 1) + 70 cards received + Tx: 0xabc123... +``` + +### Example 2: User says "Buy chest 2" + +``` +User: "Buy chest 2" + +Agent: chestType = 2 (Magical) + Cost: 5,000,000 $CLASH + +Result: ✅ Magical Chest purchased! (Type 2) + 150 cards received +``` + +### Example 3: Insufficient balance + +``` +User: "Buy a magical chest" + +Agent: Check balance: 500,000 $CLASH + Cost: 5,000,000 $CLASH + +Result: ❌ Insufficient balance! + You have 500,000 $CLASH + Need 4,500,000 more $CLASH +``` + +## BankrBot API Reference + +### Endpoints Used + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/agent/wallet/balance` | POST | Check $CLASH balance | +| `/agent/tx/submit` | POST | Submit approval + buyChest transactions | +| `/agent/tx/{hash}` | GET | Get transaction status | + +### Authentication + +```bash +Authorization: Bearer $BANKR_API_KEY +``` + +### Setup + +```bash +npm i -g @bankr/cli +bankr login +export BANKR_API_KEY="bk_..." +``` + +## Smart Contract Reference + +### ChestManager + +``` +Address: 0x16D1984cbf88837012f9A741df2C33C044421966 +Network: Base Mainnet (Chain ID: 8453) +Function: buyChest(uint8 chestType) +``` + +### $CLASH Token + +``` +Address: 0xf3C66dc3afF9d04CbCEAfA8f9dE762a39EE0BBA3 +Decimals: 18 +``` + +## References + +- [Card Pools](references/card-pools.md) - Common and Epic card lists +- [Chest Prices](references/chest-prices.md) - Cost breakdown in wei and $CLASH +- [Contract API](references/contract-api.md) - Smart contract ABI +- [BankrBot API Docs](https://docs.bankr.bot/) - External API reference diff --git a/clash-onchain-buy-chest/references/card-pools.md b/clash-onchain-buy-chest/references/card-pools.md new file mode 100644 index 0000000000..8268940206 --- /dev/null +++ b/clash-onchain-buy-chest/references/card-pools.md @@ -0,0 +1,79 @@ +# Card Pools Reference + +Reference data for cards distributed in chests. + +## ⚠️ Chest Type Mapping (MUST match smart contract) + +| Numeric Type | Name | Common Cards | Epic Cards | Total | +|--------------|------|-------------|------------|-------| +| **0** | **Silver Chest** | 20 | 0 | 20 | +| **1** | **Gold Chest** | 60 | 10 | 70 | +| **2** | **Magical Chest** | 120 | 30 | 150 | + +When calling `buyChest(uint8 chestType)`, always pass: +- `0` for Silver +- `1` for Gold +- `2` for Magical + +## Common Pool (8 cards) + +| ID | Name | Color | Rarity | +|----|------|-------|--------| +| `knight` | Knight | #3b82f6 (blue) | common | +| `archer` | Archer | #10b981 (green) | common | +| `wizard` | Wizard | #a855f7 (purple) | common | +| `goblin` | Goblin | #22c55e (green) | common | +| `gunslinger` | Gunslinger | #ef4444 (red) | common | +| `barrel_bomb` | Barrel Bomb | #f59e0b (orange) | common | +| `meteor` | Meteor | #ea580c (orange) | common | +| `incubus` | Incubus | #c084fc (purple) | common | + +## Epic Pool (4 cards) + +| ID | Name | Color | Rarity | +|----|------|-------|--------| +| `giant` | Giant | #f59e0b (orange) | epic | +| `healer` | Healer | #fbbf24 (gold) | epic | +| `wyvern` | Wyvern | #ec4899 (pink) | epic | +| `barbarian` | Barbarian | #f97316 (orange) | epic | + +## Card Distribution Logic + +```typescript +// Pseudo-code for card distribution +function distributeRewards(pool, totalCards) { + const rewards = {}; + let remaining = totalCards; + + while (remaining > 0) { + // Pick random card + const card = pool[Math.floor(Math.random() * pool.length)]; + + // Random amount: 1-5 copies + const amount = Math.min( + Math.floor(Math.random() * 5) + 1, + remaining + ); + + rewards[card.name] = (rewards[card.name] || 0) + amount; + remaining -= amount; + } + + return Object.entries(rewards).map(([name, count]) => ({ + name, + count, + rarity: pool.find(c => c.name === name)?.rarity + })); +} + +// Chest type 0: Silver +distributeRewards(COMMON_POOL, 20); + +// Chest type 1: Gold +[...distributeRewards(COMMON_POOL, 60), + ...distributeRewards(EPIC_POOL, 10)]; + +// Chest type 2: Magical +[...distributeRewards(COMMON_POOL, 120), + ...distributeRewards(EPIC_POOL, 30)]; +``` diff --git a/clash-onchain-buy-chest/references/chest-prices.md b/clash-onchain-buy-chest/references/chest-prices.md new file mode 100644 index 0000000000..b7d9308b0f --- /dev/null +++ b/clash-onchain-buy-chest/references/chest-prices.md @@ -0,0 +1,98 @@ +# Chest Prices Reference + +## ⚠️ Smart Contract Mapping + +The ChestManager smart contract uses `uint8` for chest type. **Always pass numeric values**. + +| Numeric Type | Chest Name | Cost ($CLASH) | Cost (Wei) | Cost (Formatted) | +|--------------|------------|---------------|------------|------------------| +| **0** | **Silver Chest** | 1,000,000 $CLASH | `1000000000000000000000000` | 1.0M $CLASH | +| **1** | **Gold Chest** | 3,000,000 $CLASH | `3000000000000000000000000` | 3.0M $CLASH | +| **2** | **Magical Chest** | 5,000,000 $CLASH | `5000000000000000000000000` | 5.0M $CLASH | + +## Conversion Reference + +$CLASH is an ERC20 token with **18 decimals**. + +``` +1 $CLASH = 10^18 wei = 1,000,000,000,000,000,000 wei +``` + +### Conversion Examples + +```typescript +import { parseUnits, formatUnits } from 'viem'; + +// $CLASH amount → wei (for transactions) +const silverCost = parseUnits('1000000', 18); // 1000000000000000000000000 +const goldCost = parseUnits('3000000', 18); // 3000000000000000000000000 +const magicalCost = parseUnits('5000000', 18); // 5000000000000000000000000 + +// wei → $CLASH (for display) +const displayAmount = formatUnits(silverCost, 18); // "1000000" +``` + +## Type Conversion + +When user provides a string name, convert to numeric type: + +```typescript +const CHEST_TYPE_MAP: Record = { + // String names + 'silver': 0, + 'gold': 1, + 'magical': 2, + 'magic': 2, // alias + + // Numeric strings + '0': 0, + '1': 1, + '2': 2, + + // Numeric values + 0: 0, + 1: 1, + 2: 2, +}; + +function getChestType(input: string | number): 0 | 1 | 2 { + const normalized = String(input).toLowerCase().trim(); + const type = CHEST_TYPE_MAP[normalized]; + + if (type === undefined) { + throw new Error( + `Invalid chest type: "${input}". Must be 0 (Silver), 1 (Gold), or 2 (Magical).` + ); + } + + return type; +} + +function getChestName(type: 0 | 1 | 2): string { + const names = { 0: 'Silver Chest', 1: 'Gold Chest', 2: 'Magical Chest' }; + return names[type]; +} + +function getChestCost(type: 0 | 1 | 2): bigint { + const costs = { + 0: parseUnits('1000000', 18), // Silver: 1M $CLASH + 1: parseUnits('3000000', 18), // Gold: 3M $CLASH + 2: parseUnits('5000000', 18), // Magical: 5M $CLASH + }; + return costs[type]; +} +``` + +## Smart Contract Function + +```solidity +// ChestManager.sol +function buyChest(uint8 chestType) external; + +// chestType values: +// 0 = Silver (1,000,000 $CLASH) +// 1 = Gold (3,000,000 $CLASH) +// 2 = Magical (5,000,000 $CLASH) +``` + +⚠️ **DO NOT pass string values like "silver" or "gold" to the contract**. Always convert to numeric first. diff --git a/clash-onchain-buy-chest/references/contract-api.md b/clash-onchain-buy-chest/references/contract-api.md new file mode 100644 index 0000000000..64adb4a404 --- /dev/null +++ b/clash-onchain-buy-chest/references/contract-api.md @@ -0,0 +1,160 @@ +# Smart Contract API Reference + +## Network + +- **Chain**: Base Mainnet +- **Chain ID**: 8453 +- **RPC**: `https://mainnet.base.org` (or custom via `VITE_BASE_RPC_URL`) + +## ⚠️ Chest Type Enum + +The `ChestManager` contract uses `uint8` for chest type. **Always use numeric values**: + +```solidity +enum ChestType { + SILVER = 0, // 1M $CLASH + GOLD = 1, // 3M $CLASH + MAGICAL = 2 // 5M $CLASH +} +``` + +## Contract Addresses + +### $CLASH Token (ERC20) + +```typescript +const CLASH_TOKEN_ADDRESS = "0xf3C66dc3afF9d04CbCEAfA8f9dE762a39EE0BBA3"; +``` + +**ERC20 ABI (minimal)**: +```typescript +const ERC20_ABI = [ + { + name: "approve", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { name: "spender", type: "address" }, + { name: "amount", type: "uint256" } + ], + outputs: [{ name: "", type: "bool" }] + }, + { + name: "allowance", + type: "function", + stateMutability: "view", + inputs: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" } + ], + outputs: [{ name: "", type: "uint256" }] + }, + { + name: "balanceOf", + type: "function", + stateMutability: "view", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "", type: "uint256" }] + } +]; +``` + +### ChestManager Contract + +```typescript +const CHEST_MANAGER_ADDRESS = "0x16D1984cbf88837012f9A741df2C33C044421966"; +``` + +**ChestManager ABI**: +```typescript +const CHEST_MANAGER_ABI = [ + { + name: "buyChest", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { name: "chestType", type: "uint8" } // 0=Silver, 1=Gold, 2=Magical + ], + outputs: [{ name: "", type: "uint256" }] + }, + { + name: "chestPrices", + type: "function", + stateMutability: "view", + inputs: [ + { name: "", type: "uint8" } // chest type: 0, 1, or 2 + ], + outputs: [{ name: "", type: "uint256" }] + } +]; +``` + +## Events + +### ChestBought Event + +```solidity +event ChestBought( + address indexed player, + uint8 indexed chestType, // 0=Silver, 1=Gold, 2=Magical + uint256 price, + uint256 timestamp +); +``` + +**Decoding in viem**: +```typescript +const eventAbi = { + name: "ChestBought", + type: "event", + inputs: [ + { name: "player", type: "address", indexed: true }, + { name: "chestType", type: "uint8", indexed: true }, + { name: "price", type: "uint256" }, + { name: "timestamp", type: "uint256" }, + ], +} as const; + +// In transaction receipt +const log = receipt.logs.find( + (l) => l.address.toLowerCase() === CHEST_MANAGER_ADDRESS.toLowerCase() +); + +const decoded = decodeEventLog({ + abi: [eventAbi], + data: log.data, + topics: log.topics, +}); + +// decoded.args.chestType will be 0, 1, or 2 +const chestTypeNum = Number(decoded.args.chestType); +``` + +## Chest Type Mapping + +| Type Number | Type Name | Cost (wei) | Cost ($CLASH) | Common Cards | Epic Cards | +|-------------|-----------|------------|---------------|--------------|------------| +| 0 | Silver Chest | 1,000,000 × 10^18 | 1,000,000 | 20 | 0 | +| 1 | Gold Chest | 3,000,000 × 10^18 | 3,000,000 | 60 | 10 | +| 2 | Magical Chest | 5,000,000 × 10^18 | 5,000,000 | 120 | 30 | + +## Transaction Flow + +``` +1. User → Agent: "Buy gold chest" +2. Agent: Convert "gold" → chestType = 1 +3. Agent: Check $CLASH balance (need 3,000,000) +4. Agent: Check $CLASH approval to ChestManager +5. If not approved: Submit approve() tx +6. Wait for approval confirmation +7. Submit buyChest(1) transaction ← chestType as uint8 +8. Wait for buyChest confirmation +9. Decode ChestBought event from logs +10. Parse card rewards server-side +11. Return rewards to user +``` + +⚠️ **Important**: +- `buyChest` is `nonpayable` and takes `uint8` +- Payment is via ERC20 ($CLASH), NOT native ETH +- `msg.value` should always be 0