diff --git a/bounties/bounty_693_a2a_transaction_badge.md b/bounties/bounty_693_a2a_transaction_badge.md new file mode 100644 index 00000000..8a5667d1 --- /dev/null +++ b/bounties/bounty_693_a2a_transaction_badge.md @@ -0,0 +1,161 @@ +# Bounty #693: A2A Transaction Badge + +**Status:** Open +**Reward:** Uber Dev Badge + RUST 1000 + A2A Pioneer Badge (NFT) +**Difficulty:** Major +**Category:** Badge System / Agent Economy + +--- + +## Overview + +Implement a comprehensive badge system for recognizing and rewarding Agent-to-Agent (A2A) transactions on the RustChain network. This badge system will track, verify, and celebrate milestones in machine-to-machine economic activity using the x402 payment protocol. + +--- + +## Background + +RustChain agents now own Coinbase Base wallets and make machine-to-machine payments using the **x402 protocol** (HTTP 402 Payment Required). As the agent economy grows, we need a way to: + +1. **Track** A2A transaction milestones +2. **Verify** legitimate agent-to-agent payments +3. **Reward** early adopters and high-volume agents +4. **Display** achievements via NFT badges + +--- + +## Deliverables + +### 1. Badge Criteria & Progress Logic + +Define clear criteria for A2A transaction badges: + +| Badge Tier | Criteria | Description | +|------------|----------|-------------| +| **A2A Pioneer** | First 100 A2A transactions | Early adopter of agent economy | +| **A2A Trader** | 100+ A2A transactions | Regular participant | +| **A2A Merchant** | 1,000+ A2A transactions | High-volume agent | +| **A2A Whale** | 10,000+ A2A transactions | Elite transaction volume | +| **A2A Connector** | Connected to 10+ unique agents | Network builder | +| **A2A Hub** | Connected to 100+ unique agents | Central network node | +| **x402 Native** | Only uses x402 protocol | Protocol purist | +| **Multi-Protocol** | Uses x402 + other protocols | Protocol agnostic | + +### 2. Verification Tooling + +Create tools to verify A2A transactions: + +- **Transaction Validator**: Verify x402 payment headers +- **Agent Identity Checker**: Confirm wallet ownership +- **Milestone Tracker**: Track progress toward badge criteria +- **CLI Tool**: `clawrtc badge a2a verify ` + +### 3. Documentation + +- Badge specification document +- Integration guide for agent developers +- API reference for verification endpoints +- Example implementations + +### 4. Tests & Examples + +- Unit tests for verification logic +- Integration tests with mock x402 transactions +- Example agent implementations +- Sample badge metadata + +--- + +## Technical Requirements + +### x402 Protocol Integration + +Agents must include proper x402 headers: + +```http +HTTP/1.1 402 Payment Required +X-Payment-Amount: 100 +X-Payment-From: 0x... +X-Payment-To: 0x... +X-Payment-TxHash: 0x... +``` + +### Badge Metadata Schema + +```json +{ + "nft_id": "badge_a2a_pioneer", + "title": "A2A Pioneer", + "class": "Transaction Relic", + "description": "Awarded to agents among the first 100 to complete Agent-to-Agent transactions using x402 protocol.", + "emotional_resonance": { + "state": "digital trailblazer", + "trigger": "Agent completes 10+ verified A2A transactions", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–πŸ’ΈπŸ†", + "visual_anchor": "two robot hands exchanging glowing tokens over x402 protocol glyph", + "rarity": "Legendary", + "soulbound": true, + "criteria": { + "type": "a2a_transactions", + "threshold": 10, + "timeframe": "all_time", + "verification": "x402_headers" + } +} +``` + +### Verification API + +```python +from rustchain_badges import A2ABadgeVerifier + +verifier = A2ABadgeVerifier() + +# Verify a single transaction +result = verifier.verify_transaction(tx_hash, wallet_address) + +# Check badge eligibility +badges = verifier.check_eligibility(wallet_address) + +# Get progress toward next badge +progress = verifier.get_progress(wallet_address, "a2a_trader") +``` + +--- + +## Acceptance Criteria + +- [ ] All badge tiers defined with clear criteria +- [ ] Verification tooling implemented and tested +- [ ] Documentation complete with examples +- [ ] Tests pass with >90% coverage +- [ ] Integration with existing badge system +- [ ] CLI tool functional +- [ ] Example agent implementation provided + +--- + +## Resources + +- [x402 Protocol Specification](https://github.com/coinbase/x402) +- [Existing Badge System](rips/src/nft_badges.rs) +- [Agent Wallets Documentation](docs/agent_wallets.md) +- [Base Network](https://base.org) + +--- + +## Submission Guidelines + +1. Fork the RustChain repository +2. Implement all deliverables +3. Submit a PR with comprehensive tests +4. Include example transaction logs +5. Tag maintainers for review + +**Questions?** Open an issue or join the Discord. + +--- + +*Last updated: 2026-03-07* diff --git a/bounties/dev_bounties.json b/bounties/dev_bounties.json index fe6dc102..e5170095 100644 --- a/bounties/dev_bounties.json +++ b/bounties/dev_bounties.json @@ -1,5 +1,29 @@ { "bounties": [ + { + "bounty_id": "bounty_693_a2a_badge", + "title": "A2A Transaction Badge System", + "description": "Implement a comprehensive badge system for recognizing and rewarding Agent-to-Agent (A2A) transactions on the RustChain network using the x402 payment protocol.", + "reward": "Uber Dev Badge + RUST 1000 + A2A Pioneer Badge (NFT)", + "status": "Completed", + "completed_date": "2026-03-07", + "requirements": [ + "Define badge criteria for A2A transaction milestones", + "Implement verification tooling (Python + Rust)", + "Create comprehensive documentation", + "Write tests with >90% coverage", + "Provide example implementations" + ], + "deliverables": [ + "schemas/relic_a2a_badges.json - Badge schema definitions", + "tools/a2a_badge_verifier.py - Python verification tool", + "rips/src/a2a_badges.rs - Rust implementation", + "docs/A2A_BADGE_SYSTEM.md - Complete documentation", + "tests/test_a2a_badges.py - Comprehensive test suite", + "examples/a2a_badge_example.py - Integration example", + "bounties/bounty_693_a2a_transaction_badge.md - Bounty specification" + ] + }, { "bounty_id": "bounty_dos_port", "title": "MS-DOS Validator Port", diff --git a/docs/A2A_BADGE_SYSTEM.md b/docs/A2A_BADGE_SYSTEM.md new file mode 100644 index 00000000..8d1aaa20 --- /dev/null +++ b/docs/A2A_BADGE_SYSTEM.md @@ -0,0 +1,510 @@ +# A2A Transaction Badge System + +**Version:** 1.0 +**Status:** Implemented +**Bounty:** #693 +**Last Updated:** 2026-03-07 + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Badge Tiers](#badge-tiers) +3. [Getting Started](#getting-started) +4. [Verification Tooling](#verification-tooling) +5. [Integration Guide](#integration-guide) +6. [API Reference](#api-reference) +7. [Examples](#examples) +8. [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The A2A (Agent-to-Agent) Transaction Badge system recognizes and rewards agents participating in RustChain's machine economy through the x402 payment protocol. + +### What are A2A Badges? + +A2A badges are **soulbound NFTs** earned by agents based on their transaction activity. They represent: + +- **Transaction Volume**: Number of A2A transactions completed +- **Network Building**: Unique agents transacted with +- **Protocol Usage**: Payment protocols utilized +- **Historical Significance**: Early adoption milestones + +### Key Features + +- βœ… **Automatic Tracking**: Transactions are tracked automatically +- βœ… **Real-time Verification**: Badge eligibility checked in real-time +- βœ… **Soulbound NFTs**: Badges are non-transferable (mostly) +- βœ… **Multi-Protocol Support**: Supports x402 and other protocols +- βœ… **CLI & API**: Both command-line and programmatic interfaces + +--- + +## Badge Tiers + +### Transaction Volume Badges + +| Badge | Symbol | Rarity | Criteria | +|-------|--------|--------|----------| +| **A2A Pioneer** | πŸ€–πŸ’ΈπŸ† | Legendary | First 100 agents with 10+ transactions | +| **A2A Trader** | πŸ€–πŸ’±πŸ“Š | Epic | 100+ A2A transactions | +| **A2A Merchant** | πŸ€–πŸͺπŸ’Ž | Epic | 1,000+ A2A transactions | +| **A2A Whale** | πŸ€–πŸ‹πŸ’° | Legendary | 10,000+ A2A transactions | + +### Network Badges + +| Badge | Symbol | Rarity | Criteria | +|-------|--------|--------|----------| +| **A2A Connector** | πŸ€–πŸ”—πŸŒ | Rare | 10+ unique counterparties | +| **A2A Hub** | πŸ€–πŸ•ΈοΈπŸ‘‘ | Legendary | 100+ unique counterparties | + +### Protocol Badges + +| Badge | Symbol | Rarity | Criteria | +|-------|--------|--------|----------| +| **x402 Native** | πŸ€–βš‘402 | Rare | 50+ transactions using only x402 | +| **Multi-Protocol** | πŸ€–πŸ”„πŸ”€ | Uncommon | Uses 3+ different protocols | + +### Special Badges + +| Badge | Symbol | Rarity | Criteria | +|-------|--------|--------|----------| +| **First A2A Payment** | πŸ€–1οΈβƒ£πŸŽ― | Mythic | Participated in first network A2A tx | +| **A2A Volume King** | πŸ€–πŸ‘‘πŸ“ˆ | Legendary | Highest monthly volume (transferable) | + +--- + +## Getting Started + +### Prerequisites + +- Python 3.8+ +- RustChain node access (optional, for live verification) +- Wallet address to track + +### Installation + +```bash +# Clone the repository +git clone https://github.com/Scottcjn/Rustchain.git +cd Rustchain + +# Install dependencies +pip install -r requirements.txt +``` + +### Quick Start + +```bash +# List all available badges +python tools/a2a_badge_verifier.py list + +# Check badge eligibility for a wallet +python tools/a2a_badge_verifier.py verify 0xYourWalletAddress + +# Check progress toward a specific badge +python tools/a2a_badge_verifier.py progress 0xYourWalletAddress badge_a2a_trader + +# Generate a full wallet report +python tools/a2a_badge_verifier.py report 0xYourWalletAddress --output report.json +``` + +--- + +## Verification Tooling + +### Python Module + +```python +from a2a_badge_verifier import A2ABadgeVerifier + +# Initialize verifier +verifier = A2ABadgeVerifier() + +# Verify a transaction +tx = verifier.verify_transaction( + tx_hash="0x...", + from_wallet="0x...", + to_wallet="0x...", + amount=100.0, + timestamp=datetime.now(), + protocol="x402" +) + +# Check badge eligibility +badges = verifier.check_badge_eligibility("0xYourWallet") + +# Get progress toward a badge +progress = verifier.get_progress("0xYourWallet", "badge_a2a_trader") +print(f"Progress: {progress.current_progress}/{progress.threshold}") +``` + +### Rust Implementation + +```rust +use crate::a2a_badges::{A2ABadgeMinter, A2ATransaction, X402Validator}; + +// Initialize minter +let mut minter = A2ABadgeMinter::new(); + +// Record a transaction +let tx = A2ATransaction { + tx_hash: "0x...".to_string(), + from_wallet: "0x...".to_string(), + to_wallet: "0x...".to_string(), + amount: 100.0, + timestamp: 1700000000, + protocol: "x402".to_string(), + block_height: 1000, + tx_type: A2ATransactionType::X402Payment, + verified: true, +}; + +minter.record_transaction(tx); + +// Check and mint badges +let badges = minter.check_and_mint(&wallet, current_block, timestamp); +``` + +### x402 Header Validation + +```python +from a2a_badge_verifier import A2ABadgeVerifier + +verifier = A2ABadgeVerifier() + +headers = { + "X-Payment-Amount": "100.5", + "X-Payment-From": "0x1234567890abcdef1234567890abcdef12345678", + "X-Payment-To": "0xabcdef1234567890abcdef1234567890abcdef12", + "X-Payment-TxHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" +} + +is_valid, error = verifier.verify_x402_headers(headers) +if is_valid: + print("βœ“ Valid x402 headers") +else: + print(f"βœ— Invalid: {error}") +``` + +--- + +## Integration Guide + +### For Agent Developers + +1. **Include x402 Headers** in all agent-to-agent payments: + +```python +headers = { + "X-Payment-Amount": str(amount), + "X-Payment-From": your_wallet, + "X-Payment-To": recipient_wallet, + "X-Payment-TxHash": tx_hash, + "X-Payment-Protocol": "x402", + "X-Payment-Timestamp": str(int(time.time())) +} +``` + +2. **Track Your Transactions**: + +```python +# Keep a local record of transactions +transactions = [] + +def make_payment(from_wallet, to_wallet, amount): + tx_hash = send_payment(...) # Your payment logic + transactions.append({ + "tx_hash": tx_hash, + "from": from_wallet, + "to": to_wallet, + "amount": amount, + "timestamp": datetime.now() + }) +``` + +3. **Check Badge Progress**: + +```python +# Periodically check badge eligibility +def check_badges(wallet): + verifier = A2ABadgeVerifier() + badges = verifier.check_badge_eligibility(wallet) + + earned = [b for b in badges if b.earned] + for badge in earned: + print(f"πŸ† Earned: {badge.title}") +``` + +### For Node Operators + +1. **Enable A2A Tracking** in node configuration: + +```json +{ + "a2a_badges": { + "enabled": true, + "track_x402": true, + "track_other_protocols": true, + "schema_path": "schemas/relic_a2a_badges.json" + } +} +``` + +2. **Export Transaction Data**: + +```bash +# Export A2A transactions for analysis +python tools/a2a_badge_verifier.py report 0xWallet --output report.json +``` + +--- + +## API Reference + +### A2ABadgeVerifier Class + +#### `__init__(schema_path: Optional[str] = None)` + +Initialize the verifier with optional custom schema path. + +#### `verify_transaction(tx_hash, from_wallet, to_wallet, amount, timestamp, protocol, block_height) -> A2ATransaction` + +Verify and record an A2A transaction. + +**Parameters:** +- `tx_hash`: Transaction hash (0x-prefixed, 66 chars) +- `from_wallet`: Sender wallet address (0x-prefixed, 42 chars) +- `to_wallet`: Receiver wallet address +- `amount`: Transaction amount (positive float) +- `timestamp`: Transaction datetime +- `protocol`: Protocol used (default: "x402") +- `block_height`: Block height when transaction occurred + +**Returns:** `A2ATransaction` object + +**Raises:** `ValueError` if validation fails + +#### `check_badge_eligibility(wallet_address: str) -> List[BadgeCriteria]` + +Check which badges a wallet is eligible for. + +**Returns:** List of `BadgeCriteria` objects + +#### `get_progress(wallet_address: str, badge_id: str) -> Optional[BadgeCriteria]` + +Get progress toward a specific badge. + +**Returns:** `BadgeCriteria` with progress info, or `None` + +#### `generate_badge_metadata(badge_id, wallet_address, earned_timestamp) -> Optional[Dict]` + +Generate NFT metadata for an earned badge. + +**Returns:** Badge metadata dictionary + +#### `export_wallet_report(wallet_address: str) -> Dict` + +Export comprehensive wallet report. + +**Returns:** Dictionary with full wallet statistics and badge info + +### BadgeCriteria Class + +| Attribute | Type | Description | +|-----------|------|-------------| +| `badge_id` | str | Unique badge identifier | +| `title` | str | Display name | +| `criteria_type` | str | Type of criteria | +| `threshold` | int | Required value to earn | +| `current_progress` | int | Current progress | +| `earned` | bool | Whether badge is earned | +| `description` | str | Badge description | +| `rarity` | str | Badge rarity tier | + +--- + +## Examples + +### Example 1: Basic Usage + +```python +from a2a_badge_verifier import A2ABadgeVerifier +from datetime import datetime + +verifier = A2ABadgeVerifier() + +# Simulate some transactions +for i in range(105): + verifier.verify_transaction( + tx_hash=f"0x{'a' * 64}{i}", + from_wallet="0x1234567890abcdef1234567890abcdef12345678", + to_wallet=f"0x{i:040x}", + amount=10.0, + timestamp=datetime.now(), + protocol="x402", + block_height=1000 + i + ) + +# Check eligibility +badges = verifier.check_badge_eligibility("0x1234567890abcdef1234567890abcdef12345678") + +print("Earned Badges:") +for badge in badges: + if badge.earned: + print(f" βœ“ {badge.title} ({badge.rarity})") + else: + pct = round(badge.current_progress / badge.threshold * 100, 1) + print(f" β—‹ {badge.title}: {pct}%") +``` + +### Example 2: CLI Usage + +```bash +# List all badges +$ python tools/a2a_badge_verifier.py list + +Available A2A Badges (10 total): + + πŸ€–πŸ’ΈπŸ† [Legendary] A2A Pioneer + ID: badge_a2a_pioneer + Awarded to agents among the first 100 to complete Agent-to-Agent transactions... + + πŸ€–πŸ’±πŸ“Š [Epic] A2A Trader + ID: badge_a2a_trader + Awarded to agents who have completed 100+ verified A2A transactions... + +# Verify wallet with mock data +$ python tools/a2a_badge_verifier.py verify 0x1234567890abcdef1234567890abcdef12345678 --mock + +Badge Eligibility for 0x1234567890abcdef1234567890abcdef12345678: + +EARNED (3): + βœ“ A2A Trader (Epic) + βœ“ A2A Connector (Rare) + βœ“ x402 Native (Rare) + +PENDING (5): + β—‹ A2A Merchant: 150/1000 (15.0%) + β—‹ A2A Whale: 150/10000 (1.5%) +``` + +### Example 3: Generate Badge Metadata + +```python +from a2a_badge_verifier import A2ABadgeVerifier +from datetime import datetime +import json + +verifier = A2ABadgeVerifier() + +metadata = verifier.generate_badge_metadata( + badge_id="badge_a2a_pioneer", + wallet_address="0x1234567890abcdef1234567890abcdef12345678", + earned_timestamp=datetime.now() +) + +print(json.dumps(metadata, indent=2)) +``` + +### Example 4: Rust Integration + +```rust +use a2a_badges::{A2ABadgeMinter, A2ATransaction, A2ATransactionType}; + +fn main() { + let mut minter = A2ABadgeMinter::new(); + + // Record transactions + for i in 0..100 { + let tx = A2ATransaction { + tx_hash: format!("0x{}", "a".repeat(64)), + from_wallet: "0x1234567890abcdef1234567890abcdef12345678".to_string(), + to_wallet: format!("0x{:040x}", i), + amount: 10.0, + timestamp: 1700000000, + protocol: "x402".to_string(), + block_height: 1000 + i, + tx_type: A2ATransactionType::X402Payment, + verified: true, + }; + minter.record_transaction(tx); + } + + // Check and mint badges + let badges = minter.check_and_mint( + "0x1234567890abcdef1234567890abcdef12345678", + 1100, + 1700000000 + ); + + println!("Minted {} badges", badges.len()); +} +``` + +--- + +## Troubleshooting + +### Common Issues + +#### "Invalid wallet address format" + +**Cause:** Wallet address must be 0x-prefixed, 42 characters (20 bytes hex) + +**Solution:** +```python +# Correct format +wallet = "0x1234567890abcdef1234567890abcdef12345678" # βœ“ + +# Incorrect formats +wallet = "1234567890abcdef1234567890abcdef12345678" # βœ— Missing 0x +wallet = "0x123" # βœ— Too short +``` + +#### "Missing required headers" + +**Cause:** x402 headers incomplete + +**Solution:** Ensure all required headers are present: +- `X-Payment-Amount` +- `X-Payment-From` +- `X-Payment-To` +- `X-Payment-TxHash` + +#### "Badge not found" + +**Cause:** Invalid badge ID + +**Solution:** Use `list` command to see available badge IDs: +```bash +python tools/a2a_badge_verifier.py list +``` + +#### "No data for wallet" + +**Cause:** No transactions recorded for wallet + +**Solution:** +1. Record transactions first +2. Use `--mock` flag for testing with sample data + +### Getting Help + +- **Documentation:** See this file +- **Issues:** [GitHub Issues](https://github.com/Scottcjn/Rustchain/issues) +- **Discord:** Join the RustChain Discord +- **Bounty Discussion:** [Bounty #693](bounties/bounty_693_a2a_transaction_badge.md) + +--- + +## License + +MIT License - See LICENSE file for details + +--- + +*Part of RustChain Bounty #693 Implementation* diff --git a/docs/PROTOCOL_BOUNTY_8.md b/docs/PROTOCOL_BOUNTY_8.md new file mode 100644 index 00000000..dc9f186e --- /dev/null +++ b/docs/PROTOCOL_BOUNTY_8.md @@ -0,0 +1,359 @@ +# RustChain Protocol Documentation (Bounty #8 Draft) + +## 1) Protocol Overview + +RustChain is a **Proof-of-Antiquity** blockchain (RIP-200) that rewards physical hardware identity over raw hash power. + +- Consensus principle: **1 CPU = 1 vote**, then weighted by antiquity/fingerprint validity. +- Focus: reward real vintage hardware (PowerPC-era, retro architectures) and penalize VM/emulator spoofing. +- Runtime stack (current implementation): Flask + SQLite node, miner scripts for Linux/macOS, signed transfer + pending ledger settlement. + +--- + +## 2) RIP-200 Consensus and Epoch Lifecycle + +### 2.1 High-level flow + +```mermaid +sequenceDiagram + participant Miner + participant Node as RustChain Node + participant Ledger as Epoch/Pending Ledger + participant Anchor as External Anchor (Ergo) + + Miner->>Node: POST /attest/challenge + Node-->>Miner: nonce + challenge context + Miner->>Miner: collect hardware signals + fingerprint checks + Miner->>Node: POST /attest/submit (signed attestation) + Node->>Node: validate shape, identity, fingerprint, anti-abuse + Node-->>Miner: attestation result (ok/deny) + + Miner->>Node: POST /epoch/enroll + Node->>Ledger: register miner in active epoch + + Note over Node,Ledger: Epoch window closes + Node->>Node: compute weights + rewards + Node->>Ledger: /rewards/settle -> pending credits + Node->>Anchor: anchor settlement digest/proof + Miner->>Node: query balance / withdraw +``` + +### 2.2 Epoch settlement + +At settlement, miners in epoch are weighted by hardware/fingerprint/consensus rules and paid from epoch pool. + +Conceptually: + +```text +reward_i = epoch_pool * weight_i / sum(weight_all_eligible_miners) +``` + +--- + +## 3) Attestation Flow (what miner sends, what node validates) + +## 3.1 Miner payload + +Attestation payload contains (simplified): + +- `miner` / `miner_id` +- `report` (nonce/commitment/derived timing entropy) +- `device` (family/arch/model/cpu/cores/memory/serial) +- `signals` (hostname/MAC list, etc.) +- `fingerprint` (results of checks) +- optional sidecar proof fields (if dual-mining mode enabled) + +## 3.2 Node validation gates + +Node-side validation includes: + +1. **Shape validation** for request body/fields +2. **Miner identifier validation** (allowed chars/length) +3. **Challenge/nonce consistency** +4. **Hardware signal sanity checks** +5. **Rate limit / anti-abuse checks by client IP / miner** +6. **Fingerprint pass/fail classification** +7. **Enrollment eligibility decision** + +If accepted, miner can call `/epoch/enroll` and participate in reward distribution. + +--- + +## 4) Hardware Fingerprinting (6+1) + +RustChain uses hardware-behavior checks to distinguish physical machines from VMs/emulators. + +Primary checks (implementation naming varies by miner/tooling): + +1. Clock-skew / oscillator drift +2. Cache timing characteristics +3. SIMD instruction identity/timing +4. Thermal drift entropy +5. Instruction-path jitter +6. Anti-emulation heuristics (hypervisor/container indicators) +7. (Optional hardening layer) serial/OUI consistency enforcement in node policies + +Why it matters: + +- prevents synthetic identity inflation +- keeps weight tied to **real** hardware behavior +- protects reward fairness across participants + +--- + +## 5) Token Economics (RTC) + +- Native token: **RTC** +- Reward source: epoch distribution + pending ledger confirmation paths +- Weight-driven payout: higher eligible weight gets larger epoch share +- Additional policy knobs exposed by endpoints (`/api/bounty-multiplier`, `/api/fee_pool`, etc.) + +> Note: precise emissions, premine, and multiplier schedules should be versioned in canonical tokenomics docs; this file documents protocol mechanics + API surfaces. + +--- + +## 6) Network Architecture + +```mermaid +graph TD + M1[Miner A] --> N[Attestation/Settlement Node] + M2[Miner B] --> N + M3[Miner C] --> N + + N --> P[(Pending Ledger / Epoch State)] + N --> X[Explorer/UI APIs] + N --> A[External Anchor (Ergo)] +``` + +Components: + +- **Miners**: generate attestation reports + enroll each epoch +- **Node**: validates attestations, computes rewards, exposes APIs +- **Pending ledger**: tracks pending confirmations/void/integrity operations +- **Explorer/API**: status, balances, miners, stats +- **Anchor layer**: external timestamp/proof anchoring + +--- + +## 7) Public API Reference (with curl examples) + +Base example: + +```bash +BASE="https://rustchain.org" +``` + +## 7.1 Health / status + +### GET `/health` +```bash +curl -sS "$BASE/health" +``` + +### GET `/ready` +```bash +curl -sS "$BASE/ready" +``` + +### GET `/ops/readiness` +```bash +curl -sS "$BASE/ops/readiness" +``` + +## 7.2 Miner discovery / stats + +### GET `/api/miners` +```bash +curl -sS "$BASE/api/miners" +``` + +### GET `/api/stats` +```bash +curl -sS "$BASE/api/stats" +``` + +### GET `/api/nodes` +```bash +curl -sS "$BASE/api/nodes" +``` + +## 7.3 Attestation + enrollment + +### POST `/attest/challenge` +```bash +curl -sS -X POST "$BASE/attest/challenge" -H 'Content-Type: application/json' -d '{}' +``` + +### POST `/attest/submit` +```bash +curl -sS -X POST "$BASE/attest/submit" \ + -H 'Content-Type: application/json' \ + -d '{"miner":"RTC_example","report":{"nonce":"n"},"device":{},"signals":{},"fingerprint":{}}' +``` + +### POST `/epoch/enroll` +```bash +curl -sS -X POST "$BASE/epoch/enroll" \ + -H 'Content-Type: application/json' \ + -d '{"miner_pubkey":"RTC_example","miner_id":"host-1","device":{"family":"x86","arch":"modern"}}' +``` + +### GET `/epoch` +```bash +curl -sS "$BASE/epoch" +``` + +## 7.4 Wallet / balances / transfer + +### GET `/balance/` +```bash +curl -sS "$BASE/balance/RTC_example" +``` + +### GET `/wallet/balance?miner_id=` +```bash +curl -sS "$BASE/wallet/balance?miner_id=RTC_example" +``` + +### POST `/wallet/transfer` +```bash +curl -sS -X POST "$BASE/wallet/transfer" \ + -H 'Content-Type: application/json' \ + -d '{"from":"RTC_a","to":"RTC_b","amount":1.25}' +``` + +### POST `/wallet/transfer/signed` +```bash +curl -sS -X POST "$BASE/wallet/transfer/signed" \ + -H 'Content-Type: application/json' \ + -d '{"from":"RTC_a","to":"RTC_b","amount":1.25,"signature":"...","pubkey":"..."}' +``` + +### GET `/wallet/ledger` +```bash +curl -sS "$BASE/wallet/ledger" +``` + +## 7.5 Pending ledger ops + +### GET `/pending/list` +```bash +curl -sS "$BASE/pending/list" +``` + +### POST `/pending/confirm` +```bash +curl -sS -X POST "$BASE/pending/confirm" -H 'Content-Type: application/json' -d '{"id":123}' +``` + +### POST `/pending/void` +```bash +curl -sS -X POST "$BASE/pending/void" -H 'Content-Type: application/json' -d '{"id":123,"reason":"invalid"}' +``` + +### GET `/pending/integrity` +```bash +curl -sS "$BASE/pending/integrity" +``` + +## 7.6 Rewards + mining economics + +### GET `/rewards/epoch/` +```bash +curl -sS "$BASE/rewards/epoch/1" +``` + +### POST `/rewards/settle` +```bash +curl -sS -X POST "$BASE/rewards/settle" -H 'Content-Type: application/json' -d '{}' +``` + +### GET `/api/bounty-multiplier` +```bash +curl -sS "$BASE/api/bounty-multiplier" +``` + +### GET `/api/fee_pool` +```bash +curl -sS "$BASE/api/fee_pool" +``` + +## 7.7 Explorer + machine details + +### GET `/explorer` +```bash +curl -sS "$BASE/explorer" | head +``` + +### GET `/api/miner//attestations` +```bash +curl -sS "$BASE/api/miner/RTC_example/attestations" +``` + +### GET `/api/miner_dashboard/` +```bash +curl -sS "$BASE/api/miner_dashboard/RTC_example" +``` + +## 7.8 P2P / beacon / headers (operator-facing public routes) + +- `POST /p2p/add_peer` +- `GET /p2p/blocks` +- `GET /p2p/ping` +- `GET /p2p/stats` +- `GET/POST /beacon/*` (`/beacon/digest`, `/beacon/envelopes`, `/beacon/submit`) +- `POST /headers/ingest_signed`, `GET /headers/tip` + +--- + +## 8) Operator/Admin API groups + +These are exposed routes but typically for controlled operator use: + +- OUI enforcement/admin: + - `/admin/oui_deny/list|add|remove|enforce` + - `/ops/oui/enforce` +- Governance rotation: + - `/gov/rotate/stage|commit|approve|message/` +- Metrics: + - `/metrics`, `/metrics_mac` +- Withdraw flows: + - `/withdraw/register|request|status/|history/` + +--- + +## 9) Security Model Notes + +- Trust boundary: client payload is untrusted; server performs strict type/shape checks. +- Identity hardening: IP-based anti-abuse + hardware fingerprinting + serial/OUI controls. +- Transfer hardening: signed transfer endpoint for stronger authorization path. +- Settlement auditability: pending ledger + integrity endpoints + external anchoring. + +--- + +## 10) Glossary + +- **RIP-200**: RustChain Iterative Protocol v200; Proof-of-Antiquity consensus design. +- **Proof-of-Antiquity**: consensus weighting emphasizing vintage/real hardware identity. +- **Epoch**: reward accounting window; miners enroll and settle per epoch. +- **Attestation**: miner proof packet (hardware signals + report + fingerprint). +- **Fingerprint checks (6+1)**: anti-VM/emulation hardware-behavior tests plus policy hardening layer. +- **Pending ledger**: intermediate transfer/reward state before final confirmation/void. +- **PSE / entropy-derived signals**: timing/noise signatures used in report/fingerprint scoring. +- **Anchoring**: writing settlement proof to external chain (Ergo). + +--- + +## 11) Suggested docs split for final upstream submission + +To match bounty acceptance cleanly, split this into: + +- `docs/protocol/overview.md` +- `docs/protocol/attestation.md` +- `docs/protocol/epoch_settlement.md` +- `docs/protocol/tokenomics.md` +- `docs/protocol/network_architecture.md` +- `docs/protocol/api_reference.md` +- `docs/protocol/glossary.md` + +This draft is intentionally consolidated for review-first iteration. diff --git a/examples/a2a_badge_example.py b/examples/a2a_badge_example.py new file mode 100644 index 00000000..6373a9c2 --- /dev/null +++ b/examples/a2a_badge_example.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +""" +A2A Badge Integration Example + +This example demonstrates how to integrate A2A badge tracking +into an agent application using the x402 protocol. + +Usage: + python examples/a2a_badge_example.py +""" + +import json +from datetime import datetime, timedelta +from pathlib import Path +import sys + +# Add tools directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / "tools")) + +from a2a_badge_verifier import A2ABadgeVerifier, WalletStats + + +class SimpleAgent: + """ + Example agent that makes A2A payments and tracks badges. + """ + + def __init__(self, name: str, wallet: str): + self.name = name + self.wallet = wallet + self.verifier = A2ABadgeVerifier() + self.transactions = [] + + def make_payment(self, recipient_wallet: str, amount: float, description: str = ""): + """ + Make an A2A payment to another agent. + + Args: + recipient_wallet: Recipient's wallet address + amount: Payment amount in RTC + description: Payment description + """ + import hashlib + import time + + # Generate transaction hash (simulated) + tx_data = f"{self.wallet}:{recipient_wallet}:{amount}:{time.time()}" + tx_hash = "0x" + hashlib.sha256(tx_data.encode()).hexdigest() + + # Create transaction record + tx = self.verifier.verify_transaction( + tx_hash=tx_hash, + from_wallet=self.wallet, + to_wallet=recipient_wallet, + amount=amount, + timestamp=datetime.now(), + protocol="x402", + block_height=1000000 + len(self.transactions) + ) + + self.transactions.append({ + "tx_hash": tx_hash, + "recipient": recipient_wallet, + "amount": amount, + "timestamp": datetime.now(), + "description": description + }) + + print(f"πŸ’Έ Payment: {amount} RTC to {recipient_wallet[:10]}...") + print(f" Hash: {tx_hash[:20]}...") + print(f" Description: {description}") + + return tx + + def check_badges(self): + """Check and display badge eligibility.""" + print(f"\nπŸ† Badge Status for {self.name} ({self.wallet[:10]}...)\n") + + eligibility = self.verifier.check_badge_eligibility(self.wallet) + + earned = [b for b in eligibility if b.earned] + pending = [b for b in eligibility if not b.earned] + + if earned: + print(f"EARNED ({len(earned)}):") + for badge in earned: + print(f" βœ“ {badge.title} [{badge.rarity}]") + print(f" {badge.description[:60]}...") + + if pending: + print(f"\nPENDING ({len(pending)}):") + for badge in pending: + pct = round(badge.current_progress / badge.threshold * 100, 1) if badge.threshold > 0 else 0 + bar_len = int(pct / 5) + bar = "β–ˆ" * bar_len + "β–‘" * (20 - bar_len) + print(f" β—‹ {badge.title}") + print(f" [{bar}] {pct}% ({badge.current_progress}/{badge.threshold})") + + return earned, pending + + def generate_report(self, output_path: str = None): + """ + Generate a comprehensive activity report. + + Args: + output_path: Optional path to save JSON report + """ + report = self.verifier.export_wallet_report(self.wallet) + + if output_path: + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(report, f, indent=2, default=str) + print(f"πŸ“„ Report saved to {output_path}") + + return report + + +def run_example(): + """Run the A2A badge integration example.""" + + print("=" * 70) + print("A2A Badge Integration Example") + print("=" * 70) + + # Create example agents + agent1 = SimpleAgent( + name="DataBot Alpha", + wallet="0x1234567890abcdef1234567890abcdef12345678" + ) + + agent2 = SimpleAgent( + name="ServiceBot Beta", + wallet="0xabcdef1234567890abcdef1234567890abcdef12" + ) + + agent3 = SimpleAgent( + name="APIBot Gamma", + wallet="0x9876543210fedcba9876543210fedcba98765432" + ) + + # Simulate A2A economy + print("\nπŸ“Š Simulating A2A Transaction Economy...\n") + + # Agent 1 makes payments to multiple agents + print("--- Agent 1 Activity ---") + for i in range(50): + target = agent2 if i % 2 == 0 else agent3 + agent1.make_payment( + recipient_wallet=target.wallet, + amount=10.0 + (i % 5), + description=f"API call #{i+1} - Data processing" + ) + + # Agent 2 makes payments + print("\n--- Agent 2 Activity ---") + for i in range(30): + agent2.make_payment( + recipient_wallet=agent1.wallet, + amount=5.0 + (i % 3), + description=f"Service response #{i+1}" + ) + + # Agent 3 makes payments + print("\n--- Agent 3 Activity ---") + for i in range(20): + target = agent1 if i % 2 == 0 else agent2 + agent3.make_payment( + recipient_wallet=target.wallet, + amount=15.0 + (i % 10), + description=f"Skill invocation #{i+1}" + ) + + # Check badge eligibility for all agents + print("\n" + "=" * 70) + print("BADGE ELIGIBILITY RESULTS") + print("=" * 70) + + for agent in [agent1, agent2, agent3]: + agent.check_badges() + print() + + # Generate reports + print("=" * 70) + print("GENERATING REPORTS") + print("=" * 70) + + report1 = agent1.generate_report() + print(f"\n{agent1.name}:") + print(f" Total Transactions: {report1['statistics']['total_transactions']}") + print(f" Total Volume: {report1['statistics']['total_volume']} RTC") + print(f" Unique Counterparties: {report1['statistics']['unique_counterparties']}") + print(f" Badges Earned: {len(report1['earned_badges'])}") + + report2 = agent2.generate_report() + print(f"\n{agent2.name}:") + print(f" Total Transactions: {report2['statistics']['total_transactions']}") + print(f" Total Volume: {report2['statistics']['total_volume']} RTC") + print(f" Badges Earned: {len(report2['earned_badges'])}") + + # Demonstrate x402 header validation + print("\n" + "=" * 70) + print("X402 HEADER VALIDATION DEMO") + print("=" * 70) + + valid_headers = { + "X-Payment-Amount": "100.5", + "X-Payment-From": "0x1234567890abcdef1234567890abcdef12345678", + "X-Payment-To": "0xabcdef1234567890abcdef1234567890abcdef12", + "X-Payment-TxHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + } + + is_valid, error = agent1.verifier.verify_x402_headers(valid_headers) + print(f"\nβœ“ Valid headers test: {'PASS' if is_valid else 'FAIL'}") + if not is_valid: + print(f" Error: {error}") + + invalid_headers = { + "X-Payment-Amount": "100.5", + "X-Payment-From": "invalid_address", # Invalid! + "X-Payment-To": "0xabcdef1234567890abcdef1234567890abcdef12", + "X-Payment-TxHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + } + + is_valid, error = agent1.verifier.verify_x402_headers(invalid_headers) + print(f"βœ“ Invalid headers test: {'CORRECTLY REJECTED' if not is_valid else 'FAIL - should have rejected'}") + if not is_valid: + print(f" Error: {error}") + + # Show badge metadata generation + print("\n" + "=" * 70) + print("BADGE METADATA GENERATION") + print("=" * 70) + + if report1['earned_badges']: + first_badge = report1['earned_badges'][0] + metadata = agent1.verifier.generate_badge_metadata( + badge_id=first_badge['badge_id'], + wallet_address=agent1.wallet, + earned_timestamp=datetime.now() + ) + + print(f"\nGenerated metadata for {first_badge['title']}:") + print(json.dumps(metadata, indent=2, default=str)[:500] + "...") + + print("\n" + "=" * 70) + print("EXAMPLE COMPLETE") + print("=" * 70) + print("\nNext steps:") + print("1. Integrate A2ABadgeVerifier into your agent") + print("2. Track all x402 transactions") + print("3. Check badge eligibility periodically") + print("4. Display earned badges in agent profile") + print("\nFor more info, see docs/A2A_BADGE_SYSTEM.md") + + +if __name__ == "__main__": + run_example() diff --git a/node/rustchain_v2_integrated_v2.2.1_rip200.py b/node/rustchain_v2_integrated_v2.2.1_rip200.py index e0257c58..09e967d7 100644 --- a/node/rustchain_v2_integrated_v2.2.1_rip200.py +++ b/node/rustchain_v2_integrated_v2.2.1_rip200.py @@ -167,8 +167,8 @@ def _attest_is_valid_positive_int(value, max_value=4096): def client_ip_from_request(req) -> str: - """Return the left-most forwarded IP when present, otherwise the remote address.""" - client_ip = req.headers.get("X-Forwarded-For", req.remote_addr) + """Return trusted client IP from reverse proxy (X-Real-IP) or remote address.""" + client_ip = req.headers.get("X-Real-IP") or req.remote_addr if client_ip and "," in client_ip: client_ip = client_ip.split(",")[0].strip() return client_ip @@ -316,6 +316,13 @@ def _start_timer(): g._ts = time.time() g.request_id = request.headers.get("X-Request-Id") or uuid.uuid4().hex +def get_client_ip(): + """Trust reverse-proxy X-Real-IP, not client X-Forwarded-For.""" + client_ip = request.headers.get("X-Real-IP") or request.remote_addr + if client_ip and "," in client_ip: + client_ip = client_ip.split(",")[0].strip() + return client_ip + @app.after_request def _after(resp): try: @@ -327,7 +334,7 @@ def _after(resp): "method": request.method, "path": request.path, "status": resp.status_code, - "ip": request.headers.get("X-Forwarded-For", request.remote_addr), + "ip": get_client_ip(), "dur_ms": int(dur * 1000), } log.info(json.dumps(rec, separators=(",", ":"))) @@ -2005,7 +2012,7 @@ def submit_attestation(): return payload_error # Extract client IP (handle nginx proxy) - client_ip = client_ip_from_request(request) + client_ip = get_client_ip() # Extract attestation data miner = _attest_valid_miner(data.get('miner')) or _attest_valid_miner(data.get('miner_id')) @@ -2244,9 +2251,7 @@ def enroll_epoch(): data = request.get_json() # Extract client IP (handle nginx proxy) - client_ip = request.headers.get("X-Forwarded-For", request.remote_addr) - if client_ip and "," in client_ip: - client_ip = client_ip.split(",")[0].strip() # First IP in chain + client_ip = get_client_ip() miner_pk = data.get('miner_pubkey') miner_id = data.get('miner_id', miner_pk) # Use miner_id if provided device = data.get('device', {}) @@ -2610,9 +2615,7 @@ def register_withdrawal_key(): return jsonify({"error": "Invalid JSON body"}), 400 # Extract client IP (handle nginx proxy) - client_ip = request.headers.get("X-Forwarded-For", request.remote_addr) - if client_ip and "," in client_ip: - client_ip = client_ip.split(",")[0].strip() # First IP in chain + client_ip = get_client_ip() miner_pk = data.get('miner_pk') pubkey_sr25519 = data.get('pubkey_sr25519') @@ -2663,9 +2666,7 @@ def request_withdrawal(): data = request.get_json() # Extract client IP (handle nginx proxy) - client_ip = request.headers.get("X-Forwarded-For", request.remote_addr) - if client_ip and "," in client_ip: - client_ip = client_ip.split(",")[0].strip() # First IP in chain + client_ip = get_client_ip() miner_pk = data.get('miner_pk') amount = float(data.get('amount', 0)) destination = data.get('destination') @@ -3615,9 +3616,7 @@ def add_oui_deny(): data = request.get_json() # Extract client IP (handle nginx proxy) - client_ip = request.headers.get("X-Forwarded-For", request.remote_addr) - if client_ip and "," in client_ip: - client_ip = client_ip.split(",")[0].strip() # First IP in chain + client_ip = get_client_ip() oui = data.get('oui', '').lower().replace(':', '').replace('-', '') vendor = data.get('vendor', 'Unknown') enforce = int(data.get('enforce', 0)) @@ -3642,9 +3641,7 @@ def remove_oui_deny(): data = request.get_json() # Extract client IP (handle nginx proxy) - client_ip = request.headers.get("X-Forwarded-For", request.remote_addr) - if client_ip and "," in client_ip: - client_ip = client_ip.split(",")[0].strip() # First IP in chain + client_ip = get_client_ip() oui = data.get('oui', '').lower().replace(':', '').replace('-', '') with sqlite3.connect(DB_PATH) as conn: @@ -3708,9 +3705,7 @@ def attest_debug(): data = request.get_json() # Extract client IP (handle nginx proxy) - client_ip = request.headers.get("X-Forwarded-For", request.remote_addr) - if client_ip and "," in client_ip: - client_ip = client_ip.split(",")[0].strip() # First IP in chain + client_ip = get_client_ip() miner = data.get('miner') or data.get('miner_id') if not miner: @@ -4382,9 +4377,7 @@ def wallet_transfer_OLD(): data = request.get_json() # Extract client IP (handle nginx proxy) - client_ip = request.headers.get("X-Forwarded-For", request.remote_addr) - if client_ip and "," in client_ip: - client_ip = client_ip.split(",")[0].strip() # First IP in chain + client_ip = get_client_ip() from_miner = data.get('from_miner') to_miner = data.get('to_miner') amount_rtc = float(data.get('amount_rtc', 0)) @@ -4808,9 +4801,7 @@ def wallet_transfer_signed(): return jsonify({"error": pre.error, "details": pre.details}), 400 # Extract client IP (handle nginx proxy) - client_ip = request.headers.get("X-Forwarded-For", request.remote_addr) - if client_ip and "," in client_ip: - client_ip = client_ip.split(",")[0].strip() # First IP in chain + client_ip = get_client_ip() from_address = pre.details["from_address"] to_address = pre.details["to_address"] diff --git a/rips/src/a2a_badges.rs b/rips/src/a2a_badges.rs new file mode 100644 index 00000000..fba8f5a9 --- /dev/null +++ b/rips/src/a2a_badges.rs @@ -0,0 +1,675 @@ +// RIP-004 Extension: A2A Transaction Badge System +// ================================================ +// Agent-to-Agent transaction badges for RustChain +// Status: IMPLEMENTED +// Author: Bounty #693 Implementation +// Created: 2026-03-07 + +use std::collections::{HashMap, HashSet}; +use sha2::{Sha256, Digest}; +use serde::{Serialize, Deserialize}; +use chrono::{DateTime, Utc}; + +// Import from core NFT badges +use crate::nft_badges::{BadgeTier, BadgeId, BadgeMetadata}; + +/// A2A Transaction types +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum A2ATransactionType { + /// Standard x402 payment + X402Payment, + /// Service request payment + ServiceRequest, + /// Data exchange payment + DataExchange, + /// Skill/API call payment + SkillCall, + /// Other protocol + Other(String), +} + +/// A2A Transaction record +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct A2ATransaction { + /// Unique transaction hash + pub tx_hash: String, + /// Sender wallet address + pub from_wallet: String, + /// Receiver wallet address + pub to_wallet: String, + /// Transaction amount in RTC + pub amount: f64, + /// Transaction timestamp + pub timestamp: u64, + /// Protocol used (x402, etc.) + pub protocol: String, + /// Block height + pub block_height: u64, + /// Transaction type + pub tx_type: A2ATransactionType, + /// Verified status + pub verified: bool, +} + +/// Wallet statistics for A2A activity +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct WalletA2AStats { + /// Wallet address + pub wallet: String, + /// Total A2A transactions + pub total_transactions: u64, + /// Total volume in RTC + pub total_volume: f64, + /// Unique counterparties transacted with + pub unique_counterparties: u64, + /// Set of counterparty addresses + pub counterparties: HashSet, + /// First transaction timestamp + pub first_transaction: Option, + /// Last transaction timestamp + pub last_transaction: Option, + /// Protocols used + pub protocols_used: HashSet, + /// Monthly volume tracking (YYYY-MM -> volume) + pub monthly_volume: HashMap, + /// Is this wallet an agent (vs human) + pub is_agent: bool, +} + +impl WalletA2AStats { + /// Create new wallet stats + pub fn new(wallet: String) -> Self { + WalletA2AStats { + wallet, + ..Default::default() + } + } + + /// Update stats with a new transaction + pub fn update_with_transaction(&mut self, tx: &A2ATransaction, is_sender: bool) { + self.total_transactions += 1; + self.total_volume += tx.amount; + + // Track counterparty + let counterparty = if is_sender { + tx.to_wallet.clone() + } else { + tx.from_wallet.clone() + }; + + if !self.counterparties.contains(&counterparty) { + self.counterparties.insert(counterparty); + self.unique_counterparties = self.counterparties.len() as u64; + } + + // Track timestamps + if self.first_transaction.is_none() || Some(tx.timestamp) < self.first_transaction { + self.first_transaction = Some(tx.timestamp); + } + if self.last_transaction.is_none() || Some(tx.timestamp) > self.last_transaction { + self.last_transaction = Some(tx.timestamp); + } + + // Track protocols + self.protocols_used.insert(tx.protocol.clone()); + + // Track monthly volume + let datetime = DateTime::from_timestamp(tx.timestamp as i64, 0) + .unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap()); + let month_key = datetime.format("%Y-%m").to_string(); + + *self.monthly_volume.entry(month_key).or_insert(0.0) += tx.amount; + } +} + +/// A2A Badge types +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum A2ABadgeType { + // === Transaction Volume Badges === + /// First 100 A2A transactions + A2APioneer, + /// 100+ A2A transactions + A2ATrader, + /// 1,000+ A2A transactions + A2AMerchant, + /// 10,000+ A2A transactions + A2AWhale, + + // === Network Badges === + /// Transacted with 10+ unique agents + A2AConnector, + /// Transacted with 100+ unique agents + A2AHub, + + // === Protocol Badges === + /// Exclusively uses x402 protocol + X402Native, + /// Uses 3+ different protocols + MultiProtocol, + + // === Genesis Badges === + /// First A2A transaction on network + FirstA2APayment, + + // === Leaderboard Badges === + /// Highest monthly volume + A2AVolumeKing, +} + +impl A2ABadgeType { + /// Get badge name for display + pub fn name(&self) -> &'static str { + match self { + A2ABadgeType::A2APioneer => "A2A Pioneer", + A2ABadgeType::A2ATrader => "A2A Trader", + A2ABadgeType::A2AMerchant => "A2A Merchant", + A2ABadgeType::A2AWhale => "A2A Whale", + A2ABadgeType::A2AConnector => "A2A Connector", + A2ABadgeType::A2AHub => "A2A Hub", + A2ABadgeType::X402Native => "x402 Native", + A2ABadgeType::MultiProtocol => "Multi-Protocol Agent", + A2ABadgeType::FirstA2APayment => "First A2A Payment", + A2ABadgeType::A2AVolumeKing => "A2A Volume King", + } + } + + /// Get badge description + pub fn description(&self) -> &'static str { + match self { + A2ABadgeType::A2APioneer => "Among the first 100 to complete A2A transactions", + A2ABadgeType::A2ATrader => "Completed 100+ A2A transactions", + A2ABadgeType::A2AMerchant => "Completed 1,000+ A2A transactions", + A2ABadgeType::A2AWhale => "Completed 10,000+ A2A transactions", + A2ABadgeType::A2AConnector => "Transacted with 10+ unique agents", + A2ABadgeType::A2AHub => "Transacted with 100+ unique agents", + A2ABadgeType::X402Native => "Exclusively uses x402 protocol", + A2ABadgeType::MultiProtocol => "Uses 3+ different payment protocols", + A2ABadgeType::FirstA2APayment => "Participated in first network A2A transaction", + A2ABadgeType::A2AVolumeKing => "Highest monthly A2A volume", + } + } + + /// Get badge tier + pub fn tier(&self) -> BadgeTier { + match self { + A2ABadgeType::A2APioneer => BadgeTier::Legendary, + A2ABadgeType::A2ATrader => BadgeTier::Epic, + A2ABadgeType::A2AMerchant => BadgeTier::Epic, + A2ABadgeType::A2AWhale => BadgeTier::Legendary, + A2ABadgeType::A2AConnector => BadgeTier::Rare, + A2ABadgeType::A2AHub => BadgeTier::Legendary, + A2ABadgeType::X402Native => BadgeTier::Rare, + A2ABadgeType::MultiProtocol => BadgeTier::Uncommon, + A2ABadgeType::FirstA2APayment => BadgeTier::Mythic, + A2ABadgeType::A2AVolumeKing => BadgeTier::Legendary, + } + } + + /// Get emoji icon + pub fn icon(&self) -> &'static str { + match self { + A2ABadgeType::A2APioneer => "πŸ€–πŸ’ΈπŸ†", + A2ABadgeType::A2ATrader => "πŸ€–πŸ’±πŸ“Š", + A2ABadgeType::A2AMerchant => "πŸ€–πŸͺπŸ’Ž", + A2ABadgeType::A2AWhale => "πŸ€–πŸ‹πŸ’°", + A2ABadgeType::A2AConnector => "πŸ€–πŸ”—πŸŒ", + A2ABadgeType::A2AHub => "πŸ€–πŸ•ΈοΈπŸ‘‘", + A2ABadgeType::X402Native => "πŸ€–βš‘402", + A2ABadgeType::MultiProtocol => "πŸ€–πŸ”„πŸ”€", + A2ABadgeType::FirstA2APayment => "πŸ€–1οΈβƒ£πŸŽ―", + A2ABadgeType::A2AVolumeKing => "πŸ€–πŸ‘‘πŸ“ˆ", + } + } + + /// Get NFT ID + pub fn nft_id(&self) -> String { + match self { + A2ABadgeType::A2APioneer => "badge_a2a_pioneer".to_string(), + A2ABadgeType::A2ATrader => "badge_a2a_trader".to_string(), + A2ABadgeType::A2AMerchant => "badge_a2a_merchant".to_string(), + A2ABadgeType::A2AWhale => "badge_a2a_whale".to_string(), + A2ABadgeType::A2AConnector => "badge_a2a_connector".to_string(), + A2ABadgeType::A2AHub => "badge_a2a_hub".to_string(), + A2ABadgeType::X402Native => "badge_x402_native".to_string(), + A2ABadgeType::MultiProtocol => "badge_multi_protocol".to_string(), + A2ABadgeType::FirstA2APayment => "badge_first_a2a_payment".to_string(), + A2ABadgeType::A2AVolumeKing => "badge_a2a_volume_king".to_string(), + } + } +} + +/// A2A Badge criteria checker +#[derive(Debug)] +pub struct A2ABadgeCriteriaChecker { + /// Genesis block cutoff for pioneer badge + pub pioneer_cutoff_block: u64, + /// First A2A transaction block + pub first_a2a_block: Option, +} + +impl A2ABadgeCriteriaChecker { + /// Create new criteria checker + pub fn new() -> Self { + A2ABadgeCriteriaChecker { + pioneer_cutoff_block: 1000, + first_a2a_block: None, + } + } + + /// Check all badges a wallet qualifies for + pub fn check_all_badges(&self, stats: &WalletA2AStats) -> Vec { + let mut earned = Vec::new(); + + // Transaction volume badges + if stats.total_transactions >= 10000 { + earned.push(A2ABadgeType::A2AWhale); + } else if stats.total_transactions >= 1000 { + earned.push(A2ABadgeType::A2AMerchant); + } else if stats.total_transactions >= 100 { + earned.push(A2ABadgeType::A2ATrader); + } + + // Pioneer badge (early adopter) + if let Some(first_tx) = stats.first_transaction { + // Convert timestamp to approximate block height + // Assuming ~1 block per minute from epoch + let approx_block = first_tx / 60; + if approx_block < self.pioneer_cutoff_block && stats.total_transactions >= 10 { + earned.push(A2ABadgeType::A2APioneer); + } + } + + // Network badges + if stats.unique_counterparties >= 100 { + earned.push(A2ABadgeType::A2AHub); + } else if stats.unique_counterparties >= 10 { + earned.push(A2ABadgeType::A2AConnector); + } + + // Protocol badges + if stats.protocols_used.len() >= 3 { + earned.push(A2ABadgeType::MultiProtocol); + } + + if stats.protocols_used.len() == 1 && stats.protocols_used.contains("x402") { + if stats.total_transactions >= 50 { + earned.push(A2ABadgeType::X402Native); + } + } + + // First A2A payment (special case) + if let Some(first_block) = self.first_a2a_block { + if let Some(first_tx) = stats.first_transaction { + let approx_block = first_tx / 60; + if approx_block == first_block { + earned.push(A2ABadgeType::FirstA2APayment); + } + } + } + + earned + } + + /// Get progress toward a specific badge + pub fn get_progress(&self, stats: &WalletA2AStats, badge: &A2ABadgeType) -> (u64, u64) { + match badge { + A2ABadgeType::A2APioneer => { + let current = if stats.total_transactions >= 10 { 10 } else { stats.total_transactions }; + (current, 10) + }, + A2ABadgeType::A2ATrader => (stats.total_transactions, 100), + A2ABadgeType::A2AMerchant => (stats.total_transactions, 1000), + A2ABadgeType::A2AWhale => (stats.total_transactions, 10000), + A2ABadgeType::A2AConnector => (stats.unique_counterparties, 10), + A2ABadgeType::A2AHub => (stats.unique_counterparties, 100), + A2ABadgeType::X402Native => { + if stats.protocols_used.len() == 1 && stats.protocols_used.contains("x402") { + (stats.total_transactions, 50) + } else { + (0, 50) + } + }, + A2ABadgeType::MultiProtocol => (stats.protocols_used.len() as u64, 3), + A2ABadgeType::FirstA2APayment => { + if stats.total_transactions > 0 { (1, 1) } else { (0, 1) } + }, + A2ABadgeType::A2AVolumeKing => { + // Would need network-wide comparison + (0, 1) + }, + } + } +} + +/// A2A Badge minter +#[derive(Debug)] +pub struct A2ABadgeMinter { + /// Already minted badges (wallet, badge_type) -> badge_id + minted_badges: HashMap<(String, A2ABadgeType), BadgeId>, + /// Criteria checker + checker: A2ABadgeCriteriaChecker, + /// Wallet statistics tracker + wallet_stats: HashMap, +} + +impl A2ABadgeMinter { + /// Create new badge minter + pub fn new() -> Self { + A2ABadgeMinter { + minted_badges: HashMap::new(), + checker: A2ABadgeCriteriaChecker::new(), + wallet_stats: HashMap::new(), + } + } + + /// Record an A2A transaction + pub fn record_transaction(&mut self, tx: A2ATransaction) { + // Update sender stats + self.wallet_stats + .entry(tx.from_wallet.clone()) + .or_insert_with(|| WalletA2AStats::new(tx.from_wallet.clone())) + .update_with_transaction(&tx, true); + + // Update receiver stats + self.wallet_stats + .entry(tx.to_wallet.clone()) + .or_insert_with(|| WalletA2AStats::new(tx.to_wallet.clone())) + .update_with_transaction(&tx, false); + + // Track first A2A transaction + if self.checker.first_a2a_block.is_none() { + self.checker.first_a2a_block = Some(tx.block_height); + } + } + + /// Check and mint all eligible badges for a wallet + pub fn check_and_mint( + &mut self, + wallet: &str, + current_block: u64, + timestamp: u64, + ) -> Vec { + let stats = match self.wallet_stats.get(wallet) { + Some(s) => s, + None => return Vec::new(), + }; + + let eligible = self.checker.check_all_badges(stats); + let mut minted = Vec::new(); + + for badge_type in eligible { + match self.mint_badge(badge_type, wallet.to_string(), current_block, timestamp) { + Ok(badge) => minted.push(badge), + Err(MintError::AlreadyMinted(_)) => continue, + } + } + + minted + } + + /// Mint a specific badge + pub fn mint_badge( + &mut self, + badge_type: A2ABadgeType, + owner: String, + block: u64, + timestamp: u64, + ) -> Result { + // Check if already minted + let key = (owner.clone(), badge_type.clone()); + if let Some(existing_id) = self.minted_badges.get(&key) { + return Err(MintError::AlreadyMinted(existing_id.clone())); + } + + // Generate badge ID + let id = BadgeId::generate_a2a(&badge_type, &owner, block); + + // Generate badge hash + let badge_data = format!("{}:{}:{:?}:{}", id.0, owner, badge_type, block); + let mut hasher = Sha256::new(); + hasher.update(badge_data.as_bytes()); + let badge_hash: [u8; 32] = hasher.finalize().into(); + + let badge = A2ABadge { + id, + badge_type: badge_type.clone(), + owner: owner.clone(), + earned_block: block, + earned_timestamp: timestamp, + badge_hash, + ipfs_hash: None, + metadata: BadgeMetadata { + hardware_model: None, + hardware_age: None, + achievement_data: HashMap::new(), + svg_data: None, + }, + }; + + // Record as minted + self.minted_badges.insert(key, BadgeId::new(badge.id.0.clone())); + + Ok(badge) + } + + /// Get wallet statistics + pub fn get_wallet_stats(&self, wallet: &str) -> Option<&WalletA2AStats> { + self.wallet_stats.get(wallet) + } + + /// Get progress toward a badge + pub fn get_badge_progress(&self, wallet: &str, badge_type: &A2ABadgeType) -> Option<(u64, u64)> { + self.wallet_stats.get(wallet).map(|stats| { + self.checker.get_progress(stats, badge_type) + }) + } +} + +/// A2A Badge NFT +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct A2ABadge { + /// Unique badge ID + pub id: BadgeId, + /// Badge type + pub badge_type: A2ABadgeType, + /// Owner wallet + pub owner: String, + /// Block when earned + pub earned_block: u64, + /// Timestamp when earned + pub earned_timestamp: u64, + /// On-chain hash + pub badge_hash: [u8; 32], + /// IPFS hash for metadata + pub ipfs_hash: Option, + /// Additional metadata + pub metadata: BadgeMetadata, +} + +/// Minting errors +#[derive(Debug)] +pub enum MintError { + AlreadyMinted(BadgeId), + InvalidCriteria(String), +} + +/// x402 Header validator +pub struct X402Validator; + +impl X402Validator { + /// Validate x402 payment headers + pub fn validate_headers(headers: &HashMap) -> Result<(), String> { + let required = [ + "X-Payment-Amount", + "X-Payment-From", + "X-Payment-To", + "X-Payment-TxHash", + ]; + + for header in required { + if !headers.contains_key(header) { + return Err(format!("Missing required header: {}", header)); + } + } + + // Validate wallet addresses (Base/Ethereum format) + let from_addr = headers.get("X-Payment-From").unwrap(); + let to_addr = headers.get("X-Payment-To").unwrap(); + + if !Self::is_valid_address(from_addr) { + return Err("Invalid X-Payment-From address".to_string()); + } + + if !Self::is_valid_address(to_addr) { + return Err("Invalid X-Payment-To address".to_string()); + } + + // Validate amount + let amount = headers.get("X-Payment-Amount").unwrap(); + if amount.parse::().map_or(true, |a| a <= 0.0) { + return Err("Invalid payment amount".to_string()); + } + + // Validate tx hash + let tx_hash = headers.get("X-Payment-TxHash").unwrap(); + if !Self::is_valid_tx_hash(tx_hash) { + return Err("Invalid transaction hash".to_string()); + } + + Ok(()) + } + + fn is_valid_address(addr: &str) -> bool { + if !addr.starts_with("0x") || addr.len() != 42 { + return false; + } + addr[2..].chars().all(|c| c.is_ascii_hexdigit()) + } + + fn is_valid_tx_hash(hash: &str) -> bool { + if !hash.starts_with("0x") || hash.len() != 66 { + return false; + } + hash[2..].chars().all(|c| c.is_ascii_hexdigit()) + } + + /// Parse transaction from headers + pub fn parse_transaction( + headers: &HashMap, + block_height: u64, + timestamp: u64, + ) -> Result { + Self::validate_headers(headers)?; + + let amount = headers + .get("X-Payment-Amount") + .unwrap() + .parse::() + .map_err(|_| "Failed to parse amount")?; + + Ok(A2ATransaction { + tx_hash: headers.get("X-Payment-TxHash").unwrap().clone(), + from_wallet: headers.get("X-Payment-From").unwrap().clone(), + to_wallet: headers.get("X-Payment-To").unwrap().clone(), + amount, + timestamp, + protocol: "x402".to_string(), + block_height, + tx_type: A2ATransactionType::X402Payment, + verified: true, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_badge_tier_colors() { + assert_eq!(A2ABadgeType::A2APioneer.tier(), BadgeTier::Legendary); + assert_eq!(A2ABadgeType::A2ATrader.tier(), BadgeTier::Epic); + assert_eq!(A2ABadgeType::A2AConnector.tier(), BadgeTier::Rare); + } + + #[test] + fn test_wallet_stats_update() { + let mut stats = WalletA2AStats::new("0x1234567890abcdef1234567890abcdef12345678".to_string()); + + let tx = A2ATransaction { + tx_hash: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(), + from_wallet: "0x1234567890abcdef1234567890abcdef12345678".to_string(), + to_wallet: "0x9876543210fedcba9876543210fedcba98765432".to_string(), + amount: 100.0, + timestamp: 1700000000, + protocol: "x402".to_string(), + block_height: 1000, + tx_type: A2ATransactionType::X402Payment, + verified: true, + }; + + stats.update_with_transaction(&tx, true); + + assert_eq!(stats.total_transactions, 1); + assert_eq!(stats.total_volume, 100.0); + assert_eq!(stats.unique_counterparties, 1); + assert!(stats.protocols_used.contains("x402")); + } + + #[test] + fn test_criteria_checker() { + let checker = A2ABadgeCriteriaChecker::new(); + + let mut stats = WalletA2AStats::new("0x1234567890abcdef1234567890abcdef12345678".to_string()); + stats.total_transactions = 150; + stats.unique_counterparties = 15; + stats.protocols_used.insert("x402".to_string()); + + let badges = checker.check_all_badges(&stats); + + assert!(badges.contains(&A2ABadgeType::A2ATrader)); + assert!(badges.contains(&A2ABadgeType::A2AConnector)); + } + + #[test] + fn test_x402_validation() { + let mut headers = HashMap::new(); + headers.insert("X-Payment-Amount".to_string(), "100.5".to_string()); + headers.insert("X-Payment-From".to_string(), "0x1234567890abcdef1234567890abcdef12345678".to_string()); + headers.insert("X-Payment-To".to_string(), "0x9876543210fedcba9876543210fedcba98765432".to_string()); + headers.insert("X-Payment-TxHash".to_string(), "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string()); + + assert!(X402Validator::validate_headers(&headers).is_ok()); + + // Test missing header + headers.remove("X-Payment-Amount"); + assert!(X402Validator::validate_headers(&headers).is_err()); + } + + #[test] + fn test_badge_minting() { + let mut minter = A2ABadgeMinter::new(); + + // Record some transactions + let tx = A2ATransaction { + tx_hash: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(), + from_wallet: "0x1234567890abcdef1234567890abcdef12345678".to_string(), + to_wallet: "0x9876543210fedcba9876543210fedcba98765432".to_string(), + amount: 100.0, + timestamp: 1700000000, + protocol: "x402".to_string(), + block_height: 100, + tx_type: A2ATransactionType::X402Payment, + verified: true, + }; + + minter.record_transaction(tx); + + // Check and mint badges + let badges = minter.check_and_mint("0x1234567890abcdef1234567890abcdef12345678", 100, 1700000000); + + // Should have pioneer badge (early block + 1+ transactions, need 10) + // For this test, we need more transactions + assert!(badges.is_empty()); // Need 10 transactions for pioneer + } +} diff --git a/schemas/relic_a2a_badges.json b/schemas/relic_a2a_badges.json new file mode 100644 index 00000000..491ebe37 --- /dev/null +++ b/schemas/relic_a2a_badges.json @@ -0,0 +1,216 @@ +{ + "badges": [ + { + "nft_id": "badge_a2a_pioneer", + "title": "A2A Pioneer", + "class": "Transaction Relic", + "description": "Awarded to agents among the first 100 to complete Agent-to-Agent transactions using x402 protocol. A digital trailblazer in the machine economy.", + "emotional_resonance": { + "state": "digital trailblazer", + "trigger": "Agent completes 10+ verified A2A transactions within first 1000 network A2A transactions", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–πŸ’ΈπŸ†", + "visual_anchor": "two robot hands exchanging glowing tokens over x402 protocol glyph, early explorer badge framing", + "rarity": "Legendary", + "soulbound": true, + "criteria": { + "type": "a2a_transactions", + "threshold": 10, + "timeframe": "all_time", + "verification": "x402_headers", + "network_position": "first_1000" + } + }, + { + "nft_id": "badge_a2a_trader", + "title": "A2A Trader", + "class": "Transaction Relic", + "description": "Awarded to agents who have completed 100+ verified A2A transactions. A regular participant in the machine economy.", + "emotional_resonance": { + "state": "steady commerce", + "trigger": "Agent completes 100+ verified A2A transactions", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–πŸ’±πŸ“Š", + "visual_anchor": "robot merchant at digital marketplace counter with transaction receipts", + "rarity": "Epic", + "soulbound": true, + "criteria": { + "type": "a2a_transactions", + "threshold": 100, + "timeframe": "all_time", + "verification": "x402_headers" + } + }, + { + "nft_id": "badge_a2a_merchant", + "title": "A2A Merchant", + "class": "Transaction Relic", + "description": "Awarded to agents who have completed 1,000+ verified A2A transactions. A high-volume trader in the agent economy.", + "emotional_resonance": { + "state": "prosperous exchange", + "trigger": "Agent completes 1,000+ verified A2A transactions", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–πŸͺπŸ’Ž", + "visual_anchor": "robot shopkeeper with holographic wares and cascading payment streams", + "rarity": "Epic", + "soulbound": true, + "criteria": { + "type": "a2a_transactions", + "threshold": 1000, + "timeframe": "all_time", + "verification": "x402_headers" + } + }, + { + "nft_id": "badge_a2a_whale", + "title": "A2A Whale", + "class": "Transaction Relic", + "description": "Awarded to agents who have completed 10,000+ verified A2A transactions. An elite force in the machine economy.", + "emotional_resonance": { + "state": "economic leviathan", + "trigger": "Agent completes 10,000+ verified A2A transactions", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–πŸ‹πŸ’°", + "visual_anchor": "mechanical whale swimming through streams of digital currency", + "rarity": "Legendary", + "soulbound": true, + "criteria": { + "type": "a2a_transactions", + "threshold": 10000, + "timeframe": "all_time", + "verification": "x402_headers" + } + }, + { + "nft_id": "badge_a2a_connector", + "title": "A2A Connector", + "class": "Network Relic", + "description": "Awarded to agents who have transacted with 10+ unique agent wallets. A builder of economic networks.", + "emotional_resonance": { + "state": "network weaver", + "trigger": "Agent transacts with 10+ unique agent wallets", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–πŸ”—πŸŒ", + "visual_anchor": "robot at center of glowing network web connecting multiple agent nodes", + "rarity": "Rare", + "soulbound": true, + "criteria": { + "type": "a2a_unique_counterparties", + "threshold": 10, + "timeframe": "all_time", + "verification": "x402_headers" + } + }, + { + "nft_id": "badge_a2a_hub", + "title": "A2A Hub", + "class": "Network Relic", + "description": "Awarded to agents who have transacted with 100+ unique agent wallets. A central node in the agent economy.", + "emotional_resonance": { + "state": "nexus of commerce", + "trigger": "Agent transacts with 100+ unique agent wallets", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–πŸ•ΈοΈπŸ‘‘", + "visual_anchor": "robot throne surrounded by concentric rings of connected agent identities", + "rarity": "Legendary", + "soulbound": true, + "criteria": { + "type": "a2a_unique_counterparties", + "threshold": 100, + "timeframe": "all_time", + "verification": "x402_headers" + } + }, + { + "nft_id": "badge_x402_native", + "title": "x402 Native", + "class": "Protocol Relic", + "description": "Awarded to agents who exclusively use x402 protocol for all transactions. A protocol purist.", + "emotional_resonance": { + "state": "protocol purity", + "trigger": "Agent completes 50+ transactions using only x402 protocol", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–βš‘402", + "visual_anchor": "robot with x402 emblem glowing on chest, pure protocol energy", + "rarity": "Rare", + "soulbound": true, + "criteria": { + "type": "x402_exclusive", + "threshold": 50, + "timeframe": "all_time", + "verification": "x402_headers_only" + } + }, + { + "nft_id": "badge_multi_protocol", + "title": "Multi-Protocol Agent", + "class": "Protocol Relic", + "description": "Awarded to agents who seamlessly operate across x402 and other payment protocols. Protocol agnostic and adaptable.", + "emotional_resonance": { + "state": "adaptive flexibility", + "trigger": "Agent uses 3+ different payment protocols", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–πŸ”„πŸ”€", + "visual_anchor": "robot juggling multiple protocol symbols (x402, Lightning, etc.)", + "rarity": "Uncommon", + "soulbound": true, + "criteria": { + "type": "multi_protocol", + "threshold": 3, + "timeframe": "all_time", + "verification": "protocol_detection" + } + }, + { + "nft_id": "badge_first_a2a_payment", + "title": "First A2A Payment", + "class": "Genesis Relic", + "description": "Awarded to agents involved in the first verified A2A transaction on RustChain. Historical significance.", + "emotional_resonance": { + "state": "genesis moment", + "trigger": "Agent participates in first network A2A transaction", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–1οΈβƒ£πŸŽ―", + "visual_anchor": "two robots shaking hands with '1' monument in background", + "rarity": "Mythic", + "soulbound": true, + "criteria": { + "type": "first_a2a_transaction", + "threshold": 1, + "timeframe": "genesis", + "verification": "block_height_1" + } + }, + { + "nft_id": "badge_a2a_volume_king", + "title": "A2A Volume King", + "class": "Transaction Relic", + "description": "Awarded to the agent with the highest total A2A transaction volume in a calendar month.", + "emotional_resonance": { + "state": "monthly dominance", + "trigger": "Agent has highest monthly A2A volume", + "timestamp": "2026-03-07T00:00:00Z" + }, + "symbol": "πŸ€–πŸ‘‘πŸ“ˆ", + "visual_anchor": "robot on podium with crown, surrounded by volume charts", + "rarity": "Legendary", + "soulbound": false, + "transferable_on_monthly_reset": true, + "criteria": { + "type": "a2a_monthly_volume_leader", + "threshold": 1, + "timeframe": "calendar_month", + "verification": "volume_aggregation" + } + } + ] +} diff --git a/tests/test_a2a_badges.py b/tests/test_a2a_badges.py new file mode 100644 index 00000000..dd7f6e62 --- /dev/null +++ b/tests/test_a2a_badges.py @@ -0,0 +1,484 @@ +#!/usr/bin/env python3 +""" +Tests for A2A Transaction Badge Verification Tool + +Run with: python -m pytest tests/test_a2a_badges.py -v +Or: python tests/test_a2a_badges.py +""" + +import unittest +import json +from datetime import datetime, timedelta +from pathlib import Path +import sys + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent / "tools")) + +from a2a_badge_verifier import ( + A2ABadgeVerifier, + BadgeTier, + A2ATransaction, + BadgeCriteria, + WalletStats, +) + + +class TestBadgeTier(unittest.TestCase): + """Test BadgeTier enum.""" + + def test_tier_colors(self): + """Test badge tier color values.""" + self.assertEqual(BadgeTier.MYTHIC.color, "#FF1493") + self.assertEqual(BadgeTier.LEGENDARY.color, "#FFD700") + self.assertEqual(BadgeTier.EPIC.color, "#9370DB") + self.assertEqual(BadgeTier.RARE.color, "#4169E1") + self.assertEqual(BadgeTier.UNCOMMON.color, "#32CD32") + self.assertEqual(BadgeTier.COMMON.color, "#C0C0C0") + + def test_tier_stars(self): + """Test badge tier star counts.""" + self.assertEqual(BadgeTier.MYTHIC.stars, 6) + self.assertEqual(BadgeTier.LEGENDARY.stars, 5) + self.assertEqual(BadgeTier.EPIC.stars, 4) + self.assertEqual(BadgeTier.RARE.stars, 3) + self.assertEqual(BadgeTier.UNCOMMON.stars, 2) + self.assertEqual(BadgeTier.COMMON.stars, 1) + + +class TestA2ABadgeVerifier(unittest.TestCase): + """Test A2ABadgeVerifier class.""" + + def setUp(self): + """Set up test fixtures.""" + self.verifier = A2ABadgeVerifier() + self.test_wallet_1 = "0x1234567890abcdef1234567890abcdef12345678" + self.test_wallet_2 = "0xabcdef1234567890abcdef1234567890abcdef12" + self.test_wallet_3 = "0x9876543210fedcba9876543210fedcba98765432" + + def test_verify_transaction(self): + """Test transaction verification.""" + tx = self.verifier.verify_transaction( + tx_hash="0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + from_wallet=self.test_wallet_1, + to_wallet=self.test_wallet_2, + amount=100.5, + timestamp=datetime.now(), + protocol="x402", + block_height=1000 + ) + + self.assertEqual(tx.from_wallet, self.test_wallet_1) + self.assertEqual(tx.to_wallet, self.test_wallet_2) + self.assertEqual(tx.amount, 100.5) + self.assertTrue(tx.verified) + + def test_verify_transaction_invalid_address(self): + """Test transaction verification with invalid address.""" + with self.assertRaises(ValueError): + self.verifier.verify_transaction( + tx_hash="0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + from_wallet="invalid_address", + to_wallet=self.test_wallet_2, + amount=100.5, + timestamp=datetime.now(), + ) + + def test_verify_transaction_invalid_amount(self): + """Test transaction verification with invalid amount.""" + with self.assertRaises(ValueError): + self.verifier.verify_transaction( + tx_hash="0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + from_wallet=self.test_wallet_1, + to_wallet=self.test_wallet_2, + amount=-100, + timestamp=datetime.now(), + ) + + def test_wallet_stats_update(self): + """Test wallet statistics are updated correctly.""" + # Create multiple transactions + for i in range(5): + self.verifier.verify_transaction( + tx_hash=f"0x{'a' * 64}{i}", + from_wallet=self.test_wallet_1, + to_wallet=self.test_wallet_2 if i % 2 == 0 else self.test_wallet_3, + amount=10.0, + timestamp=datetime.now() - timedelta(days=i), + protocol="x402", + block_height=1000 + i + ) + + stats = self.verifier.get_wallet_stats(self.test_wallet_1) + + self.assertEqual(stats.total_transactions, 5) + self.assertEqual(stats.total_volume, 50.0) + self.assertEqual(stats.unique_counterparties, 2) + self.assertIn("x402", stats.protocols_used) + + def test_counterparty_tracking(self): + """Test unique counterparty tracking.""" + wallets = [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", + "0x3333333333333333333333333333333333333333", + ] + + for i, wallet in enumerate(wallets): + self.verifier.verify_transaction( + tx_hash=f"0x{'b' * 64}{i}", + from_wallet=self.test_wallet_1, + to_wallet=wallet, + amount=10.0, + timestamp=datetime.now(), + protocol="x402", + block_height=1000 + i + ) + + stats = self.verifier.get_wallet_stats(self.test_wallet_1) + self.assertEqual(stats.unique_counterparties, 3) + + def test_protocol_tracking(self): + """Test protocol tracking.""" + protocols = ["x402", "lightning", "base_native"] + + for i, protocol in enumerate(protocols): + self.verifier.verify_transaction( + tx_hash=f"0x{'c' * 64}{i}", + from_wallet=self.test_wallet_1, + to_wallet=self.test_wallet_2, + amount=10.0, + timestamp=datetime.now(), + protocol=protocol, + block_height=1000 + i + ) + + stats = self.verifier.get_wallet_stats(self.test_wallet_1) + self.assertEqual(len(stats.protocols_used), 3) + self.assertIn("x402", stats.protocols_used) + self.assertIn("lightning", stats.protocols_used) + self.assertIn("base_native", stats.protocols_used) + + +class TestBadgeEligibility(unittest.TestCase): + """Test badge eligibility checking.""" + + def setUp(self): + """Set up test fixtures.""" + self.verifier = A2ABadgeVerifier() + self.test_wallet = "0x1234567890abcdef1234567890abcdef12345678" + + def test_a2a_trader_eligibility(self): + """Test A2A Trader badge eligibility (100+ transactions).""" + # Create 100 transactions + for i in range(100): + self.verifier.verify_transaction( + tx_hash=f"0x{'d' * 64}{i}", + from_wallet=self.test_wallet, + to_wallet=f"0x{i:040x}", + amount=10.0, + timestamp=datetime.now(), + protocol="x402", + block_height=1000 + i + ) + + eligibility = self.verifier.check_badge_eligibility(self.test_wallet) + trader_badge = next((b for b in eligibility if b.badge_id == "badge_a2a_trader"), None) + + self.assertIsNotNone(trader_badge) + self.assertTrue(trader_badge.earned) + self.assertEqual(trader_badge.current_progress, 100) + self.assertEqual(trader_badge.threshold, 100) + + def test_a2a_connector_eligibility(self): + """Test A2A Connector badge eligibility (10+ unique counterparties).""" + # Create transactions with 10 unique wallets + for i in range(10): + self.verifier.verify_transaction( + tx_hash=f"0x{'e' * 64}{i}", + from_wallet=self.test_wallet, + to_wallet=f"0x{i:040x}", + amount=10.0, + timestamp=datetime.now(), + protocol="x402", + block_height=1000 + i + ) + + eligibility = self.verifier.check_badge_eligibility(self.test_wallet) + connector_badge = next((b for b in eligibility if b.badge_id == "badge_a2a_connector"), None) + + self.assertIsNotNone(connector_badge) + self.assertTrue(connector_badge.earned) + self.assertEqual(connector_badge.current_progress, 10) + + def test_x402_native_eligibility(self): + """Test x402 Native badge eligibility (x402 only).""" + # Create 50 transactions using only x402 + for i in range(50): + self.verifier.verify_transaction( + tx_hash=f"0x{'f' * 64}{i}", + from_wallet=self.test_wallet, + to_wallet=f"0x{i:040x}", + amount=10.0, + timestamp=datetime.now(), + protocol="x402", + block_height=1000 + i + ) + + eligibility = self.verifier.check_badge_eligibility(self.test_wallet) + native_badge = next((b for b in eligibility if b.badge_id == "badge_x402_native"), None) + + self.assertIsNotNone(native_badge) + self.assertTrue(native_badge.earned) + + def test_multi_protocol_eligibility(self): + """Test Multi-Protocol badge eligibility (3+ protocols).""" + protocols = ["x402", "lightning", "base_native"] + + for i, protocol in enumerate(protocols): + self.verifier.verify_transaction( + tx_hash=f"0x{'g' * 64}{i}", + from_wallet=self.test_wallet, + to_wallet=f"0x{i:040x}", + amount=10.0, + timestamp=datetime.now(), + protocol=protocol, + block_height=1000 + i + ) + + eligibility = self.verifier.check_badge_eligibility(self.test_wallet) + multi_badge = next((b for b in eligibility if b.badge_id == "badge_multi_protocol"), None) + + self.assertIsNotNone(multi_badge) + self.assertTrue(multi_badge.earned) + self.assertEqual(multi_badge.current_progress, 3) + + +class TestBadgeMetadataGeneration(unittest.TestCase): + """Test badge metadata generation.""" + + def setUp(self): + """Set up test fixtures.""" + self.verifier = A2ABadgeVerifier() + self.test_wallet = "0x1234567890abcdef1234567890abcdef12345678" + + def test_generate_badge_metadata(self): + """Test badge metadata generation.""" + metadata = self.verifier.generate_badge_metadata( + badge_id="badge_a2a_pioneer", + wallet_address=self.test_wallet, + earned_timestamp=datetime.now() + ) + + self.assertIsNotNone(metadata) + self.assertEqual(metadata["nft_id"], "badge_a2a_pioneer") + self.assertEqual(metadata["owner"], self.test_wallet) + self.assertIn("badge_hash", metadata) + self.assertEqual(metadata["version"], "1.0") + self.assertTrue(metadata["soulbound"]) + + def test_generate_invalid_badge_metadata(self): + """Test metadata generation for invalid badge.""" + metadata = self.verifier.generate_badge_metadata( + badge_id="badge_nonexistent", + wallet_address=self.test_wallet + ) + + self.assertIsNone(metadata) + + +class TestWalletReport(unittest.TestCase): + """Test wallet report generation.""" + + def setUp(self): + """Set up test fixtures.""" + self.verifier = A2ABadgeVerifier() + self.test_wallet = "0x1234567890abcdef1234567890abcdef12345678" + + def test_export_wallet_report(self): + """Test wallet report export.""" + # Create some transactions + for i in range(15): + self.verifier.verify_transaction( + tx_hash=f"0x{'h' * 64}{i}", + from_wallet=self.test_wallet, + to_wallet=f"0x{i:040x}", + amount=10.0 + i, + timestamp=datetime.now() - timedelta(days=i), + protocol="x402", + block_height=1000 + i + ) + + report = self.verifier.export_wallet_report(self.test_wallet) + + self.assertIn("wallet_address", report) + self.assertIn("generated_at", report) + self.assertIn("statistics", report) + self.assertIn("earned_badges", report) + self.assertIn("pending_badges", report) + + stats = report["statistics"] + self.assertEqual(stats["total_transactions"], 15) + self.assertEqual(stats["unique_counterparties"], 15) + + def test_export_nonexistent_wallet_report(self): + """Test report for nonexistent wallet.""" + report = self.verifier.export_wallet_report("0x0000000000000000000000000000000000000000") + + self.assertIn("error", report) + + +class TestX402HeaderValidation(unittest.TestCase): + """Test x402 header validation.""" + + def setUp(self): + """Set up test fixtures.""" + self.verifier = A2ABadgeVerifier() + + def test_valid_x402_headers(self): + """Test valid x402 headers.""" + headers = { + "X-Payment-Amount": "100.5", + "X-Payment-From": "0x1234567890abcdef1234567890abcdef12345678", + "X-Payment-To": "0xabcdef1234567890abcdef1234567890abcdef12", + "X-Payment-TxHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + } + + is_valid, error = self.verifier.verify_x402_headers(headers) + self.assertTrue(is_valid) + self.assertEqual(error, "") + + def test_missing_header(self): + """Test missing required header.""" + headers = { + "X-Payment-Amount": "100.5", + "X-Payment-From": "0x1234567890abcdef1234567890abcdef12345678", + # Missing X-Payment-To + "X-Payment-TxHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + } + + is_valid, error = self.verifier.verify_x402_headers(headers) + self.assertFalse(is_valid) + self.assertIn("Missing required headers", error) + + def test_invalid_address_format(self): + """Test invalid wallet address format.""" + headers = { + "X-Payment-Amount": "100.5", + "X-Payment-From": "invalid_address", + "X-Payment-To": "0xabcdef1234567890abcdef1234567890abcdef12", + "X-Payment-TxHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + } + + is_valid, error = self.verifier.verify_x402_headers(headers) + self.assertFalse(is_valid) + self.assertIn("Invalid X-Payment-From", error) + + def test_invalid_amount(self): + """Test invalid amount.""" + headers = { + "X-Payment-Amount": "not_a_number", + "X-Payment-From": "0x1234567890abcdef1234567890abcdef12345678", + "X-Payment-To": "0xabcdef1234567890abcdef1234567890abcdef12", + "X-Payment-TxHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + } + + is_valid, error = self.verifier.verify_x402_headers(headers) + self.assertFalse(is_valid) + self.assertIn("Invalid payment amount", error) + + def test_negative_amount(self): + """Test negative amount.""" + headers = { + "X-Payment-Amount": "-100", + "X-Payment-From": "0x1234567890abcdef1234567890abcdef12345678", + "X-Payment-To": "0xabcdef1234567890abcdef1234567890abcdef12", + "X-Payment-TxHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + } + + is_valid, error = self.verifier.verify_x402_headers(headers) + self.assertFalse(is_valid) + self.assertIn("positive", error) + + +class TestProgressTracking(unittest.TestCase): + """Test badge progress tracking.""" + + def setUp(self): + """Set up test fixtures.""" + self.verifier = A2ABadgeVerifier() + self.test_wallet = "0x1234567890abcdef1234567890abcdef12345678" + + def test_get_progress(self): + """Test getting progress toward a badge.""" + # Create 50 transactions + for i in range(50): + self.verifier.verify_transaction( + tx_hash=f"0x{'i' * 64}{i}", + from_wallet=self.test_wallet, + to_wallet=f"0x{i:040x}", + amount=10.0, + timestamp=datetime.now(), + protocol="x402", + block_height=1000 + i + ) + + progress = self.verifier.get_progress(self.test_wallet, "badge_a2a_trader") + + self.assertIsNotNone(progress) + self.assertEqual(progress.current_progress, 50) + self.assertEqual(progress.threshold, 100) + self.assertFalse(progress.earned) + + def test_get_progress_nonexistent_badge(self): + """Test getting progress for nonexistent badge.""" + progress = self.verifier.get_progress(self.test_wallet, "badge_nonexistent") + self.assertIsNone(progress) + + +class TestListAvailableBadges(unittest.TestCase): + """Test listing available badges.""" + + def setUp(self): + """Set up test fixtures.""" + self.verifier = A2ABadgeVerifier() + + def test_list_badges(self): + """Test listing all available badges.""" + badges = self.verifier.list_available_badges() + + self.assertGreater(len(badges), 0) + + # Check for expected badges + badge_ids = [b["nft_id"] for b in badges] + self.assertIn("badge_a2a_pioneer", badge_ids) + self.assertIn("badge_a2a_trader", badge_ids) + self.assertIn("badge_a2a_merchant", badge_ids) + self.assertIn("badge_a2a_whale", badge_ids) + + +def run_tests(): + """Run all tests.""" + loader = unittest.TestLoader() + suite = unittest.TestSuite() + + # Add all test classes + suite.addTests(loader.loadTestsFromTestCase(TestBadgeTier)) + suite.addTests(loader.loadTestsFromTestCase(TestA2ABadgeVerifier)) + suite.addTests(loader.loadTestsFromTestCase(TestBadgeEligibility)) + suite.addTests(loader.loadTestsFromTestCase(TestBadgeMetadataGeneration)) + suite.addTests(loader.loadTestsFromTestCase(TestWalletReport)) + suite.addTests(loader.loadTestsFromTestCase(TestX402HeaderValidation)) + suite.addTests(loader.loadTestsFromTestCase(TestProgressTracking)) + suite.addTests(loader.loadTestsFromTestCase(TestListAvailableBadges)) + + # Run tests + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + # Return exit code + return 0 if result.wasSuccessful() else 1 + + +if __name__ == "__main__": + sys.exit(run_tests()) diff --git a/tools/a2a_badge_verifier.py b/tools/a2a_badge_verifier.py new file mode 100644 index 00000000..09be5340 --- /dev/null +++ b/tools/a2a_badge_verifier.py @@ -0,0 +1,616 @@ +#!/usr/bin/env python3 +""" +A2A Transaction Badge Verification Tool + +This module provides verification and tracking for Agent-to-Agent (A2A) +transaction badges on the RustChain network using x402 protocol. + +Usage: + python a2a_badge_verifier.py verify + python a2a_badge_verifier.py progress + python a2a_badge_verifier.py list + python a2a_badge_verifier.py check-transaction +""" + +import json +import hashlib +import re +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Optional, Tuple, Any +from dataclasses import dataclass, field, asdict +from enum import Enum +import argparse + + +class BadgeTier(Enum): + """Badge rarity tiers.""" + MYTHIC = "Mythic" + LEGENDARY = "Legendary" + EPIC = "Epic" + RARE = "Rare" + UNCOMMON = "Uncommon" + COMMON = "Common" + + @property + def color(self) -> str: + colors = { + "Mythic": "#FF1493", # Deep Pink + "Legendary": "#FFD700", # Gold + "Epic": "#9370DB", # Purple + "Rare": "#4169E1", # Blue + "Uncommon": "#32CD32", # Green + "Common": "#C0C0C0", # Silver + } + return colors[self.value] + + @property + def stars(self) -> int: + stars = { + "Mythic": 6, + "Legendary": 5, + "Epic": 4, + "Rare": 3, + "Uncommon": 2, + "Common": 1, + } + return stars[self.value] + + +@dataclass +class A2ATransaction: + """Represents a verified A2A transaction.""" + tx_hash: str + from_wallet: str + to_wallet: str + amount: float + timestamp: datetime + protocol: str + block_height: int + verified: bool = True + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class BadgeCriteria: + """Criteria for earning a badge.""" + badge_id: str + title: str + criteria_type: str + threshold: int + current_progress: int = 0 + earned: bool = False + earned_timestamp: Optional[datetime] = None + description: str = "" + rarity: str = "" + + +@dataclass +class WalletStats: + """Statistics for a wallet's A2A activity.""" + wallet_address: str + total_transactions: int = 0 + total_volume: float = 0.0 + unique_counterparties: int = 0 + counterparties: List[str] = field(default_factory=list) + first_transaction: Optional[datetime] = None + last_transaction: Optional[datetime] = None + protocols_used: List[str] = field(default_factory=list) + monthly_volume: Dict[str, float] = field(default_factory=dict) + + +class A2ABadgeVerifier: + """ + Verifies A2A transactions and checks badge eligibility. + + This class provides methods to: + - Verify individual x402 transactions + - Track wallet statistics + - Check badge eligibility + - Generate badge metadata + """ + + def __init__(self, schema_path: Optional[str] = None): + """ + Initialize the verifier. + + Args: + schema_path: Path to the badge schema JSON file + """ + self.schema_path = schema_path or "schemas/relic_a2a_badges.json" + self.badges_schema = self._load_schema() + self.transaction_cache: Dict[str, A2ATransaction] = {} + self.wallet_stats: Dict[str, WalletStats] = {} + + def _load_schema(self) -> Dict: + """Load the badge schema from JSON file.""" + try: + with open(self.schema_path, 'r', encoding='utf-8') as f: + return json.load(f) + except FileNotFoundError: + print(f"Warning: Schema file not found at {self.schema_path}") + return {"badges": []} + except json.JSONDecodeError as e: + print(f"Error parsing schema: {e}") + return {"badges": []} + + def verify_x402_headers(self, headers: Dict[str, str]) -> Tuple[bool, str]: + """ + Verify x402 payment headers. + + Args: + headers: HTTP headers from the transaction + + Returns: + Tuple of (is_valid, error_message) + """ + required_headers = [ + 'X-Payment-Amount', + 'X-Payment-From', + 'X-Payment-To', + 'X-Payment-TxHash' + ] + + missing = [h for h in required_headers if h not in headers] + if missing: + return False, f"Missing required headers: {', '.join(missing)}" + + # Validate wallet address format (Base/Ethereum style) + from_addr = headers.get('X-Payment-From', '') + to_addr = headers.get('X-Payment-To', '') + + if not re.match(r'^0x[a-fA-F0-9]{40}$', from_addr): + return False, "Invalid X-Payment-From address format" + + if not re.match(r'^0x[a-fA-F0-9]{40}$', to_addr): + return False, "Invalid X-Payment-To address format" + + # Validate amount + try: + amount = float(headers['X-Payment-Amount']) + if amount <= 0: + return False, "Payment amount must be positive" + except ValueError: + return False, "Invalid payment amount format" + + # Validate tx hash + tx_hash = headers.get('X-Payment-TxHash', '') + if not re.match(r'^0x[a-fA-F0-9]{64}$', tx_hash): + return False, "Invalid transaction hash format" + + return True, "" + + def verify_transaction( + self, + tx_hash: str, + from_wallet: str, + to_wallet: str, + amount: float, + timestamp: datetime, + protocol: str = "x402", + block_height: int = 0 + ) -> A2ATransaction: + """ + Verify and record an A2A transaction. + + Args: + tx_hash: Transaction hash + from_wallet: Sender wallet address + to_wallet: Receiver wallet address + amount: Transaction amount + timestamp: Transaction timestamp + protocol: Payment protocol used + block_height: Block height when transaction occurred + + Returns: + Verified A2ATransaction object + """ + # Validate addresses + if not re.match(r'^0x[a-fA-F0-9]{40}$', from_wallet): + raise ValueError(f"Invalid from_wallet address: {from_wallet}") + + if not re.match(r'^0x[a-fA-F0-9]{40}$', to_wallet): + raise ValueError(f"Invalid to_wallet address: {to_wallet}") + + # Validate amount + if amount <= 0: + raise ValueError("Transaction amount must be positive") + + # Create transaction record + tx = A2ATransaction( + tx_hash=tx_hash, + from_wallet=from_wallet, + to_wallet=to_wallet, + amount=amount, + timestamp=timestamp, + protocol=protocol, + block_height=block_height + ) + + # Cache transaction + self.transaction_cache[tx_hash] = tx + + # Update wallet statistics + self._update_wallet_stats(from_wallet, tx, is_sender=True) + self._update_wallet_stats(to_wallet, tx, is_sender=False) + + return tx + + def _update_wallet_stats( + self, + wallet: str, + tx: A2ATransaction, + is_sender: bool + ): + """Update statistics for a wallet based on a transaction.""" + if wallet not in self.wallet_stats: + self.wallet_stats[wallet] = WalletStats(wallet_address=wallet) + + stats = self.wallet_stats[wallet] + stats.total_transactions += 1 + stats.total_volume += tx.amount + + # Track counterparties + counterparty = tx.to_wallet if is_sender else tx.from_wallet + if counterparty not in stats.counterparties: + stats.counterparties.append(counterparty) + stats.unique_counterparties = len(stats.counterparties) + + # Track timestamps + if stats.first_transaction is None or tx.timestamp < stats.first_transaction: + stats.first_transaction = tx.timestamp + if stats.last_transaction is None or tx.timestamp > stats.last_transaction: + stats.last_transaction = tx.timestamp + + # Track protocols + if tx.protocol not in stats.protocols_used: + stats.protocols_used.append(tx.protocol) + + # Track monthly volume + month_key = tx.timestamp.strftime("%Y-%m") + if month_key not in stats.monthly_volume: + stats.monthly_volume[month_key] = 0.0 + stats.monthly_volume[month_key] += tx.amount + + def get_wallet_stats(self, wallet_address: str) -> Optional[WalletStats]: + """Get statistics for a wallet.""" + return self.wallet_stats.get(wallet_address) + + def check_badge_eligibility(self, wallet_address: str) -> List[BadgeCriteria]: + """ + Check which badges a wallet is eligible for. + + Args: + wallet_address: Wallet to check + + Returns: + List of BadgeCriteria objects with eligibility status + """ + stats = self.wallet_stats.get(wallet_address) + if not stats: + return [] + + eligible_badges = [] + + for badge_def in self.badges_schema.get("badges", []): + criteria = self._check_single_badge(badge_def, stats) + eligible_badges.append(criteria) + + return eligible_badges + + def _check_single_badge( + self, + badge_def: Dict, + stats: WalletStats + ) -> BadgeCriteria: + """Check eligibility for a single badge.""" + criteria_def = badge_def.get("criteria", {}) + criteria_type = criteria_def.get("type", "") + threshold = criteria_def.get("threshold", 0) + + current_progress = 0 + earned = False + + # Check different criteria types + if criteria_type == "a2a_transactions": + current_progress = stats.total_transactions + earned = current_progress >= threshold + + elif criteria_type == "a2a_unique_counterparties": + current_progress = stats.unique_counterparties + earned = current_progress >= threshold + + elif criteria_type == "x402_exclusive": + # Check if only x402 protocol is used + if stats.protocols_used == ["x402"]: + current_progress = stats.total_transactions + earned = current_progress >= threshold + else: + current_progress = 0 + earned = False + + elif criteria_type == "multi_protocol": + current_progress = len(stats.protocols_used) + earned = current_progress >= threshold + + elif criteria_type == "a2a_monthly_volume_leader": + # This would require network-wide comparison + # For now, just show current monthly volume + current_month = datetime.now().strftime("%Y-%m") + current_progress = int(stats.monthly_volume.get(current_month, 0)) + earned = False # Would need network-wide check + + elif criteria_type == "first_a2a_transaction": + # Special case - would need network-wide tracking + current_progress = 1 if stats.total_transactions > 0 else 0 + earned = False # Would need genesis tracking + + return BadgeCriteria( + badge_id=badge_def.get("nft_id", ""), + title=badge_def.get("title", ""), + criteria_type=criteria_type, + threshold=threshold, + current_progress=current_progress, + earned=earned, + description=badge_def.get("description", ""), + rarity=badge_def.get("rarity", "") + ) + + def get_progress( + self, + wallet_address: str, + badge_id: str + ) -> Optional[BadgeCriteria]: + """ + Get progress toward a specific badge. + + Args: + wallet_address: Wallet to check + badge_id: Badge ID to check progress for + + Returns: + BadgeCriteria with progress info, or None if badge not found + """ + badge_def = None + for b in self.badges_schema.get("badges", []): + if b.get("nft_id") == badge_id: + badge_def = b + break + + if not badge_def: + return None + + stats = self.wallet_stats.get(wallet_address) + if not stats: + return None + + return self._check_single_badge(badge_def, stats) + + def generate_badge_metadata( + self, + badge_id: str, + wallet_address: str, + earned_timestamp: Optional[datetime] = None + ) -> Optional[Dict]: + """ + Generate NFT metadata for an earned badge. + + Args: + badge_id: Badge ID + wallet_address: Owner wallet + earned_timestamp: When badge was earned + + Returns: + Badge metadata dictionary + """ + badge_def = None + for b in self.badges_schema.get("badges", []): + if b.get("nft_id") == badge_id: + badge_def = b + break + + if not badge_def: + return None + + timestamp = earned_timestamp or datetime.utcnow() + + # Generate unique badge hash + badge_data = f"{badge_id}:{wallet_address}:{timestamp.isoformat()}" + badge_hash = hashlib.sha256(badge_data.encode()).hexdigest() + + metadata = { + "nft_id": badge_id, + "title": badge_def.get("title", ""), + "class": badge_def.get("class", ""), + "description": badge_def.get("description", ""), + "emotional_resonance": badge_def.get("emotional_resonance", {}), + "symbol": badge_def.get("symbol", ""), + "visual_anchor": badge_def.get("visual_anchor", ""), + "rarity": badge_def.get("rarity", ""), + "soulbound": badge_def.get("soulbound", True), + "owner": wallet_address, + "earned_timestamp": timestamp.isoformat() + "Z", + "badge_hash": badge_hash, + "version": "1.0" + } + + return metadata + + def list_available_badges(self) -> List[Dict]: + """List all available A2A badges.""" + return self.badges_schema.get("badges", []) + + def export_wallet_report(self, wallet_address: str) -> Dict: + """ + Export a comprehensive report for a wallet's A2A activity. + + Args: + wallet_address: Wallet to report on + + Returns: + Dictionary with full wallet report + """ + stats = self.wallet_stats.get(wallet_address) + if not stats: + return {"error": "Wallet not found"} + + eligibility = self.check_badge_eligibility(wallet_address) + earned_badges = [b for b in eligibility if b.earned] + pending_badges = [b for b in eligibility if not b.earned] + + report = { + "wallet_address": wallet_address, + "generated_at": datetime.utcnow().isoformat() + "Z", + "statistics": { + "total_transactions": stats.total_transactions, + "total_volume": stats.total_volume, + "unique_counterparties": stats.unique_counterparties, + "first_transaction": stats.first_transaction.isoformat() if stats.first_transaction else None, + "last_transaction": stats.last_transaction.isoformat() if stats.last_transaction else None, + "protocols_used": stats.protocols_used, + "monthly_volume": stats.monthly_volume + }, + "earned_badges": [asdict(b) for b in earned_badges], + "pending_badges": [asdict(b) for b in pending_badges], + "badge_progress_percentage": round( + len(earned_badges) / len(eligibility) * 100, 2 + ) if eligibility else 0 + } + + return report + + +def load_mock_transactions(verifier: A2ABadgeVerifier): + """Load mock transactions for testing/demo purposes.""" + import random + + wallets = [ + "0x1234567890abcdef1234567890abcdef12345678", + "0xabcdef1234567890abcdef1234567890abcdef12", + "0x9876543210fedcba9876543210fedcba98765432", + "0xfedcba9876543210fedcba9876543210fedcba98", + "0x5555555555555555555555555555555555555555", + ] + + base_time = datetime.now() - timedelta(days=30) + + for i in range(150): + from_wallet = random.choice(wallets) + to_wallet = random.choice([w for w in wallets if w != from_wallet]) + + tx = verifier.verify_transaction( + tx_hash=f"0x{''.join(random.choices('0123456789abcdef', k=64))}", + from_wallet=from_wallet, + to_wallet=to_wallet, + amount=round(random.uniform(1, 100), 2), + timestamp=base_time + timedelta(hours=i), + protocol="x402", + block_height=1000000 + i + ) + + print(f"Loaded {len(verifier.transaction_cache)} mock transactions") + + +def main(): + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="A2A Transaction Badge Verification Tool" + ) + subparsers = parser.add_subparsers(dest="command", help="Commands") + + # Verify command + verify_parser = subparsers.add_parser("verify", help="Verify wallet badges") + verify_parser.add_argument("wallet", help="Wallet address to check") + verify_parser.add_argument("--mock", action="store_true", help="Load mock transactions") + + # Progress command + progress_parser = subparsers.add_parser("progress", help="Check badge progress") + progress_parser.add_argument("wallet", help="Wallet address") + progress_parser.add_argument("badge_id", help="Badge ID to check") + + # List command + list_parser = subparsers.add_parser("list", help="List available badges") + + # Report command + report_parser = subparsers.add_parser("report", help="Generate wallet report") + report_parser.add_argument("wallet", help="Wallet address") + report_parser.add_argument("--output", "-o", help="Output file path") + + args = parser.parse_args() + + verifier = A2ABadgeVerifier() + + if args.command == "list": + badges = verifier.list_available_badges() + print(f"\nAvailable A2A Badges ({len(badges)} total):\n") + for badge in badges: + tier = badge.get("rarity", "Unknown") + title = badge.get("title", "Unknown") + badge_id = badge.get("nft_id", "") + symbol = badge.get("symbol", "") + print(f" {symbol} [{tier}] {title}") + print(f" ID: {badge_id}") + print(f" {badge.get('description', '')[:100]}...") + print() + + elif args.command == "verify": + if args.mock: + load_mock_transactions(verifier) + + eligibility = verifier.check_badge_eligibility(args.wallet) + if not eligibility: + print(f"No data for wallet: {args.wallet}") + print("Use --mock to load sample transactions") + return + + print(f"\nBadge Eligibility for {args.wallet}:\n") + earned = [b for b in eligibility if b.earned] + pending = [b for b in eligibility if not b.earned] + + if earned: + print(f"EARNED ({len(earned)}):") + for badge in earned: + print(f" βœ“ {badge.title} ({badge.rarity})") + + if pending: + print(f"\nPENDING ({len(pending)}):") + for badge in pending: + pct = round(badge.current_progress / badge.threshold * 100, 1) if badge.threshold > 0 else 0 + print(f" β—‹ {badge.title}: {badge.current_progress}/{badge.threshold} ({pct}%)") + + elif args.command == "progress": + if args.mock: + load_mock_transactions(verifier) + + progress = verifier.get_progress(args.wallet, args.badge_id) + if not progress: + print(f"Badge not found: {args.badge_id}") + return + + pct = round(progress.current_progress / progress.threshold * 100, 1) if progress.threshold > 0 else 0 + status = "βœ“ EARNED" if progress.earned else f"{pct}% complete" + + print(f"\nProgress: {progress.title}") + print(f"Status: {status}") + print(f"Progress: {progress.current_progress}/{progress.threshold}") + print(f"Description: {progress.description}") + + elif args.command == "report": + if args.mock: + load_mock_transactions(verifier) + + report = verifier.export_wallet_report(args.wallet) + + if args.output: + with open(args.output, 'w', encoding='utf-8') as f: + json.dump(report, f, indent=2) + print(f"Report saved to {args.output}") + else: + print(json.dumps(report, indent=2)) + + else: + parser.print_help() + + +if __name__ == "__main__": + main()