diff --git a/.temp_wip/cargo-trades.ts b/.temp_wip/cargo-trades.ts new file mode 100644 index 0000000..f6aada3 --- /dev/null +++ b/.temp_wip/cargo-trades.ts @@ -0,0 +1,210 @@ +/* + * CandiesTrade event handlers + * + * Tracks ERC-1155 cargo/drug trading events from the CandiesTrade contract: + * - TradeProposed: User proposes a targeted trade (specific amounts of tokens) + * - TradeAccepted: Target user accepts the trade + * - TradeCancelled: Proposer cancels the trade + * + * Contract: TBD (will be deployed from /mibera-contracts/honey-road) + */ + +import { CandiesTrade as CandiesTradeContract, CandiesTrade, TradeStats } from "generated"; + +const FIFTEEN_MINUTES = 15n * 60n; // 15 minutes in seconds + +/** + * Handle TradeProposed event + * Creates a new active cargo trade proposal + */ +export const handleCandiesTradeProposed = CandiesTradeContract.TradeProposed.handler( + async ({ event, context }) => { + const { + proposer, + tradeId, + offeredTokenId, + offeredAmount, + requestedTokenId, + requestedAmount, + requestedFrom, + timestamp, + } = event.params; + + const proposerLower = proposer.toLowerCase(); + const requestedFromLower = requestedFrom.toLowerCase(); + const timestampBigInt = BigInt(timestamp.toString()); + const expiresAt = timestampBigInt + FIFTEEN_MINUTES; + + // Create trade entity + // Use tx hash + log index for unique ID + const id = `${event.transaction.hash}_${event.logIndex}`; + + const trade: CandiesTrade = { + id, + tradeId: BigInt(tradeId.toString()), + offeredTokenId: BigInt(offeredTokenId.toString()), + offeredAmount: BigInt(offeredAmount.toString()), + requestedTokenId: BigInt(requestedTokenId.toString()), + requestedAmount: BigInt(requestedAmount.toString()), + proposer: proposerLower, + requestedFrom: requestedFromLower, + acceptor: undefined, // Null until accepted + status: "active", + proposedAt: timestampBigInt, + completedAt: undefined, // Null until completed + expiresAt, + txHash: event.transaction.hash, + blockNumber: BigInt(event.block.number), + chainId: event.chainId, + }; + + context.CandiesTrade.set(trade); + + // Update stats + await updateTradeStats(context, event.chainId, "candies_proposed"); + } +); + +/** + * Handle TradeAccepted event + * Marks cargo trade as completed + */ +export const handleCandiesTradeAccepted = CandiesTradeContract.TradeAccepted.handler( + async ({ event, context }) => { + const { + acceptor, + tradeId, + offeredTokenId, + offeredAmount, + requestedTokenId, + requestedAmount, + originalProposer, + } = event.params; + + const acceptorLower = acceptor.toLowerCase(); + const proposerLower = originalProposer.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + + // Use tx hash + log index for unique ID + const id = `${event.transaction.hash}_${event.logIndex}`; + + const trade: CandiesTrade = { + id, + tradeId: BigInt(tradeId.toString()), + offeredTokenId: BigInt(offeredTokenId.toString()), + offeredAmount: BigInt(offeredAmount.toString()), + requestedTokenId: BigInt(requestedTokenId.toString()), + requestedAmount: BigInt(requestedAmount.toString()), + proposer: proposerLower, + requestedFrom: acceptorLower, // The acceptor was the requested recipient + acceptor: acceptorLower, + status: "completed", + proposedAt: timestamp, // We don't have the original proposal time + completedAt: timestamp, + expiresAt: timestamp + FIFTEEN_MINUTES, + txHash: event.transaction.hash, + blockNumber: BigInt(event.block.number), + chainId: event.chainId, + }; + + context.CandiesTrade.set(trade); + + // Update stats + await updateTradeStats(context, event.chainId, "candies_completed"); + } +); + +/** + * Handle TradeCancelled event + * Marks cargo trade as cancelled + */ +export const handleCandiesTradeCancelled = CandiesTradeContract.TradeCancelled.handler( + async ({ event, context }) => { + const { + canceller, + tradeId, + offeredTokenId, + offeredAmount, + requestedTokenId, + requestedAmount, + } = event.params; + + const cancellerLower = canceller.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + + // Use tx hash + log index for unique ID + const id = `${event.transaction.hash}_${event.logIndex}`; + + const trade: CandiesTrade = { + id, + tradeId: BigInt(tradeId.toString()), + offeredTokenId: BigInt(offeredTokenId.toString()), + offeredAmount: BigInt(offeredAmount.toString()), + requestedTokenId: BigInt(requestedTokenId.toString()), + requestedAmount: BigInt(requestedAmount.toString()), + proposer: cancellerLower, + requestedFrom: cancellerLower, // Proposer is cancelling + acceptor: undefined, + status: "cancelled", + proposedAt: timestamp, // We don't have the original proposal time + completedAt: timestamp, + expiresAt: timestamp + FIFTEEN_MINUTES, + txHash: event.transaction.hash, + blockNumber: BigInt(event.block.number), + chainId: event.chainId, + }; + + context.CandiesTrade.set(trade); + + // Update stats + await updateTradeStats(context, event.chainId, "candies_cancelled"); + } +); + +/** + * Update global trade statistics + */ +async function updateTradeStats( + context: any, + chainId: number, + action: "candies_proposed" | "candies_completed" | "candies_cancelled" +): Promise { + const statsId = "global"; + + // Get existing stats or create new + let stats = await context.TradeStats.get(statsId); + + if (!stats) { + stats = { + id: statsId, + totalMiberaTrades: 0, + completedMiberaTrades: 0, + cancelledMiberaTrades: 0, + expiredMiberaTrades: 0, + totalCandiesTrades: 0, + completedCandiesTrades: 0, + cancelledCandiesTrades: 0, + expiredCandiesTrades: 0, + uniqueTraders: 0, + lastTradeTime: undefined, + chainId: chainId, + }; + } + + // Update stats based on action + const updatedStats: TradeStats = { + ...stats, + totalCandiesTrades: action === "candies_proposed" + ? stats.totalCandiesTrades + 1 + : stats.totalCandiesTrades, + completedCandiesTrades: action === "candies_completed" + ? stats.completedCandiesTrades + 1 + : stats.completedCandiesTrades, + cancelledCandiesTrades: action === "candies_cancelled" + ? stats.cancelledCandiesTrades + 1 + : stats.cancelledCandiesTrades, + lastTradeTime: BigInt(Date.now()), + }; + + context.TradeStats.set(updatedStats); +} diff --git a/.temp_wip/mibera-trades.ts b/.temp_wip/mibera-trades.ts new file mode 100644 index 0000000..592b6f8 --- /dev/null +++ b/.temp_wip/mibera-trades.ts @@ -0,0 +1,178 @@ +/* + * MiberaTrade event handlers + * + * Tracks ERC-721 NFT trading events from the MiberaTrade contract: + * - TradeProposed: User proposes a 1-for-1 NFT swap + * - TradeAccepted: Recipient accepts the trade + * - TradeCancelled: Proposer cancels the trade + * + * Contract: 0x90485B61C9dA51A3c79fca1277899d9CD5D350c2 (Berachain) + */ + +import { MiberaTrade as MiberaTradeContract, MiberaTrade, TradeStats } from "generated"; + +const FIFTEEN_MINUTES = 15n * 60n; // 15 minutes in seconds + +/** + * Handle TradeProposed event + * Creates a new active trade proposal + */ +export const handleMiberaTradeProposed = MiberaTradeContract.TradeProposed.handler( + async ({ event, context }) => { + const { proposer, offeredTokenId, requestedTokenId, timestamp } = event.params; + + const proposerLower = proposer.toLowerCase(); + const timestampBigInt = BigInt(timestamp.toString()); + const expiresAt = timestampBigInt + FIFTEEN_MINUTES; + + // Create trade entity + // Use offeredTokenId as part of ID since each NFT can only have one active trade + const tradeId = `${event.transaction.hash}_${offeredTokenId.toString()}`; + + const trade: MiberaTrade = { + id: tradeId, + offeredTokenId: BigInt(offeredTokenId.toString()), + requestedTokenId: BigInt(requestedTokenId.toString()), + proposer: proposerLower, + acceptor: undefined, // Null until accepted + status: "active", + proposedAt: timestampBigInt, + completedAt: undefined, // Null until completed + expiresAt, + txHash: event.transaction.hash, + blockNumber: BigInt(event.block.number), + chainId: event.chainId, + }; + + context.MiberaTrade.set(trade); + + // Update stats + await updateTradeStats(context, event.chainId, "mibera_proposed"); + } +); + +/** + * Handle TradeAccepted event + * Marks trade as completed + */ +export const handleMiberaTradeAccepted = MiberaTradeContract.TradeAccepted.handler( + async ({ event, context }) => { + const { acceptor, offeredTokenId, requestedTokenId, originalProposer } = event.params; + + const acceptorLower = acceptor.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + + // Find the trade by offeredTokenId + // Need to search for active trades with this offeredTokenId + // Since we don't have complex queries, we'll use a predictable ID pattern + // The trade was created with ID: tx_hash_offeredTokenId + // We don't know the original tx hash, so we'll create a new entity with completion data + + // For completed trades, we'll use the acceptance tx hash as ID + const tradeId = `${event.transaction.hash}_${offeredTokenId.toString()}`; + + const trade: MiberaTrade = { + id: tradeId, + offeredTokenId: BigInt(offeredTokenId.toString()), + requestedTokenId: BigInt(requestedTokenId.toString()), + proposer: originalProposer.toLowerCase(), + acceptor: acceptorLower, + status: "completed", + proposedAt: timestamp, // We don't have the original proposal time, use completion time + completedAt: timestamp, + expiresAt: timestamp + FIFTEEN_MINUTES, + txHash: event.transaction.hash, + blockNumber: BigInt(event.block.number), + chainId: event.chainId, + }; + + context.MiberaTrade.set(trade); + + // Update stats + await updateTradeStats(context, event.chainId, "mibera_completed"); + } +); + +/** + * Handle TradeCancelled event + * Marks trade as cancelled + */ +export const handleMiberaTradeCancelled = MiberaTradeContract.TradeCancelled.handler( + async ({ event, context }) => { + const { canceller, offeredTokenId, requestedTokenId } = event.params; + + const cancellerLower = canceller.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + + // Similar to acceptance, use cancellation tx hash as ID + const tradeId = `${event.transaction.hash}_${offeredTokenId.toString()}`; + + const trade: MiberaTrade = { + id: tradeId, + offeredTokenId: BigInt(offeredTokenId.toString()), + requestedTokenId: BigInt(requestedTokenId.toString()), + proposer: cancellerLower, + acceptor: undefined, + status: "cancelled", + proposedAt: timestamp, // We don't have the original proposal time + completedAt: timestamp, + expiresAt: timestamp + FIFTEEN_MINUTES, + txHash: event.transaction.hash, + blockNumber: BigInt(event.block.number), + chainId: event.chainId, + }; + + context.MiberaTrade.set(trade); + + // Update stats + await updateTradeStats(context, event.chainId, "mibera_cancelled"); + } +); + +/** + * Update global trade statistics + */ +async function updateTradeStats( + context: any, + chainId: number, + action: "mibera_proposed" | "mibera_completed" | "mibera_cancelled" +): Promise { + const statsId = "global"; + + // Get existing stats or create new + let stats = await context.TradeStats.get(statsId); + + if (!stats) { + stats = { + id: statsId, + totalMiberaTrades: 0, + completedMiberaTrades: 0, + cancelledMiberaTrades: 0, + expiredMiberaTrades: 0, + totalCandiesTrades: 0, + completedCandiesTrades: 0, + cancelledCandiesTrades: 0, + expiredCandiesTrades: 0, + uniqueTraders: 0, + lastTradeTime: undefined, + chainId: chainId, + }; + } + + // Update stats based on action + const updatedStats: TradeStats = { + ...stats, + totalMiberaTrades: action === "mibera_proposed" + ? stats.totalMiberaTrades + 1 + : stats.totalMiberaTrades, + completedMiberaTrades: action === "mibera_completed" + ? stats.completedMiberaTrades + 1 + : stats.completedMiberaTrades, + cancelledMiberaTrades: action === "mibera_cancelled" + ? stats.cancelledMiberaTrades + 1 + : stats.cancelledMiberaTrades, + lastTradeTime: BigInt(Date.now()), + }; + + context.TradeStats.set(updatedStats); +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..187e8ea --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,23 @@ +# THJ Envio - Claude Code Guide + +**Purpose**: Blockchain indexer for THJ ecosystem + +## Tech Stack + +Envio 2.27.3, TypeScript, Ethers v6, Node v20, pnpm + +## Skills + +- `envio-patterns` (framework constraints, handler patterns, quest integration) +- `thj-ecosystem-overview` (cross-brand architecture) + +## Quick Commands + +```bash +pnpm codegen # After schema changes +pnpm tsc --noEmit +TUI_OFF=true pnpm dev +pnpm deploy +``` + +**For Envio patterns**: Use `envio-patterns` skill (immutability, indexed actions, etc.). diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..7fe765d --- /dev/null +++ b/DEPLOYMENT_GUIDE.md @@ -0,0 +1,166 @@ +# Indexer Deployment Guide + +## Problem: Historical Tarot Mints Not Indexed + +### Root Cause +The tarot contract (0x4B08a069381EfbB9f08C73D6B2e975C9BE3c4684) was added to the GeneralMints handler AFTER users had already minted. The indexer needs to reprocess historical blocks to capture these mints. + +### User Details +- **Address**: 0xd4920bb5a6c032eb3bce21e0c7fdac9eefa8d3f1 +- **Transaction**: 0xb5ff5e83e337e801e3c0e0e0cfb10752acad01c6b9f931260839f10fa56becf0 +- **Block**: 12,313,339 +- **Date**: Oct 27, 2025 03:21 AM UTC + +### Current Status +✅ Config is correct (commit 0879693) +✅ New tarot mints are being captured +❌ Historical mints before deployment are NOT captured + +--- + +## Solution 1: Reset HyperIndex Deployment (RECOMMENDED) + +### Steps: +1. Go to https://hosted.envio.dev +2. Log in with your Envio account +3. Find deployment ID: `029ffba` +4. Click "Reset" or "Redeploy from Start Block" +5. Wait for sync to complete (may take 30-60 minutes) + +### Verification: +```bash +# Check if user's mint is now indexed +curl -X POST 'https://indexer.hyperindex.xyz/029ffba/v1/graphql' \ + -H 'Content-Type: application/json' \ + -d '{ + "query": "query { Action(where: { txHash: { _eq: \"0xb5ff5e83e337e801e3c0e0e0cfb10752acad01c6b9f931260839f10fa56becf0\" } }) { id actor actionType primaryCollection timestamp } }" + }' | jq +``` + +Expected: Should return the mint event with `actionType: "mint"` and `primaryCollection: "mibera_tarot"` + +--- + +## Solution 2: Test Locally Before Production Reset + +### 1. Start Local Indexer +```bash +cd /Users/zksoju/Documents/GitHub/thj-api/thj-envio +TUI_OFF=true pnpm dev +``` + +This will: +- Start local indexer on http://localhost:8080/v1/graphql +- Process from start_block: 866,405 +- Capture the user's mint at block 12,313,339 + +### 2. Wait for Sync +Monitor logs until you see: +``` +Syncing block 12,313,339... +``` + +Or check current sync status: +```bash +curl -X POST 'http://localhost:8080/v1/graphql' \ + -H 'Content-Type: application/json' \ + -d '{"query": "query { Action(order_by: {timestamp: desc}, limit: 1) { timestamp } }"}' | jq +``` + +### 3. Test Query +Once synced past block 12,313,339: +```bash +curl -X POST 'http://localhost:8080/v1/graphql' \ + -H 'Content-Type: application/json' \ + -d '{ + "query": "query { Action(where: { actor: { _eq: \"0xd4920bb5a6c032eb3bce21e0c7fdac9eefa8d3f1\" }, actionType: { _eq: \"mint\" }, primaryCollection: { _eq: \"mibera_tarot\" } }) { id actor timestamp } }" + }' | jq +``` + +Expected: Should return the user's mint + +### 4. Test CubQuests Locally +```bash +cd /Users/zksoju/Documents/GitHub/thj-api/cubquests-interface + +# Update .env.local to use local indexer +echo "NEXT_PUBLIC_GRAPHQL_ENDPOINT=http://localhost:8080/v1/graphql" >> .env.local + +npm run dev +``` + +Visit http://localhost:3001/quests/harbor-initiation and test verification. + +--- + +## Solution 3: Temporary Workaround (NOT RECOMMENDED) + +Ask the user to mint another tarot NFT. The new mint will be captured by the current indexer configuration. + +**Downsides:** +- Costs gas +- Doesn't solve the problem for other users +- Only a band-aid fix + +--- + +## After Reset: Update Documentation + +Once the reset is complete and verified: + +1. Update `cubquests-interface/docs/TAROT_MINT_VERIFICATION_TROUBLESHOOTING.md`: + - Change status to "RESOLVED" + - Document the solution + - Note the reset date/time + +2. Test with the failing user: + - Address: 0xd4920bb5a6c032eb3bce21e0c7fdac9eefa8d3f1 + - Quest: Harbor Initiation, Step 2 + - Expected: Verification succeeds ✅ + +--- + +## Prevention for Future Contract Additions + +When adding new contracts to handlers mid-stream: + +1. ✅ Update config.yaml +2. ✅ Update handler constants +3. ✅ Commit changes +4. ⚠️ **IMPORTANT**: Reset indexer to reprocess from start_block +5. ✅ Verify historical events are captured +6. ✅ Deploy to production + +**Rule**: Any contract added after initial deployment requires an indexer reset to capture historical events. + +--- + +## Quick Reference + +### Production Indexer +- **URL**: https://indexer.hyperindex.xyz/029ffba/v1/graphql +- **Deployment ID**: 029ffba +- **Start Block**: 866,405 +- **Chain**: Berachain Mainnet (80094) + +### Key Commits +- **0879693**: Add mibera_tarot to GeneralMints handler +- **4f3becc7**: Update quest to use mibera_tarot collection + +### Test Queries +```bash +# Check all tarot mints +curl -X POST 'https://indexer.hyperindex.xyz/029ffba/v1/graphql' \ + -H 'Content-Type: application/json' \ + -d '{"query": "query { Action(where: { primaryCollection: { _eq: \"mibera_tarot\" }, actionType: { _eq: \"mint\" } }, limit: 10) { id actor timestamp } }"}' | jq + +# Check specific user +curl -X POST 'https://indexer.hyperindex.xyz/029ffba/v1/graphql' \ + -H 'Content-Type: application/json' \ + -d '{"query": "query { Action(where: { actor: { _eq: \"0xd4920bb5a6c032eb3bce21e0c7fdac9eefa8d3f1\" }, actionType: { _eq: \"mint\" } }) { id actionType primaryCollection timestamp } }"}' | jq +``` + +--- + +**Status**: Awaiting HyperIndex reset to capture historical tarot mints +**Next Action**: Log in to hosted.envio.dev and reset deployment 029ffba diff --git a/config.sf-vaults.yaml b/config.sf-vaults.yaml new file mode 100644 index 0000000..ca7077c --- /dev/null +++ b/config.sf-vaults.yaml @@ -0,0 +1,61 @@ +# yaml-language-server: $schema=./node_modules/envio/evm.schema.json +# Minimal config for testing SF Vaults only +name: thj-indexer-sf-vaults +contracts: + # Set & Forgetti Vaults - ERC4626 vaults + - name: SFVaultERC4626 + handler: src/SFVaultHandlers.ts + events: + - event: Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares) + field_selection: + transaction_fields: + - hash + - event: Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares) + field_selection: + transaction_fields: + - hash + - event: StrategyUpdated(address indexed oldStrategy, address indexed newStrategy) + field_selection: + transaction_fields: + - hash + # Set & Forgetti MultiRewards - Staking and reward distribution + - name: SFMultiRewards + handler: src/SFVaultHandlers.ts + events: + - event: Staked(address indexed user, uint256 amount) + field_selection: + transaction_fields: + - hash + - event: Withdrawn(address indexed user, uint256 amount) + field_selection: + transaction_fields: + - hash + - event: RewardPaid(address indexed user, address indexed rewardsToken, uint256 reward) + field_selection: + transaction_fields: + - hash + +networks: + # Berachain Mainnet only + - id: 80094 + start_block: 13869572 # SF vaults deployment block + contracts: + # Set & Forgetti Vaults (ERC4626) + - name: SFVaultERC4626 + address: + - 0x4b8e4C84901C8404F4cfe438A33ee9Ef72F345d1 # HLKD1B Vault + - 0x962D17044fB34abbF523F6bff93D05c0214d7BB3 # HLKD690M Vault + - 0xa51Dd612F0A03cBc81652078f631fb5F7081ff0F # HLKD420M Vault + - 0xb7411DdE748Fb6D13cE04B9aac5E1fEa8AD264dD # HLKD330M Vault + - 0x6552e503dfC5103BB31a3fE96Ac3c3a092607f36 # HLKD100M Vault + # Set & Forgetti MultiRewards (Staking) + - name: SFMultiRewards + address: + - 0xBfdA8746f8ABeE58a58F87C1D2BB2d9eEE6e3554 # HLKD1B MultiRewards + - 0x01c1C9c333Ea81e422E421Db63030e882851EB3d # HLKD690M MultiRewards + - 0x4EEdEe17CDFbd9910C421ecc9d3401C70C0BF624 # HLKD420M MultiRewards + - 0xec204cb71D69f1b4d334C960D16a68364B604857 # HLKD330M MultiRewards + - 0x00192Ce353151563B3bd8664327d882c7ac45CB8 # HLKD100M MultiRewards + +unordered_multichain_mode: false +preload_handlers: true diff --git a/config.yaml b/config.yaml index de39486..ac329ae 100644 --- a/config.yaml +++ b/config.yaml @@ -1,14 +1,44 @@ # yaml-language-server: $schema=./node_modules/envio/evm.schema.json -name: envio-indexer +name: thj-indexer contracts: - name: HoneyJar handler: src/EventHandlers.ts events: - - event: Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) - - event: ApprovalForAll(address indexed owner, address indexed operator, bool approved) - - event: BaseURISet(string uri) - - event: OwnershipTransferred(address indexed previousOwner, address indexed newOwner) - - event: SetGenerated(bool generated) + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + - name: HoneyJar2Eth + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + - name: HoneyJar3Eth + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + - name: HoneyJar4Eth + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + - name: HoneyJar5Eth + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + - name: Honeycomb + handler: src/EventHandlers.ts + events: - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) field_selection: transaction_fields: @@ -36,53 +66,566 @@ contracts: field_selection: transaction_fields: - hash + # Aquabera Forwarder for wall tracking + - name: AquaberaVault + handler: src/EventHandlers.ts + events: + # Track deposits through the forwarder (DepositForwarded event) + - event: DepositForwarded(address indexed sender, address indexed vault, address indexed token, uint256 amount, uint256 shares, address to) + field_selection: + transaction_fields: + - hash + # Direct Aquabera Vault events (for wall contract and other direct deposits) + - name: AquaberaVaultDirect + handler: src/EventHandlers.ts + events: + # Track direct deposits to vault (Uniswap V3 style pool) + - event: Deposit(address indexed sender, address indexed to, uint256 shares, uint256 amount0, uint256 amount1) + field_selection: + transaction_fields: + - hash + - from + # Track withdrawals from vault (Uniswap V3 style pool) + - event: Withdraw(address indexed sender, address indexed to, uint256 shares, uint256 amount0, uint256 amount1) + field_selection: + transaction_fields: + - hash + - from + # Crayons Factory emits new ERC721 collection deployments + - name: CrayonsFactory + handler: src/EventHandlers.ts + events: + - event: Factory__NewERC721Base(address indexed owner, address erc721Base) + field_selection: + transaction_fields: + - hash + # Crayons ERC721 collections emit transfers for holder tracking + - name: CrayonsCollection + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + # Static ERC721 collections for holder tracking + - name: TrackedErc721 + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + # General ERC721 mint tracking (mint events only) + - name: GeneralMints + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + # VM-specific: Capture encoded trait data from Minted event + - event: Minted(address indexed user, uint256 tokenId, string traits) + field_selection: + transaction_fields: + - hash + # Mibera staking tracking (PaddleFi & Jiko deposits/withdrawals) + - name: MiberaStaking + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + # PaddleFi lending tracking (BERA supply + NFT pawn) + - name: PaddleFi + handler: src/EventHandlers.ts + events: + # Mint = Supply BERA (lender deposits BERA, receives pTokens) + - event: Mint(address minter, uint256 mintAmount, uint256 mintTokens) + field_selection: + transaction_fields: + - hash + # Pawn = Deposit NFT as collateral (borrower pawns Mibera NFTs) + - event: Pawn(address borrower, uint256[] nftIds) + field_selection: + transaction_fields: + - hash + - name: CandiesMarket1155 + handler: src/EventHandlers.ts + events: + - event: TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) + field_selection: + transaction_fields: + - hash + - event: TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) + field_selection: + transaction_fields: + - hash + - name: CubBadges1155 + handler: src/EventHandlers.ts + events: + - event: TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) + field_selection: + transaction_fields: + - hash + - event: TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) + field_selection: + transaction_fields: + - hash + # MiberaTrade - ERC721 NFT trading contract + # MiberaTrade - Commented out until handlers are implemented + # - name: MiberaTrade + # handler: src/EventHandlers.ts + # events: + # - event: TradeProposed(address indexed proposer, uint256 indexed offeredTokenId, uint256 indexed requestedTokenId, uint256 timestamp) + # field_selection: + # transaction_fields: + # - hash + # - event: TradeAccepted(address indexed acceptor, uint256 indexed offeredTokenId, uint256 indexed requestedTokenId, address originalProposer) + # field_selection: + # transaction_fields: + # - hash + # - event: TradeCancelled(address indexed canceller, uint256 indexed offeredTokenId, uint256 indexed requestedTokenId) + # field_selection: + # transaction_fields: + # - hash + # CandiesTrade - ERC1155 Cargo/Drug trading contract - Commented out until handlers are implemented + # - name: CandiesTrade + # handler: src/EventHandlers.ts + # events: + # - event: TradeProposed(address indexed proposer, uint256 indexed tradeId, uint256 offeredTokenId, uint256 offeredAmount, uint256 requestedTokenId, uint256 requestedAmount, address indexed requestedFrom, uint256 timestamp) + # field_selection: + # transaction_fields: + # - hash + # - event: TradeAccepted(address indexed acceptor, uint256 indexed tradeId, uint256 offeredTokenId, uint256 offeredAmount, uint256 requestedTokenId, uint256 requestedAmount, address originalProposer) + # field_selection: + # transaction_fields: + # - hash + # - event: TradeCancelled(address indexed canceller, uint256 indexed tradeId, uint256 offeredTokenId, uint256 offeredAmount, uint256 requestedTokenId, uint256 requestedAmount) + # field_selection: + # transaction_fields: + # - hash + # MiberaPremint - Tracks participation and refunds in Mibera premint + - name: MiberaPremint + handler: src/EventHandlers.ts + events: + - event: Participated(uint256 indexed phase, address indexed user, uint256 amount) + field_selection: + transaction_fields: + - hash + - event: Refunded(uint256 indexed phase, address indexed user, uint256 amount) + field_selection: + transaction_fields: + - hash + # MiberaSets - ERC1155 Sets collection on Optimism (airdropped from distribution wallet) + - name: MiberaSets + handler: src/EventHandlers.ts + events: + - event: TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) + field_selection: + transaction_fields: + - hash + - event: TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) + field_selection: + transaction_fields: + - hash + # MiberaZora1155 - ERC1155 collection on Optimism (Zora platform) + - name: MiberaZora1155 + handler: src/EventHandlers.ts + events: + - event: TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) + field_selection: + transaction_fields: + - hash + - event: TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) + field_selection: + transaction_fields: + - hash + # FriendtechShares - friend.tech key trading on Base (tracking Mibera-related subjects) + - name: FriendtechShares + handler: src/EventHandlers.ts + events: + - event: Trade(address trader, address subject, bool isBuy, uint256 shareAmount, uint256 ethAmount, uint256 protocolEthAmount, uint256 subjectEthAmount, uint256 supply) + field_selection: + transaction_fields: + - hash + # MiladyCollection - Milady NFT burn tracking on Ethereum mainnet + - name: MiladyCollection + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + # MiberaTreasury - Treasury backing/marketplace for defaulted NFTs + - name: MiberaTreasury + handler: src/EventHandlers.ts + events: + # Loan lifecycle events + - event: LoanReceived(uint256 loanId, uint256[] ids, uint256 amount, uint256 expiry) + field_selection: + transaction_fields: + - hash + - from + - event: BackingLoanPayedBack(uint256 loanId, uint256 newTotalBacking) + field_selection: + transaction_fields: + - hash + - event: BackingLoanExpired(uint256 loanId, uint256 newTotalBacking) + field_selection: + transaction_fields: + - hash + - event: ItemLoaned(uint256 loanId, uint256 itemId, uint256 expiry) + field_selection: + transaction_fields: + - hash + - from + - event: LoanItemSentBack(uint256 loanId, uint256 newTotalBacking) + field_selection: + transaction_fields: + - hash + - event: ItemLoanExpired(uint256 loanId, uint256 newTotalBacking) + field_selection: + transaction_fields: + - hash + # Marketplace events + - event: ItemPurchased(uint256 itemId, uint256 newTotalBacking) + field_selection: + transaction_fields: + - hash + - from + - event: ItemRedeemed(uint256 itemId, uint256 newTotalBacking) + field_selection: + transaction_fields: + - hash + - from + - event: RFVChanged(uint256 indexed newRFV) + field_selection: + transaction_fields: + - hash + # MiberaCollection - Transfer tracking for mint activity + - name: MiberaCollection + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + field_selection: + transaction_fields: + - hash + - value + # Seaport - OpenSea marketplace for secondary sales tracking + - name: Seaport + handler: src/EventHandlers.ts + events: + - event: OrderFulfilled(bytes32 orderHash, address indexed offerer, address indexed zone, address recipient, (uint8,address,uint256,uint256)[] offer, (uint8,address,uint256,uint256,address)[] consideration) + field_selection: + transaction_fields: + - hash + - name: FatBera + handler: src/EventHandlers.ts + events: + - event: Deposit(address indexed from, address indexed to, uint256 amount, uint256 shares) + field_selection: + transaction_fields: + - hash + - from + - name: BgtToken + handler: src/EventHandlers.ts + events: + - event: QueueBoost(address indexed account, bytes indexed pubkey, uint128 amount) + field_selection: + transaction_fields: + - hash + - from + - input + # Set & Forgetti Vaults - ERC4626 vaults + - name: SFVaultERC4626 + handler: src/EventHandlers.ts + events: + - event: Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares) + field_selection: + transaction_fields: + - hash + - event: Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares) + field_selection: + transaction_fields: + - hash + - event: StrategyUpdated(address indexed oldStrategy, address indexed newStrategy) + field_selection: + transaction_fields: + - hash + # Set & Forgetti MultiRewards - Staking and reward distribution + - name: SFMultiRewards + handler: src/EventHandlers.ts + events: + - event: Staked(address indexed user, uint256 amount) + field_selection: + transaction_fields: + - hash + - event: Withdrawn(address indexed user, uint256 amount) + field_selection: + transaction_fields: + - hash + - event: RewardPaid(address indexed user, address indexed rewardsToken, uint256 reward) + field_selection: + transaction_fields: + - hash + # HenloVault for tracking HENLOCKED token mints AND Henlocker vault system + - name: HenloVault + handler: src/EventHandlers.ts + events: + # Original Mint event for HENLOCKED token tracking + - event: Mint(address indexed user, uint256 indexed strike, uint256 amount) + field_selection: + transaction_fields: + - hash + # Henlocker vault events + - event: RoundOpened(uint48 indexed epochId, uint64 indexed strike, uint256 depositLimit) + field_selection: + transaction_fields: + - hash + - event: RoundClosed(uint48 indexed epochId, uint64 indexed strike) + field_selection: + transaction_fields: + - hash + - event: DepositsPaused(uint48 indexed epochId, uint64 indexed strike) + field_selection: + transaction_fields: + - hash + - event: DepositsUnpaused(uint48 indexed epochId, uint64 indexed strike) + field_selection: + transaction_fields: + - hash + - event: MintFromReservoir(address indexed reservoir, uint64 indexed strike, uint256 amount) + field_selection: + transaction_fields: + - hash + - event: Redeem(address indexed user, uint64 indexed strike, uint256 amount) + field_selection: + transaction_fields: + - hash + - event: ReservoirSet(uint48 indexed epochId, uint64 indexed strike, address indexed reservoir) + field_selection: + transaction_fields: + - hash + # Tracked ERC-20 tokens for balance + burn tracking (HENLO + HENLOCKED tiers) + - name: TrackedErc20 + handler: src/EventHandlers.ts + events: + - event: Transfer(address indexed from, address indexed to, uint256 value) + field_selection: + transaction_fields: + - hash + - from # Required for burn tracking + - to # Required for source detection + networks: + # Ethereum Mainnet - id: 1 - start_block: 0 + start_block: 13090020 # Earliest block - Milady contract deployment (was 16751283 for Honeycomb) contracts: + # Native HoneyJar contracts on Ethereum - name: HoneyJar address: - - 0xa20cf9b0874c3e46b344deaaea9c2e0c3e1db37d - - 0x98dc31a9648f04e23e4e36b0456d1951531c2a05 + - 0xa20cf9b0874c3e46b344deaeea9c2e0c3e1db37d # HoneyJar1 + - 0x98dc31a9648f04e23e4e36b0456d1951531c2a05 # HoneyJar6 + # Honeycomb on Ethereum + - name: Honeycomb + address: - 0xcb0477d1af5b8b05795d89d59f4667b59eae9244 + # Layer Zero reminted HoneyJar contracts on Ethereum + - name: HoneyJar2Eth + address: + - 0x3f4dd25ba6fb6441bfd1a869cbda6a511966456d # HoneyJar2 L0 remint + - name: HoneyJar3Eth + address: + - 0x49f3915a52e137e597d6bf11c73e78c68b082297 # HoneyJar3 L0 remint (was missing!) + - name: HoneyJar4Eth + address: + - 0x0b820623485dcfb1c40a70c55755160f6a42186d # HoneyJar4 L0 remint (was missing!) + - name: HoneyJar5Eth + address: + - 0x39eb35a84752b4bd3459083834af1267d276a54c # HoneyJar5 L0 remint (was missing!) + # Milady NFT collection on Ethereum (burn tracking) + - name: MiladyCollection + address: + - 0x5af0d9827e0c53e4799bb226655a1de152a425a5 # Milady Maker + + # Arbitrum - id: 42161 - start_block: 0 + start_block: 102894033 contracts: - name: HoneyJar address: - - 0x1b2751328f41d1a0b91f3710edcd33e996591b72 + - 0x1b2751328f41d1a0b91f3710edcd33e996591b72 # HoneyJar2 + + # Zora - id: 7777777 - start_block: 0 + start_block: 18071873 contracts: - name: HoneyJar address: - - 0xe798c4d40bc050bc93c7f3b149a0dfe5cfc49fb0 + - 0xe798c4d40bc050bc93c7f3b149a0dfe5cfc49fb0 # HoneyJar3 + + # Optimism - id: 10 - start_block: 0 + start_block: 125031052 # MiberaSets contract creation block (0x886d...) - Sept 6, 2024 contracts: - name: HoneyJar address: - - 0xe1d16cc75c9f39a2e0f5131eb39d4b634b23f301 + - 0xe1d16cc75c9f39a2e0f5131eb39d4b634b23f301 # HoneyJar4 + # Mibera Sets - ERC1155 collection (token IDs 8-11 = Strong Set, 12 = Super Set) + - name: MiberaSets + address: + - 0x886d2176d899796cd1affa07eff07b9b2b80f1be + # Mibera Zora - ERC1155 collection on Optimism (Zora platform) + - name: MiberaZora1155 + address: + - 0x427a8f2e608e185eece69aca15e535cd6c36aad8 # mibera_zora + + # Base - id: 8453 - start_block: 0 + start_block: 2430439 # friend.tech start block (earliest contract) contracts: - name: HoneyJar address: - - 0xbad7b49d985bbfd3a22706c447fb625a28f048b4 + - 0xbad7b49d985bbfd3a22706c447fb625a28f048b4 # HoneyJar5 + # friend.tech shares trading (Mibera-related subjects: jani key, charlotte fang key) + - name: FriendtechShares + address: + - 0xCF205808Ed36593aa40a44F10c7f7C2F67d4A4d4 + + # Berachain Mainnet (DO NOT CHANGE THIS ID) - id: 80094 - start_block: 0 + start_block: 866405 # Using the start block from the HoneyJar contracts (SF vaults use 12134222 for earliest deployment) contracts: + # AquaberaVault forwarder on Berachain Mainnet + - name: AquaberaVault + address: + - 0xc0c6D4178410849eC9765B4267A73F4F64241832 # Aquabera forwarder (user deposits through UI) + # Direct vault contract for wall deposits and withdrawals + - name: AquaberaVaultDirect + address: + - 0x04fD6a7B02E2e48caedaD7135420604de5f834f8 # Aquabera HENLO/BERA vault (direct deposits/withdrawals) + # HoneyJar contracts on Berachain Mainnet - name: HoneyJar address: - - 0xedc5dfd6f37464cc91bbce572b6fe2c97f1bc7b3 - - 0x1c6c24cac266c791c4ba789c3ec91f04331725bd - - 0xf1e4a550772fabfc35b28b51eb8d0b6fcd1c4878 - - 0xdb602ab4d6bd71c8d11542a9c8c936877a9a4f45 - - 0x0263728e7f59f315c17d3c180aeade027a375f17 - - 0xb62a9a21d98478f477e134e175fd2003c15cb83a - - 0x886d2176d899796cd1affa07eff07b9b2b80f1be + - 0xedc5dfd6f37464cc91bbce572b6fe2c97f1bc7b3 # HoneyJar1 Bera + - 0x1c6c24cac266c791c4ba789c3ec91f04331725bd # HoneyJar2 Bera + - 0xf1e4a550772fabfc35b28b51eb8d0b6fcd1c4878 # HoneyJar3 Bera + - 0xdb602ab4d6bd71c8d11542a9c8c936877a9a4f45 # HoneyJar4 Bera + - 0x0263728e7f59f315c17d3c180aeade027a375f17 # HoneyJar5 Bera + - 0xb62a9a21d98478f477e134e175fd2003c15cb83a # HoneyJar6 Bera + # Honeycomb on Berachain Mainnet + - name: Honeycomb + address: + - 0x886d2176d899796cd1affa07eff07b9b2b80f1be # Honeycomb Bera + # MoneycombVault on Berachain Mainnet - name: MoneycombVault address: - 0x9279b2227b57f349a0ce552b25af341e735f6309 + + # Crayons Factory (deploys ERC721 Base collections) + - name: CrayonsFactory + address: + - 0xF1c7d49B39a5aCa29ead398ad9A7024ed6837F87 + + # Crayons ERC721 Collections (Transfer indexing) + - name: CrayonsCollection + address: [] + # Static tracked ERC721 collections + - name: TrackedErc721 + address: + - 0x6666397DFe9a8c469BF65dc744CB1C733416c420 # mibera holders + - 0x4B08a069381EfbB9f08C73D6B2e975C9BE3c4684 # tarot + - 0x86Db98cf1b81E833447b12a077ac28c36b75c8E1 # miparcels + - 0x8D4972bd5D2df474e71da6676a365fB549853991 # miladies + - 0x144B27b1A267eE71989664b3907030Da84cc4754 # mireveal_1_1 + - 0x72DB992E18a1bf38111B1936DD723E82D0D96313 # mireveal_2_2 + - 0x3A00301B713be83EC54B7B4Fb0f86397d087E6d3 # mireveal_3_3 + - 0x419F25C4f9A9c730AAcf58b8401B5b3e566Fe886 # mireveal_4_20 + - 0x81A27117bd894942BA6737402fB9e57e942C6058 # mireveal_5_5 + - 0xaaB7b4502251aE393D0590bAB3e208E2d58F4813 # mireveal_6_6 + - 0xc64126EA8dC7626c16daA2A29D375C33fcaa4C7c # mireveal_7_7 + - 0x24F4047d372139de8DACbe79e2fC576291Ec3ffc # mireveal_8_8 + # General ERC721 Mint tracking (quest/missions) + - name: GeneralMints + address: + - 0x048327A187b944ddac61c6e202BfccD20d17c008 + - 0x230945E0Ed56EF4dE871a6c0695De265DE23D8D8 # mibera_gif + # NOTE: mibera_tarot handled by TrackedErc721 (which now creates mint actions too) + # Mibera staking tracking - REMOVED: Now handled by TrackedErc721 handler + # (was causing handler conflict where TrackedHolder entries were never created) + # PaddleFi lending - BERA supply + NFT pawn tracking + - name: PaddleFi + address: + - 0x242b7126F3c4E4F8CbD7f62571293e63E9b0a4E1 # PaddleFi MIBERA-WBERA vault + - name: CandiesMarket1155 + address: + - 0x80283fbF2b8E50f6Ddf9bfc4a90A8336Bc90E38F + - 0xeca03517c5195f1edd634da6d690d6c72407c40c + # MiberaTrade and CandiesTrade contracts commented out until handlers are implemented + # - name: MiberaTrade + # address: + # - 0x90485B61C9dA51A3c79fca1277899d9CD5D350c2 # NFT trading contract + # - name: CandiesTrade + # address: [] + # # TODO: Add address after deployment + # # Contract will be deployed from /mibera-contracts/honey-road + - name: CubBadges1155 + address: + - 0x574617ab9788e614b3eb3f7bd61334720d9e1aac # Cub Universal Badges (mainnet) + - name: FatBera + address: + - 0xBAE11292a3E693AF73651BDa350d752AE4A391d4 + - name: BgtToken + address: + - 0x656b95E550C07a9ffe548Bd4085c72418Ceb1dBa + # Set & Forgetti Vaults (ERC4626) + - name: SFVaultERC4626 + address: + - 0x4b8e4C84901C8404F4cfe438A33ee9Ef72F345d1 # HLKD1B Vault + - 0x962D17044fB34abbF523F6bff93D05c0214d7BB3 # HLKD690M Vault + - 0xa51Dd612F0A03cBc81652078f631fb5F7081ff0F # HLKD420M Vault + - 0xb7411DdE748Fb6D13cE04B9aac5E1fEa8AD264dD # HLKD330M Vault + - 0x6552e503dfC5103BB31a3fE96Ac3c3a092607f36 # HLKD100M Vault + # Set & Forgetti MultiRewards (Staking) + - name: SFMultiRewards + address: + - 0xBfdA8746f8ABeE58a58F87C1D2BB2d9eEE6e3554 # HLKD1B MultiRewards + - 0x01c1C9c333Ea81e422E421Db63030e882851EB3d # HLKD690M MultiRewards + - 0x4EEdEe17CDFbd9910C421ecc9d3401C70C0BF624 # HLKD420M MultiRewards + - 0xec204cb71D69f1b4d334C960D16a68364B604857 # HLKD330M MultiRewards + - 0x00192Ce353151563B3bd8664327d882c7ac45CB8 # HLKD100M MultiRewards + # HenloVault for tracking HENLOCKED token mints + - name: HenloVault + address: + - 0x42069E3BF367C403b632CF9cD5a8d61e2c0c44fC # HenloVault + # Tracked ERC-20 tokens for balance tracking (HENLO + HENLOCKED tiers) + - name: TrackedErc20 + address: + - 0xb2F776e9c1C926C4b2e54182Fac058dA9Af0B6A5 # HENLO token + - 0xF0edfc3e122DB34773293E0E5b2C3A58492E7338 # HLKD1B + - 0x8AB854dC0672d7A13A85399A56CB628FB22102d6 # HLKD690M + - 0xF07Fa3ECE9741D408d643748Ff85710BEdEF25bA # HLKD420M + - 0x37DD8850919EBdCA911C383211a70839A94b0539 # HLKD330M + - 0x7Bdf98DdeEd209cFa26bD2352b470Ac8b5485EC5 # HLKD100M + # Mibera Treasury - NFT backing and marketplace + - name: MiberaTreasury + address: + - 0xaa04F13994A7fCd86F3BbbF4054d239b88F2744d # Mibera Treasury + # Mibera Collection - NFT transfer tracking for mint activity + - name: MiberaCollection + address: + - 0x6666397dfe9a8c469bf65dc744cb1c733416c420 # Mibera Collection + # Mibera Premint - Participation and refund tracking + - name: MiberaPremint + address: + - 0xdd5F6f41B250644E5678D77654309a5b6A5f4D55 # Mibera Premint + # Seaport - OpenSea marketplace for secondary sales + - name: Seaport + address: + - "0x0000000000000068F116a894984e2DB1123eB395" # Seaport v1.6 + +# Enable multichain mode for cross-chain tracking unordered_multichain_mode: true preload_handlers: true diff --git a/package.json b/package.json index 9337309..b993d97 100644 --- a/package.json +++ b/package.json @@ -12,16 +12,18 @@ "test": "pnpm mocha" }, "devDependencies": { - "@types/chai": "^4.3.11", + "@types/chai": "^4.3.11", "@types/mocha": "10.0.6", "@types/node": "20.8.8", - "ts-mocha": "^10.0.0", - "typescript": "5.2.2", "chai": "4.3.10", - "mocha": "10.2.0" + "mocha": "10.2.0", + "ts-mocha": "^10.0.0", + "typescript": "5.2.2" }, "dependencies": { - "envio": "2.27.3" + "envio": "2.32.2", + "ethers": "^6.15.0", + "viem": "^2.21.0" }, "optionalDependencies": { "generated": "./generated" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7072f46..dd76e63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,12 +9,14 @@ importers: .: dependencies: envio: - specifier: 2.27.3 - version: 2.27.3(typescript@5.2.2) - optionalDependencies: - generated: - specifier: ./generated - version: link:generated + specifier: 2.32.2 + version: 2.32.2(typescript@5.2.2) + ethers: + specifier: ^6.15.0 + version: 6.15.0 + viem: + specifier: ^2.21.0 + version: 2.21.0(typescript@5.2.2) devDependencies: '@types/chai': specifier: ^4.3.11 @@ -37,55 +39,117 @@ importers: typescript: specifier: 5.2.2 version: 5.2.2 + optionalDependencies: + generated: + specifier: ./generated + version: link:generated packages: '@adraffy/ens-normalize@1.10.0': resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} - '@envio-dev/hypersync-client-darwin-arm64@0.6.5': - resolution: {integrity: sha512-BjFmDFd+7QKuEkjlvwQjKy9b+ZWidkZHyKPjKSDg6u3KJe+fr+uY3rsW9TXNscUxJvl8YxJ2mZl0svOH7ukTyQ==} + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + + '@elastic/ecs-helpers@1.1.0': + resolution: {integrity: sha512-MDLb2aFeGjg46O5mLpdCzT5yOUDnXToJSrco2ShqGIXxNJaM8uJjX+4nd+hRYV4Vex8YJyDtOFEVBldQct6ndg==} + engines: {node: '>=10'} + + '@elastic/ecs-pino-format@1.4.0': + resolution: {integrity: sha512-eCSBUTgl8KbPyxky8cecDRLCYu2C1oFV4AZ72bEsI+TxXEvaljaL2kgttfzfu7gW+M89eCz55s49uF2t+YMTWA==} + engines: {node: '>=10'} + + '@envio-dev/hyperfuel-client-darwin-arm64@1.2.2': + resolution: {integrity: sha512-eQyd9kJCIz/4WCTjkjpQg80DA3pdneHP7qhJIVQ2ZG+Jew9o5XDG+uI0Y16AgGzZ6KGmJSJF6wyUaaAjJfbO1Q==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@envio-dev/hypersync-client-darwin-x64@0.6.5': - resolution: {integrity: sha512-XT1l6bfsXgZqxh8BZbPoP/3Zk0Xvwzr/ZKVmzXR5ZhPxDgEVUJMg4Rd1oy8trd1K+uevqOr2DbuIGvM7k2hb8A==} + '@envio-dev/hyperfuel-client-darwin-x64@1.2.2': + resolution: {integrity: sha512-l7lRMSoyIiIvKZgQPfgqg7H1xnrQ37A8yUp4S2ys47R8f/wSCSrmMaY1u7n6CxVYCpR9fajwy0/356UgwwhVKw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@envio-dev/hypersync-client-linux-arm64-gnu@0.6.5': - resolution: {integrity: sha512-MPTXagjE8/XQhNiZokIJWYqDcizf++TKOjbfYgCzlS6jzwgmeZs6WYcdYFC3FSaJyc9GX4diJ4GKOgbpR4XWtw==} + '@envio-dev/hyperfuel-client-linux-arm64-gnu@1.2.2': + resolution: {integrity: sha512-kNiC/1fKuXnoSxp8yEsloDw4Ot/mIcNoYYGLl2CipSIpBtSuiBH5nb6eBcxnRZdKOwf5dKZtZ7MVPL9qJocNJw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@envio-dev/hypersync-client-linux-x64-gnu@0.6.5': - resolution: {integrity: sha512-DUDY19T2O+ciniP8RHWEv6ziaCdVkkVVLhfXiovpLy+oR1K/+h7osUHD1HCPolibaU3V2EDpqTDhKBtvPXUGaQ==} + '@envio-dev/hyperfuel-client-linux-x64-gnu@1.2.2': + resolution: {integrity: sha512-XDkvkBG/frS+xiZkJdY4KqOaoAwyxPdi2MysDQgF8NmZdssi32SWch0r4LTqKWLLlCBg9/R55POeXL5UAjg2wQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@envio-dev/hypersync-client-linux-x64-musl@0.6.5': - resolution: {integrity: sha512-VolsHvPrk5PAdHN0ht1iowwXz7bwJO0L5qDuw3eSKF4qHuAzlwImB1CRhJrMIaE8McsDnN6fSlqDeTPRmzS/Ug==} + '@envio-dev/hyperfuel-client-linux-x64-musl@1.2.2': + resolution: {integrity: sha512-DKnKJJSwsYtA7YT0EFGhFB5Eqoo42X0l0vZBv4lDuxngEXiiNjeLemXoKQVDzhcbILD7eyXNa5jWUc+2hpmkEg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@envio-dev/hypersync-client-win32-x64-msvc@0.6.5': - resolution: {integrity: sha512-D+bkkWbCsbgaTrhyVdXHysKUCVzFpkWoxmaHnm2anad7+yKKfx15afYirtZMTKc7CLkYqganghN4QsBsEHl3Iw==} + '@envio-dev/hyperfuel-client-win32-x64-msvc@1.2.2': + resolution: {integrity: sha512-SwIgTAVM9QhCFPyHwL+e1yQ6o3paV6q25klESkXw+r/KW9QPhOOyA6Yr8nfnur3uqMTLJHAKHTLUnkyi/Nh7Aw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@envio-dev/hypersync-client@0.6.5': - resolution: {integrity: sha512-mii+ponVo5ZmVOlEtJxyugGHuIuzYp5bVfr88mCuRwcWZIkNrWfad/aAW6H7YNe63E0gq0ePtRDrkLzlpAUuGQ==} + '@envio-dev/hyperfuel-client@1.2.2': + resolution: {integrity: sha512-raKA6DshYSle0sAOHBV1OkSRFMN+Mkz8sFiMmS3k+m5nP6pP56E17CRRePBL5qmR6ZgSEvGOz/44QUiKNkK9Pg==} engines: {node: '>= 10'} + '@envio-dev/hypersync-client-darwin-arm64@0.6.6': + resolution: {integrity: sha512-5uAwSNrnekbHiZBLipUPM0blfO0TS2svyuMmDVE+xbT3M+ODuQl4BFoINd9VY6jC5EoKt8xKCO2K/DHHSeRV4A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@envio-dev/hypersync-client-darwin-x64@0.6.6': + resolution: {integrity: sha512-KFMXWpHbyA0q+sRQ6I8YcLIwZFbBjMEncTnRz6IWXNWAXOsIc1GOORz0j5c9I330bEa4cdQdVVWhgCR1gJiBBA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@envio-dev/hypersync-client-linux-arm64-gnu@0.6.6': + resolution: {integrity: sha512-Iiok/+YNtVft37KGWwDPC8yiN4rAZujYTiYiu+j+vfRpJT6DnYj/TbklZ/6LnSafg18BMPZ2fHT804jP0LndHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@envio-dev/hypersync-client-linux-x64-gnu@0.6.6': + resolution: {integrity: sha512-WgQRjJS1ncdP/f89dGBKD1luC/r+0EJZgvXSJ+8Jy4dnAeMHUgDFCpjJqIqQKxCWX0fmoiJ7a31SzBNV8Lwqbg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@envio-dev/hypersync-client-linux-x64-musl@0.6.6': + resolution: {integrity: sha512-upFn8FfcUP5pTdSiQAsEr06L2SwyxluMWMaeUCgAEYxDcKTxUkg0J2eDq37RGUQ0KVlLoWLthnSsg4lUz7NIXg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@envio-dev/hypersync-client-win32-x64-msvc@0.6.6': + resolution: {integrity: sha512-bVFDkyrddbMnNGYd6o/QwhrviHOa4th/aMjzMPRjXu48GI8xqlamQ6RBxDGy2lg+BoPhs5k3kwOWl/DY29RwUQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@envio-dev/hypersync-client@0.6.6': + resolution: {integrity: sha512-0r4lPFtk49zB94uvZiONV0SWdr9kigdNIYfYTYcSSuZ396E77tjskjMigDwimZsAA5Qf64x6MsIyzUYIzk/KPg==} + engines: {node: '>= 10'} + + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + '@noble/curves@1.4.0': resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -115,6 +179,9 @@ packages: '@types/node@20.8.8': resolution: {integrity: sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + abitype@1.0.5: resolution: {integrity: sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw==} peerDependencies: @@ -130,6 +197,12 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-colors@4.1.1: resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} engines: {node: '>=6'} @@ -250,6 +323,10 @@ packages: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + diff@3.5.0: resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} engines: {node: '>=0.3.1'} @@ -264,28 +341,28 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - envio-darwin-arm64@2.27.3: - resolution: {integrity: sha512-/+QSoyTTsffhqlnIPy3PIhnn4HnP6S5UCm2HachLgpQKeEpV/Wmab3SHY0kj7uPp7W1Amhx6N1X1NiMMBpGC7A==} + envio-darwin-arm64@2.32.2: + resolution: {integrity: sha512-tCyzTAJ6X/L9lISYQtddNUCu/WdZu88/4nBpVD2sJ5cDGdSCcEsuwQlREQ888H5OL2ai2c7YcIJM0N+jh8plPg==} cpu: [arm64] os: [darwin] - envio-darwin-x64@2.27.3: - resolution: {integrity: sha512-Vk83E3G0SJL6AfpYyrrCs4xy6AdSEGWevq9vrSAMybE+xXbWBhovedF4F/MXOp8SbLCALhxyEmzdSGBECpArCA==} + envio-darwin-x64@2.32.2: + resolution: {integrity: sha512-e1pM8UCSbVt/V5ONc8pFLycPqOyPBgQTLuZpPCRDdw1vFXpFy0Tz/0hbK9eMXJqBkZmunYYy3m62NAkLb4bAuQ==} cpu: [x64] os: [darwin] - envio-linux-arm64@2.27.3: - resolution: {integrity: sha512-bnmhgF/Ee/fDrVs/i5p4y1gM71zKvI1lKBOzq9/tGBOVdGCb8JP22ZtSgklo3YgSJD5xdM0hdXHk88G2dR268A==} + envio-linux-arm64@2.32.2: + resolution: {integrity: sha512-eRXYiMLujWLq167leiktcHaejjpCQS0nJcixEAXRzeqYMYfiEr3N8SnTjqUOM4StEoaj6D3LGjpS4621OaOcDw==} cpu: [arm64] os: [linux] - envio-linux-x64@2.27.3: - resolution: {integrity: sha512-/Ak6d75gcwWnAs+za7vrmf9Lb7C/2kIsDp0CQ96VMXnuW63a90W1cOEAVHBdEm8Q6kqg2rm7uZ8XRvh30OO5iQ==} + envio-linux-x64@2.32.2: + resolution: {integrity: sha512-zdNjjjis1p4ens+lKHyfbzwHNvvjWUIzPguOLVQZyOCjWsNhr2LGI30yTjvGaAJ6haEm+dYFR0e0CD+ZLGrvpw==} cpu: [x64] os: [linux] - envio@2.27.3: - resolution: {integrity: sha512-tj7uq4KWkDy4iV14e7MgGpOFVTX2qvdo56YW/PzP/PWAVCYkvig6Z3UJVpZkr2JXZk9JPg6+FyCbHGIqdhAaMQ==} + envio@2.32.2: + resolution: {integrity: sha512-5tK8DErwbsmDa90IC7MNv4P1GvhAQ2ALHChBkXsTT47KB3K6P+kMNeyxQzLtf5pZKdmc7plsghfjxdBadxb6cQ==} hasBin: true escalade@3.2.0: @@ -296,6 +373,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + ethers@6.15.0: + resolution: {integrity: sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==} + engines: {node: '>=14.0.0'} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -307,6 +388,16 @@ packages: fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-json-stringify@2.7.13: + resolution: {integrity: sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==} + engines: {node: '>= 10.0.0'} + fast-redact@3.5.0: resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} engines: {node: '>=6'} @@ -416,6 +507,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -533,6 +627,10 @@ packages: pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -572,6 +670,9 @@ packages: engines: {node: '>=10'} hasBin: true + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -599,6 +700,10 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + string-similarity@4.0.4: + resolution: {integrity: sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -651,6 +756,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + type-detect@4.1.0: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} @@ -663,6 +771,12 @@ packages: undici-types@5.25.3: resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -727,37 +841,80 @@ snapshots: '@adraffy/ens-normalize@1.10.0': {} - '@envio-dev/hypersync-client-darwin-arm64@0.6.5': + '@adraffy/ens-normalize@1.10.1': {} + + '@elastic/ecs-helpers@1.1.0': + dependencies: + fast-json-stringify: 2.7.13 + + '@elastic/ecs-pino-format@1.4.0': + dependencies: + '@elastic/ecs-helpers': 1.1.0 + + '@envio-dev/hyperfuel-client-darwin-arm64@1.2.2': + optional: true + + '@envio-dev/hyperfuel-client-darwin-x64@1.2.2': + optional: true + + '@envio-dev/hyperfuel-client-linux-arm64-gnu@1.2.2': optional: true - '@envio-dev/hypersync-client-darwin-x64@0.6.5': + '@envio-dev/hyperfuel-client-linux-x64-gnu@1.2.2': optional: true - '@envio-dev/hypersync-client-linux-arm64-gnu@0.6.5': + '@envio-dev/hyperfuel-client-linux-x64-musl@1.2.2': optional: true - '@envio-dev/hypersync-client-linux-x64-gnu@0.6.5': + '@envio-dev/hyperfuel-client-win32-x64-msvc@1.2.2': optional: true - '@envio-dev/hypersync-client-linux-x64-musl@0.6.5': + '@envio-dev/hyperfuel-client@1.2.2': + optionalDependencies: + '@envio-dev/hyperfuel-client-darwin-arm64': 1.2.2 + '@envio-dev/hyperfuel-client-darwin-x64': 1.2.2 + '@envio-dev/hyperfuel-client-linux-arm64-gnu': 1.2.2 + '@envio-dev/hyperfuel-client-linux-x64-gnu': 1.2.2 + '@envio-dev/hyperfuel-client-linux-x64-musl': 1.2.2 + '@envio-dev/hyperfuel-client-win32-x64-msvc': 1.2.2 + + '@envio-dev/hypersync-client-darwin-arm64@0.6.6': optional: true - '@envio-dev/hypersync-client-win32-x64-msvc@0.6.5': + '@envio-dev/hypersync-client-darwin-x64@0.6.6': optional: true - '@envio-dev/hypersync-client@0.6.5': + '@envio-dev/hypersync-client-linux-arm64-gnu@0.6.6': + optional: true + + '@envio-dev/hypersync-client-linux-x64-gnu@0.6.6': + optional: true + + '@envio-dev/hypersync-client-linux-x64-musl@0.6.6': + optional: true + + '@envio-dev/hypersync-client-win32-x64-msvc@0.6.6': + optional: true + + '@envio-dev/hypersync-client@0.6.6': optionalDependencies: - '@envio-dev/hypersync-client-darwin-arm64': 0.6.5 - '@envio-dev/hypersync-client-darwin-x64': 0.6.5 - '@envio-dev/hypersync-client-linux-arm64-gnu': 0.6.5 - '@envio-dev/hypersync-client-linux-x64-gnu': 0.6.5 - '@envio-dev/hypersync-client-linux-x64-musl': 0.6.5 - '@envio-dev/hypersync-client-win32-x64-msvc': 0.6.5 + '@envio-dev/hypersync-client-darwin-arm64': 0.6.6 + '@envio-dev/hypersync-client-darwin-x64': 0.6.6 + '@envio-dev/hypersync-client-linux-arm64-gnu': 0.6.6 + '@envio-dev/hypersync-client-linux-x64-gnu': 0.6.6 + '@envio-dev/hypersync-client-linux-x64-musl': 0.6.6 + '@envio-dev/hypersync-client-win32-x64-msvc': 0.6.6 + + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 '@noble/curves@1.4.0': dependencies: '@noble/hashes': 1.4.0 + '@noble/hashes@1.3.2': {} + '@noble/hashes@1.4.0': {} '@opentelemetry/api@1.9.0': {} @@ -786,6 +943,10 @@ snapshots: dependencies: undici-types: 5.25.3 + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + abitype@1.0.5(typescript@5.2.2): optionalDependencies: typescript: 5.2.2 @@ -794,6 +955,15 @@ snapshots: dependencies: event-target-shim: 5.0.1 + aes-js@4.0.0-beta.5: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-colors@4.1.1: {} ansi-regex@5.0.1: {} @@ -910,6 +1080,8 @@ snapshots: dependencies: type-detect: 4.1.0 + deepmerge@4.3.1: {} + diff@3.5.0: {} diff@5.0.0: {} @@ -920,21 +1092,23 @@ snapshots: dependencies: once: 1.4.0 - envio-darwin-arm64@2.27.3: + envio-darwin-arm64@2.32.2: optional: true - envio-darwin-x64@2.27.3: + envio-darwin-x64@2.32.2: optional: true - envio-linux-arm64@2.27.3: + envio-linux-arm64@2.32.2: optional: true - envio-linux-x64@2.27.3: + envio-linux-x64@2.32.2: optional: true - envio@2.27.3(typescript@5.2.2): + envio@2.32.2(typescript@5.2.2): dependencies: - '@envio-dev/hypersync-client': 0.6.5 + '@elastic/ecs-pino-format': 1.4.0 + '@envio-dev/hyperfuel-client': 1.2.2 + '@envio-dev/hypersync-client': 0.6.6 bignumber.js: 9.1.2 pino: 8.16.1 pino-pretty: 10.2.3 @@ -943,10 +1117,10 @@ snapshots: rescript-schema: 9.3.0(rescript@11.1.3) viem: 2.21.0(typescript@5.2.2) optionalDependencies: - envio-darwin-arm64: 2.27.3 - envio-darwin-x64: 2.27.3 - envio-linux-arm64: 2.27.3 - envio-linux-x64: 2.27.3 + envio-darwin-arm64: 2.32.2 + envio-darwin-x64: 2.32.2 + envio-linux-arm64: 2.32.2 + envio-linux-x64: 2.32.2 transitivePeerDependencies: - bufferutil - typescript @@ -957,12 +1131,36 @@ snapshots: escape-string-regexp@4.0.0: {} + ethers@6.15.0: + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + event-target-shim@5.0.1: {} events@3.3.0: {} fast-copy@3.0.2: {} + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-json-stringify@2.7.13: + dependencies: + ajv: 6.12.6 + deepmerge: 4.3.1 + rfdc: 1.4.1 + string-similarity: 4.0.4 + fast-redact@3.5.0: {} fast-safe-stringify@2.1.1: {} @@ -1054,6 +1252,8 @@ snapshots: dependencies: argparse: 2.0.1 + json-schema-traverse@0.4.1: {} + json5@1.0.2: dependencies: minimist: 1.2.8 @@ -1203,6 +1403,8 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + punycode@2.3.1: {} + quick-format-unescaped@4.0.4: {} randombytes@2.1.0: @@ -1237,6 +1439,8 @@ snapshots: rescript@11.1.3: {} + rfdc@1.4.1: {} + safe-buffer@5.2.1: {} safe-stable-stringify@2.5.0: {} @@ -1260,6 +1464,8 @@ snapshots: split2@4.2.0: {} + string-similarity@4.0.4: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -1325,12 +1531,20 @@ snapshots: strip-bom: 3.0.0 optional: true + tslib@2.7.0: {} + type-detect@4.1.0: {} typescript@5.2.2: {} undici-types@5.25.3: {} + undici-types@6.19.8: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + util-deprecate@1.0.2: {} viem@2.21.0(typescript@5.2.2): diff --git a/schema.graphql b/schema.graphql index 27a2d0d..50ee162 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,3 +1,16 @@ +type Action { + id: ID! + actionType: String! + actor: String! + primaryCollection: String + timestamp: BigInt! + chainId: Int! + txHash: String! + numeric1: BigInt + numeric2: BigInt + context: String +} + type Transfer { id: ID! tokenId: BigInt! @@ -10,6 +23,97 @@ type Transfer { chainId: Int! } +type MintEvent { + id: ID! + collectionKey: String! + tokenId: BigInt! + minter: String! + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! + encodedTraits: String # VM-specific: encoded trait data from Minted event +} + +type Erc1155MintEvent { + id: ID! + collectionKey: String! + tokenId: BigInt! + value: BigInt! + minter: String! + operator: String! + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +type CandiesInventory { + id: ID! # contract_tokenId (e.g., "0x80283fbf2b8e50f6ddf9bfc4a90a8336bc90e38f_1") + contract: String! + tokenId: BigInt! + currentSupply: BigInt! # Cumulative mints + mintCount: Int! # Number of mint transactions + lastMintTime: BigInt + chainId: Int! +} + +type BadgeHolder { + id: ID! + address: String! + chainId: Int! + totalBadges: BigInt! + totalAmount: BigInt! + holdings: Json! + updatedAt: BigInt! + badgeBalances: [BadgeBalance!]! @derivedFrom(field: "holder") + badgesHeld: [BadgeAmount!]! @derivedFrom(field: "holder") +} + +type BadgeAmount { + id: ID! + holder: BadgeHolder! + badgeId: String! + amount: BigInt! + updatedAt: BigInt! +} + +type BadgeBalance { + id: ID! + holder: BadgeHolder! + contract: String! + tokenId: BigInt! + chainId: Int! + amount: BigInt! + updatedAt: BigInt! +} + +type FatBeraDeposit { + id: ID! + collectionKey: String! + depositor: String! + recipient: String! + amount: BigInt! + shares: BigInt! + transactionFrom: String + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +type BgtBoostEvent { + id: ID! + account: String! + validatorPubkey: String! + amount: BigInt! + transactionFrom: String! + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + type HoneyJar_Approval { id: ID! owner: String! @@ -69,23 +173,62 @@ type Holder { chainId: Int! } +type TrackedHolder { + id: ID! + contract: String! + collectionKey: String! + chainId: Int! + address: String! + tokenCount: Int! +} + type CollectionStat { id: ID! collection: String! totalSupply: Int! + totalMinted: Int! + totalBurned: Int! uniqueHolders: Int! lastMintTime: BigInt chainId: Int! } +type GlobalCollectionStat { + id: ID! + collection: String! + circulatingSupply: Int! + homeChainSupply: Int! + ethereumSupply: Int! + berachainSupply: Int! + proxyLockedSupply: Int! + totalMinted: Int! + totalBurned: Int! + uniqueHoldersTotal: Int! + lastUpdateTime: BigInt! + homeChainId: Int! +} + +type Token { + id: ID! + collection: String! + chainId: Int! + tokenId: BigInt! + owner: String! + isBurned: Boolean! + mintedAt: BigInt! + lastTransferTime: BigInt! +} + type UserBalance { id: ID! address: String! generation: Int! balanceHomeChain: Int! + balanceEthereum: Int! balanceBerachain: Int! balanceTotal: Int! mintedHomeChain: Int! + mintedEthereum: Int! mintedBerachain: Int! mintedTotal: Int! lastActivityTime: BigInt! @@ -136,3 +279,735 @@ type UserVaultSummary { firstVaultTime: BigInt lastActivityTime: BigInt! } + +# ============================ +# NFT BURN TRACKING MODELS +# ============================ + +type NftBurn { + id: ID! # tx_hash_logIndex + collectionKey: String! # "mibera", "milady", etc. + tokenId: BigInt! + from: String! # Address that burned the NFT + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +type NftBurnStats { + id: ID! # chainId_collectionKey (e.g., "80094_mibera" or "1_milady") + chainId: Int! + collectionKey: String! + totalBurned: Int! + uniqueBurners: Int! + lastBurnTime: BigInt + firstBurnTime: BigInt +} + +# ============================ +# HENLO BURN TRACKING MODELS +# ============================ + +type HenloBurn { + id: ID! # tx_hash_logIndex + amount: BigInt! + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + from: String! # Address that initiated the burn + source: String! # "incinerator", "overunder", "beratrackr", or "user" + chainId: Int! +} + +type HenloBurnStats { + id: ID! # chainId_source (e.g., "80084_incinerator" or "80084_total") + chainId: Int! + source: String! # "incinerator", "overunder", "beratrackr", "user", or "total" + totalBurned: BigInt! + burnCount: Int! + uniqueBurners: Int! # Count of unique addresses for this source on this chain + lastBurnTime: BigInt + firstBurnTime: BigInt +} + +type HenloGlobalBurnStats { + id: ID! # "global" + totalBurnedAllChains: BigInt! + totalBurnedMainnet: BigInt! + totalBurnedTestnet: BigInt! + burnCountAllChains: Int! + incineratorBurns: BigInt! + overunderBurns: BigInt! + beratrackrBurns: BigInt! + userBurns: BigInt! + uniqueBurners: Int! # Count of unique addresses that have burned at least once (all chains) + incineratorUniqueBurners: Int! # Unique addresses that have burned via the incinerator (all chains) + lastUpdateTime: BigInt! +} + +# ============================ +# HENLO HOLDER TRACKING MODELS +# ============================ + +type HenloHolder { + id: ID! # address (lowercase) + address: String! # Holder address (lowercase) + balance: BigInt! # Current balance + firstTransferTime: BigInt # First time they received HENLO + lastActivityTime: BigInt! # Last transfer activity + chainId: Int! +} + +type HenloHolderStats { + id: ID! # chainId (e.g., "80084") + chainId: Int! + uniqueHolders: Int! # Count of addresses with balance > 0 + totalSupply: BigInt! # Sum of all holder balances + lastUpdateTime: BigInt! +} + +# ============================ +# UNIQUE BURNERS MATERIALIZATION +# ============================ + +type HenloBurner { + id: ID! # address (lowercase) + address: String! # duplicate of id for convenience + firstBurnTime: BigInt + chainId: Int! +} + +type HenloSourceBurner { + id: ID! # chainId_source_address (e.g., "80084_incinerator_0x...") + chainId: Int! + source: String! + address: String! + firstBurnTime: BigInt +} + +type HenloChainBurner { + id: ID! # chainId_address + chainId: Int! + address: String! + firstBurnTime: BigInt +} + +# ============================ +# AQUABERA WALL TRACKING MODELS +# ============================ + +type AquaberaDeposit { + id: ID! # tx_hash_logIndex + amount: BigInt! # Amount of BERA deposited + shares: BigInt! # LP tokens received + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + from: String! # Address that made the deposit + isWallContribution: Boolean! # True if from wall contract address + chainId: Int! +} + +type AquaberaWithdrawal { + id: ID! # tx_hash_logIndex + amount: BigInt! # Amount of BERA withdrawn + shares: BigInt! # LP tokens burned + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + from: String! # Address that made the withdrawal + chainId: Int! +} + +type AquaberaBuilder { + id: ID! # user address + address: String! + totalDeposited: BigInt! # Total BERA deposited + totalWithdrawn: BigInt! # Total BERA withdrawn + netDeposited: BigInt! # Deposited minus withdrawn + currentShares: BigInt! # Current LP token balance + depositCount: Int! + withdrawalCount: Int! + firstDepositTime: BigInt + lastActivityTime: BigInt! + isWallContract: Boolean! # True if this is the wall contract address + chainId: Int! +} + +type AquaberaStats { + id: ID! # "global" or "chainId" for per-chain stats + totalBera: BigInt! # Total BERA in vault + totalShares: BigInt! # Total LP tokens + totalDeposited: BigInt! # All-time deposits + totalWithdrawn: BigInt! # All-time withdrawals + uniqueBuilders: Int! # Unique addresses that deposited + depositCount: Int! + withdrawalCount: Int! + wallContributions: BigInt! # Total BERA from wall contract + wallDepositCount: Int! # Number of wall deposits + lastUpdateTime: BigInt! + chainId: Int +} + +# ============================================================================ +# TRADING SYSTEM +# ============================================================================ + +# Mibera NFT Trade (ERC-721 trades) +type MiberaTrade { + id: ID! # tx_hash_logIndex for proposals, tx_hash_offeredTokenId for accept/cancel + offeredTokenId: BigInt! + requestedTokenId: BigInt! + proposer: String! + acceptor: String # Null until accepted + status: String! # 'active', 'completed', 'cancelled', 'expired' + proposedAt: BigInt! + completedAt: BigInt # Null until completed or cancelled + expiresAt: BigInt! # proposedAt + 15 minutes + txHash: String! + blockNumber: BigInt! + chainId: Int! +} + +# Cargo/Candies Trade (ERC-1155 trades) +type CandiesTrade { + id: ID! # tx_hash_logIndex + tradeId: BigInt! # Sequential ID from smart contract + offeredTokenId: BigInt! + offeredAmount: BigInt! + requestedTokenId: BigInt! + requestedAmount: BigInt! + proposer: String! + requestedFrom: String! # Target user for this trade + acceptor: String # Null until accepted + status: String! # 'active', 'completed', 'cancelled', 'expired' + proposedAt: BigInt! + completedAt: BigInt # Null until completed or cancelled + expiresAt: BigInt! # proposedAt + 15 minutes + txHash: String! + blockNumber: BigInt! + chainId: Int! +} + +# Trade statistics +type TradeStats { + id: ID! # "global" for all-time stats + totalMiberaTrades: Int! + completedMiberaTrades: Int! + cancelledMiberaTrades: Int! + expiredMiberaTrades: Int! + totalCandiesTrades: Int! + completedCandiesTrades: Int! + cancelledCandiesTrades: Int! + expiredCandiesTrades: Int! + uniqueTraders: Int! # Count of unique addresses that have traded + lastTradeTime: BigInt + chainId: Int +} + +# ============================================================================ +# SET & FORGETTI VAULT SYSTEM +# ============================================================================ + +# User's active position in a Set & Forgetti vault (stateful tracking) +# IMPORTANT FIELDS EXPLANATION: +# - totalDeposited & totalWithdrawn: Cumulative lifetime flows of kitchen tokens +# * Use (totalDeposited - totalWithdrawn) to check if user has net deposits +# - vaultShares: Current unstaked vault shares in user's wallet +# - stakedShares: AGGREGATE of shares staked across ALL MultiRewards for this vault +# * This is the SUM of all SFMultiRewardsPosition.stakedShares for this user+vault +# * Does NOT show which MultiRewards contract holds which shares +# * For per-MultiRewards breakdown, query SFMultiRewardsPosition entities +# - totalShares: Total ownership = vaultShares + stakedShares +type SFPosition { + id: ID! # {chainId}_{user}_{vault} + user: String! # User address (lowercase) + vault: String! # SFVault address (lowercase) + multiRewards: String! # MultiRewards address (lowercase) + kitchenToken: String! # Underlying kitchen token address (lowercase) + strategy: String! # BeradromeStrategy address (lowercase) + kitchenTokenSymbol: String! # Token symbol (e.g., "HLKD1B") + vaultShares: BigInt! # Current vault shares in user's wallet (not staked) + stakedShares: BigInt! # Current staked vault shares in MultiRewards (aggregate across all generations) + totalShares: BigInt! # Total shares owned (vaultShares + stakedShares) + totalDeposited: BigInt! # Lifetime kitchen tokens deposited into vault (cumulative flow) + totalWithdrawn: BigInt! # Lifetime kitchen tokens withdrawn from vault (cumulative flow) + totalClaimed: BigInt! # Lifetime HENLO rewards claimed + firstDepositAt: BigInt! # Timestamp of first deposit + lastActivityAt: BigInt! # Timestamp of most recent activity + chainId: Int! +} + +# Vault-level aggregated statistics (income tracking per pot) +type SFVaultStats { + id: ID! # {chainId}_{vault} + vault: String! # SFVault address (lowercase) + kitchenToken: String! # Underlying kitchen token address (lowercase) + kitchenTokenSymbol: String! # Token symbol (e.g., "HLKD1B") + strategy: String! # BeradromeStrategy address (lowercase) + totalDeposited: BigInt! # All-time kitchen tokens deposited + totalWithdrawn: BigInt! # All-time kitchen tokens withdrawn + totalStaked: BigInt! # All-time vault shares staked + totalUnstaked: BigInt! # All-time vault shares unstaked + totalClaimed: BigInt! # All-time HENLO rewards claimed (income metric!) + uniqueDepositors: Int! # Count of unique users who have deposited + activePositions: Int! # Current count of positions with stakedShares > 0 + depositCount: Int! # Total number of deposit transactions + withdrawalCount: Int! # Total number of withdrawal transactions + claimCount: Int! # Total number of claim transactions + firstDepositAt: BigInt # Timestamp of first vault deposit + lastActivityAt: BigInt! # Timestamp of most recent activity + chainId: Int! +} + +# Tracks user staking in individual MultiRewards contracts +# Linked to SFPosition via user+vault to show breakdown across old/new MultiRewards +# IMPORTANT: This entity tracks PER-MULTIREWARDS positions separately +# - When vaults migrate strategies, new MultiRewards contracts are created +# - Users may have stakedShares > 0 in MULTIPLE MultiRewards for the same vault +# - To identify migration opportunities: +# 1. Query SFMultiRewardsPosition where stakedShares > 0 +# 2. Check SFVaultStrategy to see if that multiRewards has activeTo != null (inactive) +# 3. If inactive && stakedShares > 0, user needs to migrate to the new MultiRewards +type SFMultiRewardsPosition { + id: ID! # {chainId}_{user}_{multiRewards} + user: String! # User address (lowercase) + vault: String! # Vault address this MultiRewards belongs to + multiRewards: String! # MultiRewards contract address (lowercase) + stakedShares: BigInt! # Current shares staked in THIS specific MultiRewards contract + totalStaked: BigInt! # Cumulative shares ever staked in this MultiRewards (lifetime flow) + totalUnstaked: BigInt! # Cumulative shares ever unstaked from this MultiRewards (lifetime flow) + totalClaimed: BigInt! # HENLO claimed from THIS MultiRewards + firstStakeAt: BigInt # First stake timestamp + lastActivityAt: BigInt! # Last activity timestamp + chainId: Int! +} + +# Tracks vault strategy versions (for handling strategy migrations) +# Allows historical tracking so old MultiRewards can still be indexed +type SFVaultStrategy { + id: ID! # {chainId}_{vault}_{strategy} + vault: String! # SFVault address (lowercase) + strategy: String! # Strategy address (lowercase) + multiRewards: String! # MultiRewards address (lowercase) + kitchenToken: String! # Underlying kitchen token address (lowercase) + kitchenTokenSymbol: String! # Token symbol (e.g., "HLKD1B") + activeFrom: BigInt! # Block timestamp when this strategy became active + activeTo: BigInt # Block timestamp when replaced (null if current) + isActive: Boolean! # True if this is the current strategy + chainId: Int! +} + +# ============================ +# PADDLEFI LENDING TRACKING +# ============================ + +# Individual BERA supply event (lender deposits BERA) +type PaddleSupply { + id: ID! # txHash_logIndex + minter: String! # User who supplied BERA + mintAmount: BigInt! # Amount of BERA supplied (in wei) + mintTokens: BigInt! # pTokens received + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +# Individual pawn event (borrower deposits NFT as collateral) +type PaddlePawn { + id: ID! # txHash_logIndex + borrower: String! # User who pawned NFTs + nftIds: [BigInt!]! # Array of Mibera token IDs used as collateral + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +# Aggregate supplier stats +type PaddleSupplier { + id: ID! # address (lowercase) + address: String! # Supplier address + totalSupplied: BigInt! # Lifetime BERA supplied + totalPTokens: BigInt! # Total pTokens received + supplyCount: Int! # Number of supply transactions + firstSupplyTime: BigInt + lastActivityTime: BigInt! + chainId: Int! +} + +# Aggregate borrower stats +type PaddleBorrower { + id: ID! # address (lowercase) + address: String! # Borrower address + totalNftsPawned: Int! # Total NFTs used as collateral (lifetime) + currentNftsPawned: Int! # Currently pawned NFTs + pawnCount: Int! # Number of pawn transactions + firstPawnTime: BigInt + lastActivityTime: BigInt! + chainId: Int! +} + +# ============================ +# MIBERA STAKING TRACKING +# ============================ + +type MiberaStakedToken { + id: ID! # stakingContract_tokenId (e.g., "paddlefi_123") + stakingContract: String! # "paddlefi" or "jiko" + contractAddress: String! # 0x242b... or 0x8778... (lowercase) + tokenId: BigInt! + owner: String! # current holder address (lowercase) + isStaked: Boolean! # true if currently staked, false if withdrawn + depositedAt: BigInt! + depositTxHash: String! + depositBlockNumber: BigInt! + withdrawnAt: BigInt # null if still staked + withdrawTxHash: String + withdrawBlockNumber: BigInt + chainId: Int! +} + +type MiberaStaker { + id: ID! # stakingContract_address (e.g., "paddlefi_0x123...") + stakingContract: String! # "paddlefi" or "jiko" + contractAddress: String! # 0x242b... or 0x8778... (lowercase) + address: String! # user address (lowercase) + currentStakedCount: Int! # Number of tokens currently staked + totalDeposits: Int! # All-time deposits + totalWithdrawals: Int! # All-time withdrawals + firstDepositTime: BigInt + lastActivityTime: BigInt! + chainId: Int! +} + +# ============================ +# MIBERA TREASURY MARKETPLACE +# ============================ + +# ============================ +# MIBERA LOAN SYSTEM +# ============================ + +# Active loans tracking (both backing loans and item loans) +type MiberaLoan @entity { + id: ID! # chainId_loanType_loanId (e.g., "80094_backing_1") + loanId: BigInt! + loanType: String! # "backing" | "item" + user: String! # User who took the loan + tokenIds: [BigInt!]! # NFT token IDs used as collateral (backing loans have multiple) + amount: BigInt! # Loan amount (for backing loans) + expiry: BigInt! # Timestamp when loan expires + status: String! # "ACTIVE" | "REPAID" | "DEFAULTED" + createdAt: BigInt! # Timestamp when loan was created + repaidAt: BigInt # Timestamp when repaid (null if active/defaulted) + defaultedAt: BigInt # Timestamp when defaulted (null if active/repaid) + transactionHash: String! + chainId: Int! +} + +# Loan stats aggregate +type MiberaLoanStats @entity { + id: ID! # "80094_global" + totalActiveLoans: Int! + totalLoansCreated: Int! + totalLoansRepaid: Int! + totalLoansDefaulted: Int! + totalAmountLoaned: BigInt! + totalNftsWithLoans: Int! # Current NFTs being used as collateral + chainId: Int! +} + +# Daily RFV snapshots for historical charting +type DailyRfvSnapshot @entity { + id: ID! # chainId_day (e.g., "80094_19875") + day: Int! # Days since epoch + rfv: BigInt! # RFV value for this day + timestamp: BigInt! # Timestamp of when recorded + chainId: Int! +} + +# Collection mint/transfer activity (for activity feed) +type MiberaTransfer @entity { + id: ID! # txHash_logIndex + from: String! + to: String! + tokenId: BigInt! + isMint: Boolean! # True if from is zero address + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +# SilkRoad marketplace orders (from CandiesMarket ERC1155) +type MiberaOrder @entity { + id: ID! # chainId_txHash_logIndex + user: String! # Buyer address (lowercase) + tokenId: BigInt! # Candies token ID purchased + amount: BigInt! # Quantity purchased + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +# Unified activity feed for liquid backing contributions (replaces mibera-squid MintActivity) +type MintActivity @entity { + id: ID! # txHash_tokenId_user_activityType + user: String! # User address (lowercase) + contract: String! # Contract address where activity occurred + tokenStandard: String! # "ERC721" | "ERC1155" + tokenId: BigInt # Token ID (nullable for some activities) + quantity: BigInt! # Quantity (usually 1) + amountPaid: BigInt! # BERA paid in wei (KEY FIELD for backing calculation) + activityType: String! # "MINT" | "SALE" | "PURCHASE" + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + operator: String # Operator address (for ERC1155 or marketplace) + chainId: Int! +} + +# ============================ +# TREASURY MARKETPLACE +# ============================ + +# Treasury-owned NFT tracking (defaulted/redeemed items available for purchase) +type TreasuryItem { + id: ID! # tokenId as string + tokenId: BigInt! + isTreasuryOwned: Boolean! # true if currently owned by treasury + acquiredAt: BigInt # timestamp when treasury acquired it + acquiredVia: String # "backing_loan_default" | "item_loan_default" | "redemption" + acquiredTxHash: String # transaction that transferred to treasury + purchasedAt: BigInt # timestamp when purchased (null if still available) + purchasedBy: String # address that purchased (null if available) + purchasedTxHash: String # purchase transaction hash + purchasePrice: BigInt # RFV + royalty at time of purchase + chainId: Int! +} + +# Treasury aggregate statistics +type TreasuryStats { + id: ID! # "80094_global" + totalItemsOwned: Int! # current count of treasury-owned items + totalItemsEverOwned: Int! # all-time items acquired + totalItemsSold: Int! # all-time items purchased from treasury + realFloorValue: BigInt! # current RFV (from RFVChanged event) + lastRfvUpdate: BigInt # timestamp of last RFV update + lastActivityAt: BigInt! # last event timestamp + chainId: Int! +} + +# Treasury activity event log (for history/feed) +type TreasuryActivity { + id: ID! # txHash_logIndex + activityType: String! # "item_acquired" | "item_purchased" | "rfv_updated" | "backing_loan_defaulted" + tokenId: BigInt # NFT tokenId (null for RFV updates and backing loan defaults) + user: String # user involved (acquirer or purchaser) + amount: BigInt # RFV/price at time of event + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +# ============================ +# TRACKED ERC-20 TOKEN BALANCES +# ============================ + +type TrackedTokenBalance { + id: ID! # {address}_{tokenAddress}_{chainId} + address: String! # Holder address (lowercase) + tokenAddress: String! # Token contract address (lowercase) + tokenKey: String! # Human-readable key (e.g., "henlo", "hlkd1b") + chainId: Int! + balance: BigInt! # Current balance + lastUpdated: BigInt! +} + +# ============================ +# HENLOCKER VAULT SYSTEM +# ============================ + +# Vault round (per strike price per epoch) +type HenloVaultRound { + id: ID! # {strike}_{epochId}_{chainId} + strike: BigInt! # Strike price + epochId: BigInt! # Epoch ID + exists: Boolean! + closed: Boolean! + depositsPaused: Boolean! + timestamp: BigInt! # When round was opened + depositLimit: BigInt! # Maximum deposit capacity + totalDeposits: BigInt! # Total deposited amount + whaleDeposits: BigInt! # Deposits from reservoir (whale matching) + userDeposits: BigInt! # Regular user deposits + remainingCapacity: BigInt! # depositLimit - totalDeposits + canRedeem: Boolean! # Can users redeem from this round + chainId: Int! +} + +# Individual deposit record +type HenloVaultDeposit { + id: ID! # {txHash}_{logIndex} + user: String! # User address (lowercase) + strike: BigInt! # Strike price + epochId: BigInt! # Epoch ID + amount: BigInt! # Deposit amount + timestamp: BigInt! # When deposit occurred + transactionHash: String! # Transaction hash + chainId: Int! +} + +# User balance per strike +type HenloVaultBalance { + id: ID! # {user}_{strike}_{chainId} + user: String! # User address (lowercase) + strike: BigInt! # Strike price + balance: BigInt! # Current balance for this strike + lastUpdated: BigInt! # Last update timestamp + chainId: Int! +} + +# Epoch-level aggregates +type HenloVaultEpoch { + id: ID! # {epochId}_{chainId} + epochId: BigInt! # Epoch ID + strike: BigInt! # Associated strike + closed: Boolean! # Epoch closed + depositsPaused: Boolean! # Deposits paused + timestamp: BigInt! # When epoch created + depositLimit: BigInt! # Deposit limit + totalDeposits: BigInt! # Total user deposits + reservoir: String! # Reservoir contract address + totalWhitelistDeposit: BigInt! # Whitelist deposit total + totalMatched: BigInt! # Matched amounts from reservoir + chainId: Int! +} + +# Global vault statistics (singleton per chain) +type HenloVaultStats { + id: ID! # chainId as string + totalDeposits: BigInt! # Sum of all deposits + totalUsers: Int! # Count of unique users + totalRounds: Int! # Count of rounds created + totalEpochs: Int! # Count of epochs created + chainId: Int! +} + +# Tracks unique users who have deposited +type HenloVaultUser { + id: ID! # {user}_{chainId} + user: String! # User address (lowercase) + firstDepositTime: BigInt # First deposit timestamp + lastActivityTime: BigInt! # Last activity timestamp + chainId: Int! +} + +# ============================ +# MIBERA PREMINT TRACKING +# ============================ + +# Individual premint participation event +type PremintParticipation { + id: ID! # txHash_logIndex + phase: BigInt! # Premint phase (1, 2, etc.) + user: String! # User address (lowercase) + amount: BigInt! # Amount contributed + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +# Individual refund event +type PremintRefund { + id: ID! # txHash_logIndex + phase: BigInt! # Premint phase + user: String! # User address (lowercase) + amount: BigInt! # Amount refunded + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +# Aggregate user premint stats +type PremintUser { + id: ID! # user_chainId + user: String! # User address (lowercase) + totalContributed: BigInt! # Total amount contributed across all phases + totalRefunded: BigInt! # Total amount refunded across all phases + netContribution: BigInt! # totalContributed - totalRefunded + participationCount: Int! # Number of participation events + refundCount: Int! # Number of refund events + firstParticipationTime: BigInt + lastActivityTime: BigInt! + chainId: Int! +} + +# Per-phase statistics +type PremintPhaseStats { + id: ID! # phase_chainId + phase: BigInt! + totalContributed: BigInt! # Total contributions in this phase + totalRefunded: BigInt! # Total refunds in this phase + netContribution: BigInt! # Net amount still in phase + uniqueParticipants: Int! # Count of unique addresses + participationCount: Int! # Total participation events + refundCount: Int! # Total refund events + chainId: Int! +} + +# ============================ +# FRIEND.TECH KEY TRACKING +# ============================ + +# Individual trade event (buy or sell) +type FriendtechTrade { + id: ID! # txHash_logIndex + trader: String! # Address that made the trade + subject: String! # Subject (key) being traded + subjectKey: String! # Human-readable key name (e.g., "jani_key") + isBuy: Boolean! # true = buy, false = sell + shareAmount: BigInt! # Number of shares traded + ethAmount: BigInt! # ETH amount for the trade + supply: BigInt! # Total supply after trade + timestamp: BigInt! + blockNumber: BigInt! + transactionHash: String! + chainId: Int! +} + +# Aggregate holder balance per subject +type FriendtechHolder { + id: ID! # subject_trader_chainId + subject: String! # Subject address + subjectKey: String! # Human-readable key name + holder: String! # Holder address + balance: Int! # Current key balance (buys - sells) + totalBought: Int! # Lifetime keys bought + totalSold: Int! # Lifetime keys sold + firstTradeTime: BigInt # First trade timestamp + lastTradeTime: BigInt! # Last trade timestamp + chainId: Int! +} + +# Per-subject statistics +type FriendtechSubjectStats { + id: ID! # subject_chainId + subject: String! # Subject address + subjectKey: String! # Human-readable key name + totalSupply: BigInt! # Current total supply + uniqueHolders: Int! # Count of addresses with balance > 0 + totalTrades: Int! # Total trade count + totalBuys: Int! # Total buy count + totalSells: Int! # Total sell count + totalVolumeEth: BigInt! # Total ETH volume + lastTradeTime: BigInt! # Last trade timestamp + chainId: Int! +} diff --git a/scripts/analyze-deposits.js b/scripts/analyze-deposits.js new file mode 100644 index 0000000..6c1fd1a --- /dev/null +++ b/scripts/analyze-deposits.js @@ -0,0 +1,126 @@ +#!/usr/bin/env node + +/** + * Analyze deposit sources to understand what's being captured + */ + +const GRAPHQL_ENDPOINT = 'https://indexer.dev.hyperindex.xyz/b318773/v1/graphql'; + +async function queryGraphQL(query) { + const response = await fetch(GRAPHQL_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); +} + +async function analyzeDeposits() { + console.log('🔍 Analyzing Deposit Sources...\n'); + + // Count deposits by unique from addresses + const uniqueFromQuery = ` + query { + AquaberaDeposit(distinct_on: from) { + from + } + } + `; + + // Get a sample of deposits with full details + const sampleDepositsQuery = ` + query { + AquaberaDeposit(limit: 20, order_by: {amount: desc}) { + id + amount + shares + from + isWallContribution + blockNumber + transactionHash + } + } + `; + + // Check for any deposits with isWallContribution = true + const wallDepositsQuery = ` + query { + AquaberaDeposit(where: {isWallContribution: {_eq: true}}, limit: 10) { + id + amount + from + transactionHash + } + } + `; + + try { + // Get unique depositors + console.log('📊 Unique Depositors:'); + const uniqueResult = await queryGraphQL(uniqueFromQuery); + const uniqueAddresses = uniqueResult.data?.AquaberaDeposit || []; + console.log(` Total unique addresses: ${uniqueAddresses.length}`); + + // Check for wall address + const wallAddress = '0x05c98986fc75d63ef973c648f22687d1a8056cd6'; + const hasWallAddress = uniqueAddresses.some( + item => item.from.toLowerCase() === wallAddress.toLowerCase() + ); + console.log(` Wall contract found: ${hasWallAddress ? '✅ YES' : '❌ NO'}`); + + // Get sample of largest deposits + console.log('\n💰 Largest Deposits (by amount):'); + const sampleResult = await queryGraphQL(sampleDepositsQuery); + const samples = sampleResult.data?.AquaberaDeposit || []; + + samples.slice(0, 5).forEach((deposit, index) => { + const amountInBera = (BigInt(deposit.amount) / BigInt(10**18)).toString(); + console.log(`\n ${index + 1}. Amount: ${amountInBera} BERA`); + console.log(` From: ${deposit.from}`); + console.log(` Block: ${deposit.blockNumber}`); + console.log(` Is Wall: ${deposit.isWallContribution}`); + console.log(` TX: ${deposit.transactionHash.slice(0, 10)}...`); + }); + + // Check for wall deposits + console.log('\n🏗️ Wall Contributions:'); + const wallResult = await queryGraphQL(wallDepositsQuery); + const wallDeposits = wallResult.data?.AquaberaDeposit || []; + + if (wallDeposits.length > 0) { + console.log(` Found ${wallDeposits.length} wall contributions`); + wallDeposits.forEach((deposit, index) => { + const amountInBera = (BigInt(deposit.amount) / BigInt(10**18)).toString(); + console.log(` ${index + 1}. ${amountInBera} BERA from ${deposit.from}`); + }); + } else { + console.log(' ❌ No deposits marked as wall contributions'); + } + + // Analysis + console.log('\n🔍 Analysis:'); + console.log(` Total deposits indexed: ${samples.length > 0 ? '✅ YES' : '❌ NO'}`); + console.log(` Wall address in depositors: ${hasWallAddress ? '✅ YES' : '❌ NO'}`); + console.log(` Wall contributions marked: ${wallDeposits.length > 0 ? '✅ YES' : '❌ NO'}`); + + if (!hasWallAddress) { + console.log('\n ⚠️ The wall contract address is NOT in the depositors list!'); + console.log(' This means either:'); + console.log(' 1. The wall deposits are not being captured by the indexer'); + console.log(' 2. The Deposit event is not being emitted for wall transactions'); + console.log(' 3. The vault might be using a different event signature'); + } + + } catch (error) { + console.error('Error:', error); + } +} + +analyzeDeposits(); \ No newline at end of file diff --git a/scripts/check-aquabera-stats.js b/scripts/check-aquabera-stats.js new file mode 100644 index 0000000..7fdd16e --- /dev/null +++ b/scripts/check-aquabera-stats.js @@ -0,0 +1,214 @@ +#!/usr/bin/env node + +/** + * Diagnostic script to check Aquabera stats and identify issues + */ + +// GraphQL endpoint - update if needed +const GRAPHQL_ENDPOINT = 'https://indexer.dev.hyperindex.xyz/b318773/v1/graphql'; + +async function queryGraphQL(query) { + const response = await fetch(GRAPHQL_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); +} + +async function checkAquaberaStats() { + console.log('🔍 Checking Aquabera Stats...\n'); + + // Query global stats + const statsQuery = ` + query { + aquaberaStats(where: { id_eq: "global" }) { + id + totalBera + totalShares + totalDeposited + totalWithdrawn + uniqueBuilders + depositCount + withdrawalCount + wallContributions + wallDepositCount + lastUpdateTime + } + } + `; + + // Query recent deposits + const depositsQuery = ` + query { + aquaberaDeposits(orderBy: timestamp_DESC, limit: 10) { + id + amount + shares + from + isWallContribution + timestamp + transactionHash + } + } + `; + + // Query wall contract builder + const wallBuilderQuery = ` + query { + aquaberaBuilder(id: "0x05c98986fc75d63ef973c648f22687d1a8056cd6") { + id + address + totalDeposited + totalWithdrawn + netDeposited + currentShares + depositCount + withdrawalCount + isWallContract + } + } + `; + + // Query top builders + const topBuildersQuery = ` + query { + aquaberaBuilders(orderBy: totalDeposited_DESC, limit: 5) { + id + address + totalDeposited + totalWithdrawn + netDeposited + currentShares + depositCount + isWallContract + } + } + `; + + try { + // Get global stats + console.log('📊 Global Stats:'); + const statsResult = await queryGraphQL(statsQuery); + const stats = statsResult.data?.aquaberaStats?.[0]; + + if (stats) { + console.log(` Total BERA Value: ${formatBigInt(stats.totalBera)} BERA`); + console.log(` Total LP Shares: ${formatBigInt(stats.totalShares)}`); + console.log(` Total Deposited: ${formatBigInt(stats.totalDeposited)} BERA`); + console.log(` Total Withdrawn: ${formatBigInt(stats.totalWithdrawn)} BERA`); + console.log(` Unique Builders: ${stats.uniqueBuilders}`); + console.log(` Deposit Count: ${stats.depositCount}`); + console.log(` Wall Contributions: ${formatBigInt(stats.wallContributions)} BERA`); + console.log(` Wall Deposit Count: ${stats.wallDepositCount}`); + console.log(` Last Update: ${new Date(Number(stats.lastUpdateTime) * 1000).toISOString()}`); + } else { + console.log(' ❌ No global stats found!'); + } + + // Get wall builder stats + console.log('\n🏗️ Wall Contract (Poku Trump) Stats:'); + const wallResult = await queryGraphQL(wallBuilderQuery); + const wallBuilder = wallResult.data?.aquaberaBuilder; + + if (wallBuilder) { + console.log(` Address: ${wallBuilder.address}`); + console.log(` Total Deposited: ${formatBigInt(wallBuilder.totalDeposited)} BERA`); + console.log(` Net Deposited: ${formatBigInt(wallBuilder.netDeposited)} BERA`); + console.log(` Current Shares: ${formatBigInt(wallBuilder.currentShares)}`); + console.log(` Deposit Count: ${wallBuilder.depositCount}`); + console.log(` Is Wall Contract: ${wallBuilder.isWallContract}`); + } else { + console.log(' ❌ Wall contract builder not found!'); + } + + // Get recent deposits + console.log('\n📝 Recent Deposits:'); + const depositsResult = await queryGraphQL(depositsQuery); + const deposits = depositsResult.data?.aquaberaDeposits || []; + + if (deposits.length > 0) { + deposits.forEach((deposit, index) => { + console.log(` ${index + 1}. Amount: ${formatBigInt(deposit.amount)} BERA`); + console.log(` Shares: ${formatBigInt(deposit.shares)}`); + console.log(` From: ${deposit.from}`); + console.log(` Wall Contribution: ${deposit.isWallContribution}`); + console.log(` TX: ${deposit.transactionHash}`); + console.log(` Time: ${new Date(Number(deposit.timestamp) * 1000).toISOString()}`); + console.log(''); + }); + } else { + console.log(' ❌ No deposits found!'); + } + + // Get top builders + console.log('\n🏆 Top Builders:'); + const buildersResult = await queryGraphQL(topBuildersQuery); + const builders = buildersResult.data?.aquaberaBuilders || []; + + if (builders.length > 0) { + builders.forEach((builder, index) => { + console.log(` ${index + 1}. ${builder.address.slice(0, 8)}...`); + console.log(` Total Deposited: ${formatBigInt(builder.totalDeposited)} BERA`); + console.log(` Net Deposited: ${formatBigInt(builder.netDeposited)} BERA`); + console.log(` Deposits: ${builder.depositCount}`); + console.log(` Is Wall: ${builder.isWallContract}`); + console.log(''); + }); + } else { + console.log(' ❌ No builders found!'); + } + + // Analysis + console.log('\n🔍 Analysis:'); + if (stats) { + if (stats.totalBera === '0' && stats.depositCount > 0) { + console.log(' ⚠️ Issue: totalBera is 0 despite having deposits!'); + console.log(' Possible causes:'); + console.log(' - Event parameters are being misinterpreted'); + console.log(' - BigInt conversion issues'); + console.log(' - Wrong field mapping in handlers'); + } + + if (stats.wallContributions === '0' && stats.wallDepositCount > 0) { + console.log(' ⚠️ Issue: wallContributions is 0 despite having wall deposits!'); + console.log(' Possible causes:'); + console.log(' - Wall contract address not being detected correctly'); + console.log(' - isWallContribution logic issue'); + } + + if (stats.totalBera !== '0' || stats.wallContributions !== '0') { + console.log(' ✅ Stats appear to be tracking correctly!'); + } + } + + } catch (error) { + console.error('Error querying GraphQL:', error); + } +} + +function formatBigInt(value) { + if (!value) return '0'; + + // Convert to string if BigInt + const str = value.toString(); + + // If it's a large number (likely in wei), convert to more readable format + if (str.length > 18) { + const whole = str.slice(0, -18) || '0'; + const decimal = str.slice(-18).slice(0, 4); + return `${whole}.${decimal}`; + } + + return str; +} + +// Run the check +checkAquaberaStats(); \ No newline at end of file diff --git a/scripts/latest_new_events.graphql b/scripts/latest_new_events.graphql new file mode 100644 index 0000000..9355f89 --- /dev/null +++ b/scripts/latest_new_events.graphql @@ -0,0 +1,103 @@ +# Multi-block sanity check for the newly tracked sources. +query LatestNewEvents { + candiesMints: Erc1155MintEvent( + where: { + collectionKey: { _eq: "mibera_drugs" } + chainId: { _eq: 80094 } + } + order_by: { timestamp: desc } + limit: 3 + ) { + id + collectionKey + tokenId + value + minter + operator + timestamp + blockNumber + transactionHash + } + + miberaVmMints: MintEvent( + where: { + collectionKey: { _eq: "mibera_vm" } + chainId: { _eq: 80094 } + } + order_by: { timestamp: desc } + limit: 3 + ) { + id + collectionKey + tokenId + minter + timestamp + blockNumber + transactionHash + } + + henloBurns: HenloBurn( + where: { chainId: { _eq: 80094 } } + order_by: { timestamp: desc } + limit: 3 + ) { + id + amount + source + from + timestamp + blockNumber + transactionHash + } + + aquaberaLiquidity: AquaberaDeposit( + where: { chainId: { _eq: 80094 } } + order_by: { timestamp: desc } + limit: 3 + ) { + id + amount + shares + from + isWallContribution + timestamp + blockNumber + transactionHash + } + + fatBeraDeposits: FatBeraDeposit( + where: { collectionKey: { _eq: "fatbera_deposit" } } + order_by: { timestamp: desc } + limit: 3 + ) { + id + depositor + recipient + amount + shares + transactionFrom + timestamp + blockNumber + transactionHash + } + + bgtBoosts: BgtBoostEvent( + where: { + validatorPubkey: { + _eq: "0xa0c673180d97213c1c35fe3bf4e684dd3534baab235a106d1f71b9c8a37e4d37a056d47546964fd075501dff7f76aeaf" + } + chainId: { _eq: 80094 } + } + order_by: { timestamp: desc } + limit: 3 + ) { + id + account + validatorPubkey + amount + transactionFrom + timestamp + blockNumber + transactionHash + } +} diff --git a/src/EventHandlers.ts b/src/EventHandlers.ts index 2c85a40..b2e25df 100644 --- a/src/EventHandlers.ts +++ b/src/EventHandlers.ts @@ -1,517 +1,258 @@ /* - * Please refer to https://docs.envio.dev for a thorough guide on all Envio indexer features + * THJ Indexer - Main Event Handler Entry Point + * + * This file imports and registers all event handlers from modular files. + * Each product/feature has its own handler module for better maintainability. */ + +// Import HoneyJar NFT handlers +import { + handleHoneyJarTransfer, + handleHoneycombTransfer, + handleHoneyJar2EthTransfer, + handleHoneyJar3EthTransfer, + handleHoneyJar4EthTransfer, + handleHoneyJar5EthTransfer, +} from "./handlers/honey-jar-nfts"; + +// Import MoneycombVault handlers +import { + handleAccountOpened, + handleAccountClosed, + handleHJBurned, + handleSharesMinted, + handleRewardClaimed, +} from "./handlers/moneycomb-vault"; + +// Import Aquabera wall tracking handlers (forwarder events) +import { + handleAquaberaDeposit, + // handleAquaberaWithdraw, // Not implemented - forwarder doesn't emit withdrawal events +} from "./handlers/aquabera-wall"; + +// Crayons factory + collections (skeleton) +import { handleCrayonsFactoryNewBase } from "./handlers/crayons"; +import { handleCrayonsErc721Transfer } from "./handlers/crayons-collections"; +import { handleTrackedErc721Transfer } from "./handlers/tracked-erc721"; +// Import Aquabera direct vault handlers +import { + handleDirectDeposit, + handleDirectWithdraw, +} from "./handlers/aquabera-vault-direct"; +// General mint tracking +import { handleGeneralMintTransfer } from "./handlers/mints"; +import { handleVmMinted } from "./handlers/vm-minted"; +import { + handleCandiesMintSingle, + handleCandiesMintBatch, +} from "./handlers/mints1155"; +import { handleFatBeraDeposit } from "./handlers/fatbera"; +import { handleBgtQueueBoost } from "./handlers/bgt"; +import { + handleCubBadgesTransferSingle, + handleCubBadgesTransferBatch, +} from "./handlers/badges1155"; + +// Set & Forgetti vault handlers +import { + handleSFVaultDeposit, + handleSFVaultWithdraw, + handleSFVaultStrategyUpdated, + handleSFMultiRewardsStaked, + handleSFMultiRewardsWithdrawn, + handleSFMultiRewardsRewardPaid, +} from "./handlers/sf-vaults"; + +// Tracked ERC-20 token balance handler (HENLO + HENLOCKED tiers) +import { handleTrackedErc20Transfer } from "./handlers/tracked-erc20"; + +// HenloVault handlers (HENLOCKED token mints + Henlocker vault system) +import { + handleHenloVaultMint, + handleHenloVaultRoundOpened, + handleHenloVaultRoundClosed, + handleHenloVaultDepositsPaused, + handleHenloVaultDepositsUnpaused, + handleHenloVaultMintFromReservoir, + handleHenloVaultRedeem, + handleHenloVaultReservoirSet, +} from "./handlers/henlo-vault"; + +// Mibera Treasury handlers (defaulted NFT marketplace + loan system) import { - HoneyJar, - HoneyJar_Approval, - HoneyJar_ApprovalForAll, - HoneyJar_BaseURISet, - HoneyJar_OwnershipTransferred, - HoneyJar_SetGenerated, - HoneyJar_Transfer, - MoneycombVault, - Transfer, - Holder, - CollectionStat, - Mint, - UserBalance, - Vault, - VaultActivity, - UserVaultSummary, -} from "generated"; - -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; -const ADDRESS_TO_COLLECTION: Record = { - // mainnet - "0xa20cf9b0874c3e46b344deaaea9c2e0c3e1db37d": "HoneyJar1", - "0x98dc31a9648f04e23e4e36b0456d1951531c2a05": "HoneyJar6", - "0xcb0477d1af5b8b05795d89d59f4667b59eae9244": "Honeycomb", - // arbitrum - "0x1b2751328f41d1a0b91f3710edcd33e996591b72": "HoneyJar2", - // zora - "0xe798c4d40bc050bc93c7f3b149a0dfe5cfc49fb0": "HoneyJar3", - // optimism - "0xe1d16cc75c9f39a2e0f5131eb39d4b634b23f301": "HoneyJar4", - // base - "0xbad7b49d985bbfd3a22706c447fb625a28f048b4": "HoneyJar5", - // berachain (map to base collections) - "0xedc5dfd6f37464cc91bbce572b6fe2c97f1bc7b3": "HoneyJar1", - "0x1c6c24cac266c791c4ba789c3ec91f04331725bd": "HoneyJar2", - "0xf1e4a550772fabfc35b28b51eb8d0b6fcd1c4878": "HoneyJar3", - "0xdb602ab4d6bd71c8d11542a9c8c936877a9a4f45": "HoneyJar4", - "0x0263728e7f59f315c17d3c180aeade027a375f17": "HoneyJar5", - "0xb62a9a21d98478f477e134e175fd2003c15cb83a": "HoneyJar6", - "0x886d2176d899796cd1affa07eff07b9b2b80f1be": "Honeycomb", -}; - -const COLLECTION_TO_GENERATION: Record = { - HoneyJar1: 1, - HoneyJar2: 2, - HoneyJar3: 3, - HoneyJar4: 4, - HoneyJar5: 5, - HoneyJar6: 6, - Honeycomb: 0, -}; - -const HOME_CHAIN_IDS: Record = { - 1: 1, - 2: 42161, - 3: 7777777, - 4: 10, - 5: 8453, - 6: 1, - 0: 1, -}; - -HoneyJar.Transfer.handler(async ({ event, context }) => { - // Keep the original simple event entity for reference/testing - const basic: HoneyJar_Transfer = { - id: `${event.chainId}_${event.block.number}_${event.logIndex}`, - from: event.params.from, - to: event.params.to, - tokenId: event.params.tokenId, - }; - context.HoneyJar_Transfer.set(basic); - - const from = event.params.from.toLowerCase(); - const to = event.params.to.toLowerCase(); - const tokenId = event.params.tokenId; - const timestamp = BigInt(event.block.timestamp); - const blockNumber = BigInt(event.block.number); - const chainId = event.chainId; - const txHash = event.transaction.hash; - const isMint = from === ZERO_ADDRESS; - - const contractAddress = event.srcAddress.toLowerCase(); - const collection = ADDRESS_TO_COLLECTION[contractAddress] ?? "unknown"; - - const transferId = `${collection}-${txHash}-${event.logIndex}`; - const transferEntity: Transfer = { - id: transferId, - tokenId, - from, - to, - timestamp, - blockNumber, - transactionHash: txHash, - collection, - chainId, - }; - context.Transfer.set(transferEntity); - - // Track mints separately for activity feed - if (isMint) { - const mintId = `${collection}-${chainId}-${txHash}-${event.logIndex}`; - const mintEntity: Mint = { - id: mintId, - tokenId, - to, - timestamp, - blockNumber, - transactionHash: txHash, - collection, - chainId, - }; - context.Mint.set(mintEntity); - } - - // Update holders - if (!isMint) { - const fromHolderId = `${from}-${collection}-${chainId}`; - const fromHolder = await context.Holder.get(fromHolderId); - if (fromHolder) { - const updatedFrom: Holder = { - ...fromHolder, - balance: Math.max(0, fromHolder.balance - 1), - lastActivityTime: timestamp, - }; - context.Holder.set(updatedFrom); - } - } - - let isNewToHolder = false; - if (to !== ZERO_ADDRESS) { - const toHolderId = `${to}-${collection}-${chainId}`; - const existingTo = await context.Holder.get(toHolderId); - if (existingTo) { - const updatedTo: Holder = { - ...existingTo, - balance: existingTo.balance + 1, - totalMinted: isMint - ? existingTo.totalMinted + 1 - : existingTo.totalMinted, - lastActivityTime: timestamp, - }; - context.Holder.set(updatedTo); - } else { - isNewToHolder = true; - const newTo: Holder = { - id: toHolderId, - address: to, - balance: 1, - totalMinted: isMint ? 1 : 0, - lastActivityTime: timestamp, - firstMintTime: isMint ? timestamp : undefined, - collection, - chainId, - }; - context.Holder.set(newTo); - } - } - - // Update cross-chain user balance summary - const generation = COLLECTION_TO_GENERATION[collection] ?? -1; - const isBerachain = chainId === 80094; - const homeChainId = HOME_CHAIN_IDS[generation]; - const isHomeChain = chainId === homeChainId; - - if (generation >= 0) { - // From user (transfer out) - if (!isMint) { - const fromUserId = `${from}-gen${generation}`; - const fromUser = await context.UserBalance.get(fromUserId); - if (fromUser) { - const newHomeBalance = isHomeChain - ? Math.max(0, fromUser.balanceHomeChain - 1) - : fromUser.balanceHomeChain; - const newBeraBalance = isBerachain - ? Math.max(0, fromUser.balanceBerachain - 1) - : fromUser.balanceBerachain; - const updatedFromUser: UserBalance = { - ...fromUser, - balanceHomeChain: newHomeBalance, - balanceBerachain: newBeraBalance, - balanceTotal: newHomeBalance + newBeraBalance, - lastActivityTime: timestamp, - }; - context.UserBalance.set(updatedFromUser); - } - } - - // To user (transfer in) - if (to !== ZERO_ADDRESS) { - const toUserId = `${to}-gen${generation}`; - const toUser = await context.UserBalance.get(toUserId); - if (toUser) { - const newHomeBalance = isHomeChain - ? toUser.balanceHomeChain + 1 - : toUser.balanceHomeChain; - const newBeraBalance = isBerachain - ? toUser.balanceBerachain + 1 - : toUser.balanceBerachain; - const newMintedHome = - isMint && isHomeChain - ? toUser.mintedHomeChain + 1 - : toUser.mintedHomeChain; - const newMintedBera = - isMint && isBerachain - ? toUser.mintedBerachain + 1 - : toUser.mintedBerachain; - const updatedToUser: UserBalance = { - ...toUser, - balanceHomeChain: newHomeBalance, - balanceBerachain: newBeraBalance, - balanceTotal: newHomeBalance + newBeraBalance, - mintedHomeChain: newMintedHome, - mintedBerachain: newMintedBera, - mintedTotal: newMintedHome + newMintedBera, - lastActivityTime: timestamp, - }; - context.UserBalance.set(updatedToUser); - } else { - const newUser: UserBalance = { - id: toUserId, - address: to, - generation, - balanceHomeChain: isHomeChain ? 1 : 0, - balanceBerachain: isBerachain ? 1 : 0, - balanceTotal: 1, - mintedHomeChain: isMint && isHomeChain ? 1 : 0, - mintedBerachain: isMint && isBerachain ? 1 : 0, - mintedTotal: isMint ? 1 : 0, - lastActivityTime: timestamp, - firstMintTime: isMint ? timestamp : undefined, - }; - context.UserBalance.set(newUser); - } - } - } - - // Update collection stats - const statsId = `${collection}-${chainId}`; - const existingStats = await context.CollectionStat.get(statsId); - const currentTokenId = Number(tokenId); - - if (existingStats) { - const shouldUpdateSupply = - currentTokenId > (existingStats.totalSupply || 0); - const updatedStats: CollectionStat = { - ...existingStats, - totalSupply: shouldUpdateSupply - ? currentTokenId - : existingStats.totalSupply, - lastMintTime: isMint ? timestamp : existingStats.lastMintTime, - uniqueHolders: - to !== ZERO_ADDRESS && isNewToHolder - ? existingStats.uniqueHolders + 1 - : existingStats.uniqueHolders, - }; - context.CollectionStat.set(updatedStats); - } else { - const initialStats: CollectionStat = { - id: statsId, - collection, - totalSupply: currentTokenId, - uniqueHolders: to !== ZERO_ADDRESS ? 1 : 0, - lastMintTime: isMint ? timestamp : undefined, - chainId, - }; - context.CollectionStat.set(initialStats); - } -}); - -// ============================== -// Moneycomb Vault Event Handlers -// ============================== - -MoneycombVault.AccountOpened.handler(async ({ event, context }) => { - const user = event.params.user.toLowerCase(); - const accountIndex = Number(event.params.accountIndex); - const honeycombId = event.params.honeycombId; - const timestamp = BigInt(event.block.timestamp); - - const vaultId = `${user}-${accountIndex}`; - const activityId = `${event.transaction.hash}-${event.logIndex}`; - - const newVault: Vault = { - id: vaultId, - user, - accountIndex, - honeycombId, - isActive: true, - shares: BigInt(0), - totalBurned: 0, - burnedGen1: false, - burnedGen2: false, - burnedGen3: false, - burnedGen4: false, - burnedGen5: false, - burnedGen6: false, - createdAt: timestamp, - closedAt: undefined, - lastActivityTime: timestamp, - }; - context.Vault.set(newVault); - - const newActivity: VaultActivity = { - id: activityId, - user, - accountIndex, - activityType: "opened", - timestamp, - blockNumber: BigInt(event.block.number), - transactionHash: event.transaction.hash, - honeycombId, - hjGen: undefined, - shares: undefined, - reward: undefined, - }; - context.VaultActivity.set(newActivity); - - const summary = await context.UserVaultSummary.get(user); - if (summary) { - const updated: UserVaultSummary = { - ...summary, - totalVaults: summary.totalVaults + 1, - activeVaults: summary.activeVaults + 1, - lastActivityTime: timestamp, - }; - context.UserVaultSummary.set(updated); - } else { - const created: UserVaultSummary = { - id: user, - user, - totalVaults: 1, - activeVaults: 1, - totalShares: BigInt(0), - totalRewardsClaimed: BigInt(0), - totalHJsBurned: 0, - firstVaultTime: timestamp, - lastActivityTime: timestamp, - }; - context.UserVaultSummary.set(created); - } -}); - -MoneycombVault.HJBurned.handler(async ({ event, context }) => { - const user = event.params.user.toLowerCase(); - const accountIndex = Number(event.params.accountIndex); - const hjGen = Number(event.params.hjGen); - const timestamp = BigInt(event.block.timestamp); - - const vaultId = `${user}-${accountIndex}`; - const activityId = `${event.transaction.hash}-${event.logIndex}`; - - const vault = await context.Vault.get(vaultId); - if (vault) { - const updated: Vault = { - ...vault, - totalBurned: vault.totalBurned + 1, - lastActivityTime: timestamp, - ...(Object.fromEntries([ - [`burnedGen${hjGen}`, true], - ]) as unknown as Partial), - } as Vault; - context.Vault.set(updated); - } - - const activity: VaultActivity = { - id: activityId, - user, - accountIndex, - activityType: "burned", - timestamp, - blockNumber: BigInt(event.block.number), - transactionHash: event.transaction.hash, - hjGen, - honeycombId: undefined, - shares: undefined, - reward: undefined, - }; - context.VaultActivity.set(activity); - - const summary = await context.UserVaultSummary.get(user); - if (summary) { - const updatedSummary: UserVaultSummary = { - ...summary, - totalHJsBurned: summary.totalHJsBurned + 1, - lastActivityTime: timestamp, - }; - context.UserVaultSummary.set(updatedSummary); - } -}); - -MoneycombVault.SharesMinted.handler(async ({ event, context }) => { - const user = event.params.user.toLowerCase(); - const accountIndex = Number(event.params.accountIndex); - const shares = event.params.shares; - const timestamp = BigInt(event.block.timestamp); - - const vaultId = `${user}-${accountIndex}`; - const activityId = `${event.transaction.hash}-${event.logIndex}`; - - const vault = await context.Vault.get(vaultId); - if (vault) { - const updated: Vault = { - ...vault, - shares: vault.shares + shares, - lastActivityTime: timestamp, - }; - context.Vault.set(updated); - } - - const activity: VaultActivity = { - id: activityId, - user, - accountIndex, - activityType: "shares_minted", - timestamp, - blockNumber: BigInt(event.block.number), - transactionHash: event.transaction.hash, - shares, - hjGen: undefined, - honeycombId: undefined, - reward: undefined, - }; - context.VaultActivity.set(activity); - - const summary = await context.UserVaultSummary.get(user); - if (summary) { - const updatedSummary: UserVaultSummary = { - ...summary, - totalShares: summary.totalShares + shares, - lastActivityTime: timestamp, - }; - context.UserVaultSummary.set(updatedSummary); - } -}); - -MoneycombVault.RewardClaimed.handler(async ({ event, context }) => { - const user = event.params.user.toLowerCase(); - const reward = event.params.reward; - const timestamp = BigInt(event.block.timestamp); - - const activityId = `${event.transaction.hash}-${event.logIndex}`; - - const activity: VaultActivity = { - id: activityId, - user, - accountIndex: 0, - activityType: "claimed", - timestamp, - blockNumber: BigInt(event.block.number), - transactionHash: event.transaction.hash, - reward, - hjGen: undefined, - honeycombId: undefined, - shares: undefined, - }; - context.VaultActivity.set(activity); - - const summary = await context.UserVaultSummary.get(user); - if (summary) { - const updatedSummary: UserVaultSummary = { - ...summary, - totalRewardsClaimed: summary.totalRewardsClaimed + reward, - lastActivityTime: timestamp, - }; - context.UserVaultSummary.set(updatedSummary); - } -}); - -MoneycombVault.AccountClosed.handler(async ({ event, context }) => { - const user = event.params.user.toLowerCase(); - const accountIndex = Number(event.params.accountIndex); - const honeycombId = event.params.honeycombId; - const timestamp = BigInt(event.block.timestamp); - - const vaultId = `${user}-${accountIndex}`; - const activityId = `${event.transaction.hash}-${event.logIndex}`; - - const vault = await context.Vault.get(vaultId); - if (vault) { - const updated: Vault = { - ...vault, - isActive: false, - closedAt: timestamp, - lastActivityTime: timestamp, - }; - context.Vault.set(updated); - } - - const activity: VaultActivity = { - id: activityId, - user, - accountIndex, - activityType: "closed", - timestamp, - blockNumber: BigInt(event.block.number), - transactionHash: event.transaction.hash, - honeycombId, - hjGen: undefined, - shares: undefined, - reward: undefined, - }; - context.VaultActivity.set(activity); - - const summary = await context.UserVaultSummary.get(user); - if (summary) { - const updatedSummary: UserVaultSummary = { - ...summary, - activeVaults: Math.max(0, summary.activeVaults - 1), - lastActivityTime: timestamp, - }; - context.UserVaultSummary.set(updatedSummary); - } -}); + handleLoanReceived, + handleBackingLoanPayedBack, + handleBackingLoanExpired, + handleItemLoaned, + handleLoanItemSentBack, + handleItemLoanExpired, + handleItemPurchased, + handleItemRedeemed, + handleRFVChanged, +} from "./handlers/mibera-treasury"; + +// Mibera Collection handlers (transfer/mint/burn tracking) +import { handleMiberaCollectionTransfer } from "./handlers/mibera-collection"; + +// Milady Collection handlers (burn tracking on ETH mainnet) +import { handleMiladyCollectionTransfer } from "./handlers/milady-collection"; + +// Mibera Premint handlers (participation/refund tracking) +import { + handlePremintParticipated, + handlePremintRefunded, +} from "./handlers/mibera-premint"; + +// Mibera Sets handlers (ERC-1155 on Optimism) +import { + handleMiberaSetsSingle, + handleMiberaSetsBatch, +} from "./handlers/mibera-sets"; + +// Mibera Zora handlers (ERC-1155 on Optimism via Zora platform) +import { + handleMiberaZoraSingle, + handleMiberaZoraBatch, +} from "./handlers/mibera-zora"; + +// friend.tech handlers (key trading on Base) +import { handleFriendtechTrade } from "./handlers/friendtech"; + +// Seaport marketplace handlers (secondary sales tracking) +import { handleSeaportOrderFulfilled } from "./handlers/seaport"; + +// PaddleFi lending handlers (BERA supply + NFT pawn) +import { handlePaddleMint, handlePaddlePawn } from "./handlers/paddlefi"; + +// Trading system handlers +// TODO: Fix TypeScript errors in trade handlers before uncommenting +// import { +// handleMiberaTradeProposed, +// handleMiberaTradeAccepted, +// handleMiberaTradeCancelled, +// } from "./handlers/mibera-trades"; +// import { +// handleCandiesTradeProposed, +// handleCandiesTradeAccepted, +// handleCandiesTradeCancelled, +// } from "./handlers/cargo-trades"; + +// Mibera staking tracking - REMOVED: Now handled by TrackedErc721 handler +// import { handleMiberaStakingTransfer } from "./handlers/mibera-staking"; + +/* + * Export all handlers for Envio to register + * + * The handlers are already defined with their event bindings in the module files. + * This re-export makes them available to Envio's event processing system. + */ + +// HoneyJar NFT Transfer handlers +export { handleHoneyJarTransfer }; +export { handleHoneycombTransfer }; +export { handleHoneyJar2EthTransfer }; +export { handleHoneyJar3EthTransfer }; +export { handleHoneyJar4EthTransfer }; +export { handleHoneyJar5EthTransfer }; + +// MoneycombVault handlers +export { handleAccountOpened }; +export { handleAccountClosed }; +export { handleHJBurned }; +export { handleSharesMinted }; +export { handleRewardClaimed }; + +// Aquabera wall tracking handlers (forwarder) +export { handleAquaberaDeposit }; +// export { handleAquaberaWithdraw }; // Not implemented - forwarder doesn't emit withdrawal events + +// Aquabera direct vault handlers +export { handleDirectDeposit }; +export { handleDirectWithdraw }; + +// Crayons handlers +export { handleCrayonsFactoryNewBase }; +export { handleCrayonsErc721Transfer }; +export { handleTrackedErc721Transfer }; + +// General mint handlers +export { handleGeneralMintTransfer }; +export { handleVmMinted }; +export { handleCandiesMintSingle }; +export { handleCandiesMintBatch }; +export { handleFatBeraDeposit }; +export { handleBgtQueueBoost }; +export { handleCubBadgesTransferSingle }; +export { handleCubBadgesTransferBatch }; + +// Set & Forgetti vault handlers +export { handleSFVaultDeposit }; +export { handleSFVaultWithdraw }; +export { handleSFVaultStrategyUpdated }; +export { handleSFMultiRewardsStaked }; +export { handleSFMultiRewardsWithdrawn }; +export { handleSFMultiRewardsRewardPaid }; + +// Tracked ERC-20 token balance handler +export { handleTrackedErc20Transfer }; + +// HenloVault handlers (HENLOCKED token mints + Henlocker vault system) +export { handleHenloVaultMint }; +export { handleHenloVaultRoundOpened }; +export { handleHenloVaultRoundClosed }; +export { handleHenloVaultDepositsPaused }; +export { handleHenloVaultDepositsUnpaused }; +export { handleHenloVaultMintFromReservoir }; +export { handleHenloVaultRedeem }; +export { handleHenloVaultReservoirSet }; + +// Trading system handlers +// TODO: Fix TypeScript errors in trade handlers before uncommenting +// export { handleMiberaTradeProposed }; +// export { handleMiberaTradeAccepted }; +// export { handleMiberaTradeCancelled }; +// export { handleCandiesTradeProposed }; +// export { handleCandiesTradeAccepted }; +// export { handleCandiesTradeCancelled }; + +// Mibera staking handlers - REMOVED: Now handled by TrackedErc721 handler +// export { handleMiberaStakingTransfer }; + +// Mibera Treasury handlers (defaulted NFT marketplace + loan system) +export { handleLoanReceived }; +export { handleBackingLoanPayedBack }; +export { handleBackingLoanExpired }; +export { handleItemLoaned }; +export { handleLoanItemSentBack }; +export { handleItemLoanExpired }; +export { handleItemPurchased }; +export { handleItemRedeemed }; +export { handleRFVChanged }; + +// Mibera Collection handlers (transfer/mint/burn tracking) +export { handleMiberaCollectionTransfer }; + +// Milady Collection handlers (burn tracking on ETH mainnet) +export { handleMiladyCollectionTransfer }; + +// Mibera Premint handlers (participation/refund tracking) +export { handlePremintParticipated }; +export { handlePremintRefunded }; + +// Mibera Sets handlers (ERC-1155 on Optimism) +export { handleMiberaSetsSingle }; +export { handleMiberaSetsBatch }; + +// Mibera Zora handlers (ERC-1155 on Optimism via Zora platform) +export { handleMiberaZoraSingle }; +export { handleMiberaZoraBatch }; + +// friend.tech handlers (key trading on Base) +export { handleFriendtechTrade }; + +// Seaport marketplace handlers (secondary sales tracking) +export { handleSeaportOrderFulfilled }; + +// PaddleFi lending handlers (BERA supply + NFT pawn) +export { handlePaddleMint }; +export { handlePaddlePawn }; diff --git a/src/SFVaultHandlers.ts b/src/SFVaultHandlers.ts new file mode 100644 index 0000000..d123c16 --- /dev/null +++ b/src/SFVaultHandlers.ts @@ -0,0 +1,24 @@ +/* + * SF Vaults - Dedicated Event Handler Entry Point + * + * This file is used for testing SF vaults in isolation. + * It only imports SF vault handlers to avoid type errors from other contracts. + */ + +// Set & Forgetti vault handlers +import { + handleSFVaultDeposit, + handleSFVaultWithdraw, + handleSFVaultStrategyUpdated, + handleSFMultiRewardsStaked, + handleSFMultiRewardsWithdrawn, + handleSFMultiRewardsRewardPaid, +} from "./handlers/sf-vaults"; + +// Export all SF vault handlers +export { handleSFVaultDeposit }; +export { handleSFVaultWithdraw }; +export { handleSFVaultStrategyUpdated }; +export { handleSFMultiRewardsStaked }; +export { handleSFMultiRewardsWithdrawn }; +export { handleSFMultiRewardsRewardPaid }; diff --git a/src/handlers/aquabera-vault-direct.ts b/src/handlers/aquabera-vault-direct.ts new file mode 100644 index 0000000..ef59a22 --- /dev/null +++ b/src/handlers/aquabera-vault-direct.ts @@ -0,0 +1,298 @@ +/* + * CORRECTED Aquabera Vault Handlers + * + * Tracks WBERA/HENLO deposits and withdrawals, not LP token amounts + * The vault is a WBERA/HENLO liquidity pool + */ + +import { + AquaberaVaultDirect, + AquaberaDeposit, + AquaberaWithdrawal, + AquaberaBuilder, + AquaberaStats, +} from "generated"; + +import { recordAction } from "../lib/actions"; + +const WALL_CONTRACT_ADDRESS = "0x05c98986Fc75D63eF973C648F22687d1a8056CD6".toLowerCase(); +const BERACHAIN_ID = 80094; + +/* + * Handle direct Deposit events (Uniswap V3 style pool) + * Event: Deposit(address indexed sender, address indexed to, uint256 shares, uint256 amount0, uint256 amount1) + * amount0 = WBERA amount + * amount1 = HENLO amount (usually 0 for single-sided deposits) + * shares = LP tokens minted + */ +export const handleDirectDeposit = AquaberaVaultDirect.Deposit.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const sender = event.params.sender.toLowerCase(); + const recipient = event.params.to.toLowerCase(); + + // IMPORTANT: Skip if this deposit came from the forwarder contract + // The forwarder already emits DepositForwarded which we track separately + const FORWARDER_ADDRESS = "0xc0c6d4178410849ec9765b4267a73f4f64241832"; + if (sender === FORWARDER_ADDRESS) { + // Silently skip - no logging needed + return; // Don't double-count forwarder deposits + } + + // Map the event parameters from the actual Deposit event + // Based on the actual events we've seen, the parameters are: + // Deposit(address indexed sender, address indexed to, uint256 shares, uint256 amount0, uint256 amount1) + const lpTokensReceived = event.params.shares; // LP tokens minted + const wberaAmount = event.params.amount0; // WBERA deposited (token0 in the pool) + const henloAmount = event.params.amount1; // HENLO deposited (token1 in the pool) + + // Check if it's a wall contribution - check both sender and recipient + const txFrom = event.transaction.from ? event.transaction.from.toLowerCase() : null; + const isWallContribution: boolean = + sender === WALL_CONTRACT_ADDRESS || + recipient === WALL_CONTRACT_ADDRESS || + (txFrom !== null && txFrom === WALL_CONTRACT_ADDRESS); + + // Verbose logging removed - uncomment for debugging if needed + // context.log.info( + // `📊 Direct Deposit Event: + // - Sender: ${sender} + // - To: ${recipient} + // - Shares (LP tokens): ${lpTokensReceived} + // - Amount0 (WBERA): ${wberaAmount} wei = ${wberaAmount / BigInt(10**18)} WBERA + // - Amount1 (HENLO): ${henloAmount} wei + // - TX From: ${txFrom || 'N/A'} + // - Is Wall: ${isWallContribution}` + // ); + + // Create deposit record with WBERA amount + const id = `${event.transaction.hash}_${event.logIndex}`; + const chainId = event.chainId; + + const deposit: AquaberaDeposit = { + id, + amount: wberaAmount, // Store WBERA amount, not LP tokens + shares: lpTokensReceived, + timestamp: timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + from: txFrom || sender, // Use sender if txFrom is not available + isWallContribution: isWallContribution, + chainId: BERACHAIN_ID, + }; + context.AquaberaDeposit.set(deposit); + + // Batch queries for parallel execution + const builderId = sender; + const statsId = "global"; + + const [builder, stats] = await Promise.all([ + context.AquaberaBuilder.get(builderId), + context.AquaberaStats.get(statsId), + ]); + + // Prepare builder (create if doesn't exist) + const builderToUpdate = builder || { + id: builderId, + address: builderId, + totalDeposited: BigInt(0), + totalWithdrawn: BigInt(0), + netDeposited: BigInt(0), + currentShares: BigInt(0), + depositCount: 0, + withdrawalCount: 0, + firstDepositTime: timestamp, + lastActivityTime: timestamp, + isWallContract: builderId === WALL_CONTRACT_ADDRESS, + chainId: BERACHAIN_ID, + }; + + const updatedBuilder = { + ...builderToUpdate, + totalDeposited: builderToUpdate.totalDeposited + wberaAmount, // Track WBERA + netDeposited: builderToUpdate.netDeposited + wberaAmount, + currentShares: builderToUpdate.currentShares + lpTokensReceived, // Track LP tokens separately + depositCount: builderToUpdate.depositCount + 1, + lastActivityTime: timestamp, + isWallContract: builderToUpdate.isWallContract || (builderId === WALL_CONTRACT_ADDRESS), + }; + context.AquaberaBuilder.set(updatedBuilder); + + // Prepare global stats (create if doesn't exist) + const statsToUpdate = stats || { + id: statsId, + totalBera: BigInt(0), // This tracks WBERA, not LP tokens + totalShares: BigInt(0), // This tracks LP tokens + totalDeposited: BigInt(0), + totalWithdrawn: BigInt(0), + uniqueBuilders: 0, + depositCount: 0, + withdrawalCount: 0, + wallContributions: BigInt(0), + wallDepositCount: 0, + lastUpdateTime: timestamp, + chainId: BERACHAIN_ID, + }; + + const uniqueBuildersIncrement = !builder || builder.depositCount === 0 ? 1 : 0; + + const updatedStats = { + ...statsToUpdate, + totalBera: statsToUpdate.totalBera + wberaAmount, // Add WBERA amount + totalShares: statsToUpdate.totalShares + lpTokensReceived, // Track LP tokens separately + totalDeposited: statsToUpdate.totalDeposited + wberaAmount, + uniqueBuilders: statsToUpdate.uniqueBuilders + uniqueBuildersIncrement, + depositCount: statsToUpdate.depositCount + 1, + wallContributions: isWallContribution + ? statsToUpdate.wallContributions + wberaAmount + : statsToUpdate.wallContributions, + wallDepositCount: isWallContribution + ? statsToUpdate.wallDepositCount + 1 + : statsToUpdate.wallDepositCount, + lastUpdateTime: timestamp, + }; + context.AquaberaStats.set(updatedStats); + + // Verbose logging removed - uncomment for debugging if needed + // context.log.info( + // `Updated stats - Total WBERA: ${updatedStats.totalBera}, Total LP: ${updatedStats.totalShares}` + // ); + + recordAction(context, { + id, + actionType: "deposit", + actor: sender, + primaryCollection: "henlo_build", + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: wberaAmount, + numeric2: lpTokensReceived, + context: { + vault: event.srcAddress.toLowerCase(), + recipient, + henloAmount: henloAmount.toString(), + isWallContribution, + txFrom, + forwarder: false, + }, + }); + } +); + +/* + * Handle Withdraw events (Uniswap V3 style pool) + * Event: Withdraw(address indexed sender, address indexed to, uint256 shares, uint256 amount0, uint256 amount1) + * amount0 = WBERA amount withdrawn + * amount1 = HENLO amount withdrawn + * shares = LP tokens burned + */ +export const handleDirectWithdraw = AquaberaVaultDirect.Withdraw.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const sender = event.params.sender.toLowerCase(); + const recipient = event.params.to.toLowerCase(); + + // Skip if this withdrawal came from the forwarder contract + const FORWARDER_ADDRESS = "0xc0c6d4178410849ec9765b4267a73f4f64241832"; + if (sender === FORWARDER_ADDRESS) { + // Silently skip - no logging needed + return; + } + + // Map the event parameters from the actual Withdraw event + // Withdraw(address indexed sender, address indexed to, uint256 shares, uint256 amount0, uint256 amount1) + const lpTokensBurned = event.params.shares; // LP tokens burned + const wberaReceived = event.params.amount0; // WBERA withdrawn (token0) + const henloReceived = event.params.amount1; // HENLO withdrawn (token1) + + // Verbose logging removed - uncomment for debugging if needed + // context.log.info( + // `Withdraw: ${wberaReceived} WBERA for ${lpTokensBurned} LP tokens to ${recipient}` + // ); + + // Create withdrawal record with WBERA amount + const id = `${event.transaction.hash}_${event.logIndex}`; + const chainId = event.chainId; + + const withdrawal: AquaberaWithdrawal = { + id, + amount: wberaReceived, // Store WBERA amount, not LP tokens + shares: lpTokensBurned, + timestamp: timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + from: sender, // Use sender as the withdrawer + chainId: BERACHAIN_ID, + }; + context.AquaberaWithdrawal.set(withdrawal); + + // Batch queries for parallel execution + const builderId = sender; + const statsId = "global"; + + const [builder, stats] = await Promise.all([ + context.AquaberaBuilder.get(builderId), + context.AquaberaStats.get(statsId), + ]); + + // Update builder stats if exists + if (builder) { + const updatedBuilder = { + ...builder, + totalWithdrawn: builder.totalWithdrawn + wberaReceived, // Track WBERA + netDeposited: builder.netDeposited > wberaReceived + ? builder.netDeposited - wberaReceived + : BigInt(0), + currentShares: builder.currentShares > lpTokensBurned + ? builder.currentShares - lpTokensBurned + : BigInt(0), + withdrawalCount: builder.withdrawalCount + 1, + lastActivityTime: timestamp, + }; + context.AquaberaBuilder.set(updatedBuilder); + } + + // Update global stats - subtract WBERA withdrawn + + if (stats) { + const updatedStats = { + ...stats, + totalBera: stats.totalBera > wberaReceived + ? stats.totalBera - wberaReceived // Subtract WBERA amount + : BigInt(0), + totalShares: stats.totalShares > lpTokensBurned + ? stats.totalShares - lpTokensBurned // Subtract LP tokens + : BigInt(0), + totalWithdrawn: stats.totalWithdrawn + wberaReceived, + withdrawalCount: stats.withdrawalCount + 1, + lastUpdateTime: timestamp, + }; + context.AquaberaStats.set(updatedStats); + + // Verbose logging removed - uncomment for debugging if needed + // context.log.info( + // `Updated stats - Total WBERA: ${updatedStats.totalBera}, Total LP: ${updatedStats.totalShares}` + // ); + } + + recordAction(context, { + id, + actionType: "withdraw", + actor: sender, + primaryCollection: "henlo_build", + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: wberaReceived, + numeric2: lpTokensBurned, + context: { + vault: event.srcAddress.toLowerCase(), + recipient, + henloReceived: henloReceived.toString(), + }, + }); + } +); diff --git a/src/handlers/aquabera-wall.ts b/src/handlers/aquabera-wall.ts new file mode 100644 index 0000000..fc479b1 --- /dev/null +++ b/src/handlers/aquabera-wall.ts @@ -0,0 +1,284 @@ +/* + * Aquabera Wall Tracking Handlers + * + * Tracks deposits and withdrawals to the Aquabera HENLO/BERA vault. + * Identifies contributions from the wall contract and tracks unique builders. + */ + +import { + AquaberaVault, + AquaberaDeposit, + AquaberaWithdrawal, + AquaberaBuilder, + AquaberaStats, +} from "generated"; + +import { recordAction } from "../lib/actions"; + +// Wall contract address that makes special contributions (Poku Trump) +const WALL_CONTRACT_ADDRESS = + "0x05c98986Fc75D63eF973C648F22687d1a8056CD6".toLowerCase(); +const BERACHAIN_ID = 80094; + +/* + * Handle DepositForwarded events - when users add liquidity through the Aquabera forwarder + */ +export const handleAquaberaDeposit = AquaberaVault.DepositForwarded.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const depositor = event.params.sender.toLowerCase(); // The sender is who initiated the deposit + const assets = event.params.amount; // BERA/WBERA amount deposited (THIS IS THE CORRECT WBERA AMOUNT) + const shares = event.params.shares; // LP tokens received + const vault = event.params.vault.toLowerCase(); // The vault receiving the deposit + const token = event.params.token.toLowerCase(); // Token being deposited (BERA or WBERA) + const recipient = event.params.to.toLowerCase(); // Who receives the LP tokens + const isWallContribution = depositor === WALL_CONTRACT_ADDRESS; + + // Create deposit record + const depositId = `${event.transaction.hash}_${event.logIndex}`; + const deposit: AquaberaDeposit = { + id: depositId, + amount: assets, + shares: shares, + timestamp: timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + from: depositor, + isWallContribution: isWallContribution, + chainId: BERACHAIN_ID, + }; + context.AquaberaDeposit.set(deposit); + + // Batch all entity queries for parallel execution + const builderId = depositor; + const statsId = "global"; + const chainStatsId = `${BERACHAIN_ID}`; + + const [builder, stats, chainStats] = await Promise.all([ + context.AquaberaBuilder.get(builderId), + context.AquaberaStats.get(statsId), + context.AquaberaStats.get(chainStatsId), + ]); + + // Prepare builder (create if doesn't exist) + const builderToUpdate = builder || { + id: builderId, + address: depositor, + totalDeposited: BigInt(0), + totalWithdrawn: BigInt(0), + netDeposited: BigInt(0), + currentShares: BigInt(0), + depositCount: 0, + withdrawalCount: 0, + firstDepositTime: timestamp, + lastActivityTime: timestamp, + isWallContract: isWallContribution, + chainId: BERACHAIN_ID, + }; + + // Update builder stats with immutable pattern + const updatedBuilder = { + ...builderToUpdate, + totalDeposited: builderToUpdate.totalDeposited + assets, + netDeposited: builderToUpdate.netDeposited + assets, + currentShares: builderToUpdate.currentShares + shares, + depositCount: builderToUpdate.depositCount + 1, + lastActivityTime: timestamp, + }; + context.AquaberaBuilder.set(updatedBuilder); + + // Prepare global stats (create if doesn't exist) + const statsToUpdate = stats || { + id: statsId, + totalBera: BigInt(0), + totalShares: BigInt(0), + totalDeposited: BigInt(0), + totalWithdrawn: BigInt(0), + uniqueBuilders: 0, + depositCount: 0, + withdrawalCount: 0, + wallContributions: BigInt(0), + wallDepositCount: 0, + lastUpdateTime: timestamp, + chainId: BERACHAIN_ID, + }; + + // Calculate unique builders increment + const uniqueBuildersIncrement = + !builder || builder.depositCount === 0 ? 1 : 0; + + // Update global stats with immutable pattern + const updatedStats = { + ...statsToUpdate, + totalBera: statsToUpdate.totalBera + assets, + totalShares: statsToUpdate.totalShares + shares, + totalDeposited: statsToUpdate.totalDeposited + assets, + uniqueBuilders: statsToUpdate.uniqueBuilders + uniqueBuildersIncrement, + depositCount: statsToUpdate.depositCount + 1, + wallContributions: isWallContribution + ? statsToUpdate.wallContributions + assets + : statsToUpdate.wallContributions, + wallDepositCount: isWallContribution + ? statsToUpdate.wallDepositCount + 1 + : statsToUpdate.wallDepositCount, + lastUpdateTime: timestamp, + }; + context.AquaberaStats.set(updatedStats); + + // Prepare chain stats (create if doesn't exist) + const chainStatsToUpdate = chainStats || { + id: chainStatsId, + totalBera: BigInt(0), + totalShares: BigInt(0), + totalDeposited: BigInt(0), + totalWithdrawn: BigInt(0), + uniqueBuilders: 0, + depositCount: 0, + withdrawalCount: 0, + wallContributions: BigInt(0), + wallDepositCount: 0, + lastUpdateTime: timestamp, + chainId: BERACHAIN_ID, + }; + + // Update chain stats with immutable pattern + const updatedChainStats = { + ...chainStatsToUpdate, + totalBera: chainStatsToUpdate.totalBera + assets, + totalShares: chainStatsToUpdate.totalShares + shares, + totalDeposited: chainStatsToUpdate.totalDeposited + assets, + uniqueBuilders: chainStatsToUpdate.uniqueBuilders + uniqueBuildersIncrement, + depositCount: chainStatsToUpdate.depositCount + 1, + wallContributions: isWallContribution + ? chainStatsToUpdate.wallContributions + assets + : chainStatsToUpdate.wallContributions, + wallDepositCount: isWallContribution + ? chainStatsToUpdate.wallDepositCount + 1 + : chainStatsToUpdate.wallDepositCount, + lastUpdateTime: timestamp, + }; + context.AquaberaStats.set(updatedChainStats); + + // Removed verbose logging - uncomment for debugging if needed + // context.log.info( + // `Aquabera deposit: ${assets} BERA from ${depositor}${ + // isWallContribution ? " (WALL CONTRIBUTION)" : "" + // } for ${shares} shares` + // ); + + recordAction(context, { + id: depositId, + actionType: "deposit", + actor: depositor, + primaryCollection: "henlo_build", + timestamp, + chainId: event.chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: assets, + numeric2: shares, + context: { + vault, + token, + recipient, + isWallContribution, + forwarder: event.srcAddress.toLowerCase(), + }, + }); + } +); + +/* + * Handle Withdraw events - NOT IMPLEMENTED + * Note: The Aquabera forwarder doesn't emit withdrawal events + * Withdrawals would need to be tracked directly from the vault or through other means + */ +/* +export const handleAquaberaWithdraw = AquaberaVault.Withdraw.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const withdrawer = event.params.owner.toLowerCase(); + const assets = event.params.assets; // BERA amount + const shares = event.params.shares; // LP tokens burned + + // Create withdrawal record + const withdrawalId = `${event.transaction.hash}_${event.logIndex}`; + const withdrawal: AquaberaWithdrawal = { + id: withdrawalId, + amount: assets, + shares: shares, + timestamp: timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + from: withdrawer, + chainId: BERACHAIN_ID, + }; + context.AquaberaWithdrawal.set(withdrawal); + + // Update builder stats + const builderId = withdrawer; + let builder = await context.AquaberaBuilder.get(builderId); + + if (builder) { + // Update builder stats with immutable pattern + const updatedBuilder = { + ...builder, + totalWithdrawn: builder.totalWithdrawn + assets, + netDeposited: builder.netDeposited - assets, + currentShares: builder.currentShares > shares + ? builder.currentShares - shares + : BigInt(0), // Prevent negative shares + withdrawalCount: builder.withdrawalCount + 1, + lastActivityTime: timestamp, + }; + context.AquaberaBuilder.set(updatedBuilder); + } + + // Update global stats + const statsId = "global"; + let stats = await context.AquaberaStats.get(statsId); + + if (stats) { + // Update stats with immutable pattern + const updatedStats = { + ...stats, + totalBera: stats.totalBera > assets + ? stats.totalBera - assets + : BigInt(0), // Prevent negative balance + totalShares: stats.totalShares > shares + ? stats.totalShares - shares + : BigInt(0), + totalWithdrawn: stats.totalWithdrawn + assets, + withdrawalCount: stats.withdrawalCount + 1, + lastUpdateTime: timestamp, + }; + context.AquaberaStats.set(updatedStats); + } + + // Also update chain-specific stats + const chainStatsId = `${BERACHAIN_ID}`; + let chainStats = await context.AquaberaStats.get(chainStatsId); + + if (chainStats) { + // Update chain stats with immutable pattern + const updatedChainStats = { + ...chainStats, + totalBera: chainStats.totalBera > assets + ? chainStats.totalBera - assets + : BigInt(0), + totalShares: chainStats.totalShares > shares + ? chainStats.totalShares - shares + : BigInt(0), + totalWithdrawn: chainStats.totalWithdrawn + assets, + withdrawalCount: chainStats.withdrawalCount + 1, + lastUpdateTime: timestamp, + }; + context.AquaberaStats.set(updatedChainStats); + } + + context.log.info( + `Aquabera withdrawal: ${assets} BERA to ${withdrawer} for ${shares} shares` + ); + } +); +*/ diff --git a/src/handlers/badges1155.ts b/src/handlers/badges1155.ts new file mode 100644 index 0000000..214fbd7 --- /dev/null +++ b/src/handlers/badges1155.ts @@ -0,0 +1,352 @@ +import { CubBadges1155 } from "generated"; +import type { + handlerContext, + BadgeHolder as BadgeHolderEntity, + BadgeBalance as BadgeBalanceEntity, + BadgeAmount as BadgeAmountEntity, +} from "generated"; + +import { ZERO_ADDRESS } from "./constants"; +import { recordAction } from "../lib/actions"; + +const ZERO = ZERO_ADDRESS.toLowerCase(); + +interface BalanceAdjustmentArgs { + context: handlerContext; + holderAddress: string; + contractAddress: string; + tokenId: bigint; + amountDelta: bigint; + timestamp: bigint; + chainId: number; + txHash: string; + logIndex: number; + direction: "in" | "out"; + batchIndex?: number; +} + +const makeHolderId = (address: string) => address; + +const makeBalanceId = ( + chainId: number, + address: string, + contract: string, + tokenId: bigint +) => `${chainId}-${address}-${contract}-${tokenId.toString()}`; + +const makeBadgeAmountId = ( + holderId: string, + contract: string, + tokenId: bigint, +) => `${holderId}-${contract}-${tokenId.toString()}`; + +const makeHoldingsKey = (contract: string, tokenId: bigint): string => + `${contract}-${tokenId.toString()}`; + +const cloneHoldings = ( + rawHoldings: unknown, +): Record => { + if (!rawHoldings || typeof rawHoldings !== "object") { + return {}; + } + + const entries = Object.entries( + rawHoldings as Record, + ); + + const result: Record = {}; + for (const [key, value] of entries) { + if (typeof value === "string") { + result[key] = value; + } else if (typeof value === "number") { + result[key] = Math.trunc(value).toString(); + } else if (typeof value === "bigint") { + result[key] = value.toString(); + } + } + + return result; +}; + +async function adjustBadgeBalances({ + context, + holderAddress, + contractAddress, + tokenId, + amountDelta, + timestamp, + chainId, + txHash, + logIndex, + direction, + batchIndex, +}: BalanceAdjustmentArgs): Promise { + if (amountDelta === 0n) { + return; + } + + const normalizedAddress = holderAddress.toLowerCase(); + if (normalizedAddress === ZERO) { + return; + } + + const normalizedContract = contractAddress.toLowerCase(); + const holderId = makeHolderId(normalizedAddress); + const balanceId = makeBalanceId( + chainId, + normalizedAddress, + normalizedContract, + tokenId + ); + const badgeAmountId = makeBadgeAmountId( + holderId, + normalizedContract, + tokenId + ); + const legacyBadgeAmountId = `${holderId}-${tokenId.toString()}`; + + const existingBalance = await context.BadgeBalance.get(balanceId); + const currentBalance = existingBalance?.amount ?? 0n; + + let appliedDelta = amountDelta; + let nextBalance = currentBalance + amountDelta; + + if (amountDelta < 0n) { + const removeAmount = + currentBalance < -amountDelta ? currentBalance : -amountDelta; + + if (removeAmount === 0n) { + return; + } + + appliedDelta = -removeAmount; // Both are bigint now + nextBalance = currentBalance - removeAmount; + } + + if (appliedDelta === 0n) { + return; + } + + const holdingsKey = makeHoldingsKey(normalizedContract, tokenId); + const legacyKey = tokenId.toString(); + const existingHolder = await context.BadgeHolder.get(holderId); + const holderAddressField = existingHolder?.address ?? normalizedAddress; + const currentHoldings = cloneHoldings(existingHolder?.holdings); + const resolvedHoldingRaw = + currentHoldings[holdingsKey] ?? currentHoldings[legacyKey] ?? "0"; + const previousHoldingAmount = BigInt(resolvedHoldingRaw); + let nextHoldingAmount = previousHoldingAmount + appliedDelta; + if (nextHoldingAmount < 0n) { + nextHoldingAmount = 0n; + } + + if (nextHoldingAmount === 0n) { + delete currentHoldings[holdingsKey]; + delete currentHoldings[legacyKey]; + } else { + currentHoldings[holdingsKey] = nextHoldingAmount.toString(); + if (legacyKey in currentHoldings && legacyKey !== holdingsKey) { + delete currentHoldings[legacyKey]; + } + } + + const currentTotal = existingHolder?.totalBadges ?? 0n; + let nextTotal = currentTotal + appliedDelta; + + if (nextTotal < 0n) { + nextTotal = 0n; + } + + const actionSuffixParts = [ + direction, + tokenId.toString(), + batchIndex !== undefined ? batchIndex.toString() : undefined, + ].filter((part): part is string => part !== undefined); + const actionId = `${txHash}_${logIndex}_${actionSuffixParts.join("_")}`; + const tokenCount = nextHoldingAmount < 0n ? 0n : nextHoldingAmount; + + recordAction(context, { + id: actionId, + actionType: "hold1155", + actor: normalizedAddress, + primaryCollection: normalizedContract, + timestamp, + chainId, + txHash, + logIndex, + numeric1: tokenCount, + context: { + contract: normalizedContract, + tokenId: tokenId.toString(), + amount: tokenCount.toString(), + direction, + holdingsKey, + batchIndex, + }, + }); + + const holder: BadgeHolderEntity = { + id: holderId, + address: holderAddressField, + chainId, + totalBadges: nextTotal, + totalAmount: nextTotal, + holdings: currentHoldings, + updatedAt: timestamp, + }; + + context.BadgeHolder.set(holder); + + const existingBadgeAmount = + (await context.BadgeAmount.get(badgeAmountId)) ?? + (await context.BadgeAmount.get(legacyBadgeAmountId)); + if (nextHoldingAmount === 0n) { + if (existingBadgeAmount) { + context.BadgeAmount.deleteUnsafe(existingBadgeAmount.id); + } + if ( + legacyBadgeAmountId !== existingBadgeAmount?.id && + legacyBadgeAmountId !== badgeAmountId + ) { + const legacyRecord = await context.BadgeAmount.get(legacyBadgeAmountId); + if (legacyRecord) { + context.BadgeAmount.deleteUnsafe(legacyBadgeAmountId); + } + } + } else { + const badgeAmount: BadgeAmountEntity = { + id: badgeAmountId, + holder_id: holderId, + badgeId: holdingsKey, + amount: nextHoldingAmount, + updatedAt: timestamp, + }; + context.BadgeAmount.set(badgeAmount); + + if (legacyBadgeAmountId !== badgeAmountId) { + const legacyRecord = await context.BadgeAmount.get(legacyBadgeAmountId); + if (legacyRecord) { + context.BadgeAmount.deleteUnsafe(legacyBadgeAmountId); + } + } + } + + if (nextBalance <= 0n) { + if (existingBalance) { + context.BadgeBalance.deleteUnsafe(balanceId); + } + return; + } + + const balance: BadgeBalanceEntity = { + id: balanceId, + holder_id: holderId, + contract: normalizedContract, + tokenId, + chainId, + amount: nextBalance, + updatedAt: timestamp, + }; + + context.BadgeBalance.set(balance); +} + +export const handleCubBadgesTransferSingle = + CubBadges1155.TransferSingle.handler(async ({ event, context }) => { + const { from, to, id, value } = event.params; + const chainId = event.chainId; + const timestamp = BigInt(event.block.timestamp); + const contractAddress = event.srcAddress.toLowerCase(); + const tokenId = BigInt(id.toString()); + const quantity = BigInt(value.toString()); + const txHash = event.transaction.hash; + const logIndex = Number(event.logIndex); + + if (quantity === 0n) { + return; + } + + await adjustBadgeBalances({ + context, + holderAddress: from, + contractAddress, + tokenId, + amountDelta: -quantity, + timestamp, + chainId, + txHash, + logIndex, + direction: "out", + }); + + await adjustBadgeBalances({ + context, + holderAddress: to, + contractAddress, + tokenId, + amountDelta: quantity, + timestamp, + chainId, + txHash, + logIndex, + direction: "in", + }); + }); + +export const handleCubBadgesTransferBatch = + CubBadges1155.TransferBatch.handler(async ({ event, context }) => { + const { from, to, ids, values } = event.params; + const chainId = event.chainId; + const timestamp = BigInt(event.block.timestamp); + const contractAddress = event.srcAddress.toLowerCase(); + const txHash = event.transaction.hash; + const baseLogIndex = Number(event.logIndex); + + const idsArray = Array.from(ids); + const valuesArray = Array.from(values); + const length = Math.min(idsArray.length, valuesArray.length); + + for (let index = 0; index < length; index += 1) { + const rawId = idsArray[index]; + const rawValue = valuesArray[index]; + + if (rawId === undefined || rawValue === undefined || rawValue === null) { + continue; + } + + const tokenId = BigInt(rawId.toString()); + const quantity = BigInt(rawValue.toString()); + + if (quantity === 0n) { + continue; + } + + await adjustBadgeBalances({ + context, + holderAddress: from, + contractAddress, + tokenId, + amountDelta: -quantity, + timestamp, + chainId, + txHash, + logIndex: baseLogIndex, + direction: "out", + batchIndex: index, + }); + + await adjustBadgeBalances({ + context, + holderAddress: to, + contractAddress, + tokenId, + amountDelta: quantity, + timestamp, + chainId, + txHash, + logIndex: baseLogIndex, + direction: "in", + batchIndex: index, + }); + } + }); diff --git a/src/handlers/bgt.ts b/src/handlers/bgt.ts new file mode 100644 index 0000000..a317964 --- /dev/null +++ b/src/handlers/bgt.ts @@ -0,0 +1,116 @@ +/* + * BGT queue boost tracking. + * + * Captures QueueBoost events emitted when users delegate BGT to validators. + */ + +import { Interface, hexlify } from "ethers"; + +import { BgtToken, BgtBoostEvent } from "generated"; + +import { recordAction } from "../lib/actions"; + +const QUEUE_BOOST_INTERFACE = new Interface([ + "function queueBoost(bytes pubkey, uint128 amount)", + "function queue_boost(bytes pubkey, uint128 amount)", +]); + +const normalizePubkey = (raw: unknown): string | undefined => { + if (typeof raw === "string") { + return raw.toLowerCase(); + } + + if (raw instanceof Uint8Array) { + try { + return hexlify(raw).toLowerCase(); + } catch (_err) { + return undefined; + } + } + + if (Array.isArray(raw)) { + try { + return hexlify(Uint8Array.from(raw as number[])).toLowerCase(); + } catch (_err) { + return undefined; + } + } + + return undefined; +}; + +export const handleBgtQueueBoost = BgtToken.QueueBoost.handler( + async ({ event, context }) => { + const { account, pubkey, amount } = event.params; + + if (amount === 0n) { + return; + } + + const accountLower = account.toLowerCase(); + let validatorPubkey = pubkey.toLowerCase(); + const transactionFrom = event.transaction.from + ? event.transaction.from.toLowerCase() + : accountLower; + + const inputData = event.transaction.input; + if (inputData && inputData !== "0x") { + try { + const parsed = QUEUE_BOOST_INTERFACE.parseTransaction({ + data: inputData, + }); + + if (parsed) { + const decodedPubkey = normalizePubkey( + (parsed.args as any)?.pubkey ?? parsed.args?.[0] + ); + + if (decodedPubkey) { + validatorPubkey = decodedPubkey; + } + } + } catch (error) { + context.log.warn( + `Failed to decode queue_boost input for ${event.transaction.hash}: ${String( + error + )}` + ); + } + } + + const id = `${event.transaction.hash}_${event.logIndex}`; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + + const boostEvent: BgtBoostEvent = { + id, + account: accountLower, + validatorPubkey, + amount, + transactionFrom, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }; + + context.BgtBoostEvent.set(boostEvent); + + recordAction(context, { + id, + actionType: "delegate", + actor: transactionFrom, + primaryCollection: "thj_delegate", + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: amount, + context: { + account: accountLower, + validatorPubkey, + contract: event.srcAddress.toLowerCase(), + }, + }); + } +); diff --git a/src/handlers/constants.ts b/src/handlers/constants.ts new file mode 100644 index 0000000..4e87f45 --- /dev/null +++ b/src/handlers/constants.ts @@ -0,0 +1,69 @@ +/* + * Shared constants for THJ indexer + */ + +export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +export const BERACHAIN_TESTNET_ID = 80094; +export const BERACHAIN_MAINNET_ID = 80084; +// Note: Despite the naming above, 80094 is actually mainnet. Use BERACHAIN_ID for clarity. +export const BERACHAIN_ID = 80094; + +// Kingdomly proxy bridge contracts (these hold NFTs when bridged to Berachain) +export const PROXY_CONTRACTS: Record = { + HoneyJar1: "0xe0b791529f7876dc2b9d748a2e6570e605f40e5e", + HoneyJar2: "0xd1d5df5f85c0fcbdc5c9757272de2ee5296ed512", + HoneyJar3: "0x3992605f13bc182c0b0c60029fcbb21c0626a5f1", + HoneyJar4: "0xeeaa4926019eaed089b8b66b544deb320c04e421", + HoneyJar5: "0x00331b0e835c511489dba62a2b16b8fa380224f9", + HoneyJar6: "0x0de0f0a9f7f1a56dafd025d0f31c31c6cb190346", + Honeycomb: "0x33a76173680427cba3ffc3a625b7bc43b08ce0c5", +}; + +// Address to collection mapping (includes all contracts) +export const ADDRESS_TO_COLLECTION: Record = { + // Ethereum mainnet + "0xa20cf9b0874c3e46b344deaeea9c2e0c3e1db37d": "HoneyJar1", + "0x98dc31a9648f04e23e4e36b0456d1951531c2a05": "HoneyJar6", + "0xcb0477d1af5b8b05795d89d59f4667b59eae9244": "Honeycomb", + // Ethereum L0 reminted contracts (when bridged from native chains) + "0x3f4dd25ba6fb6441bfd1a869cbda6a511966456d": "HoneyJar2", + "0x49f3915a52e137e597d6bf11c73e78c68b082297": "HoneyJar3", + "0x0b820623485dcfb1c40a70c55755160f6a42186d": "HoneyJar4", + "0x39eb35a84752b4bd3459083834af1267d276a54c": "HoneyJar5", + // Arbitrum + "0x1b2751328f41d1a0b91f3710edcd33e996591b72": "HoneyJar2", + // Zora + "0xe798c4d40bc050bc93c7f3b149a0dfe5cfc49fb0": "HoneyJar3", + // Optimism + "0xe1d16cc75c9f39a2e0f5131eb39d4b634b23f301": "HoneyJar4", + // Base + "0xbad7b49d985bbfd3a22706c447fb625a28f048b4": "HoneyJar5", + // Berachain + "0xedc5dfd6f37464cc91bbce572b6fe2c97f1bc7b3": "HoneyJar1", + "0x1c6c24cac266c791c4ba789c3ec91f04331725bd": "HoneyJar2", + "0xf1e4a550772fabfc35b28b51eb8d0b6fcd1c4878": "HoneyJar3", + "0xdb602ab4d6bd71c8d11542a9c8c936877a9a4f45": "HoneyJar4", + "0x0263728e7f59f315c17d3c180aeade027a375f17": "HoneyJar5", + "0xb62a9a21d98478f477e134e175fd2003c15cb83a": "HoneyJar6", + "0x886d2176d899796cd1affa07eff07b9b2b80f1be": "Honeycomb", +}; + +export const COLLECTION_TO_GENERATION: Record = { + HoneyJar1: 1, + HoneyJar2: 2, + HoneyJar3: 3, + HoneyJar4: 4, + HoneyJar5: 5, + HoneyJar6: 6, + Honeycomb: 0, +}; + +export const HOME_CHAIN_IDS: Record = { + 1: 1, // Gen 1 - Ethereum + 2: 42161, // Gen 2 - Arbitrum + 3: 7777777, // Gen 3 - Zora + 4: 10, // Gen 4 - Optimism + 5: 8453, // Gen 5 - Base + 6: 1, // Gen 6 - Ethereum + 0: 1, // Honeycomb - Ethereum +}; \ No newline at end of file diff --git a/src/handlers/crayons-collections.ts b/src/handlers/crayons-collections.ts new file mode 100644 index 0000000..24934d0 --- /dev/null +++ b/src/handlers/crayons-collections.ts @@ -0,0 +1,22 @@ +/* + * Crayons ERC721 Collections - Transfer Indexing + * + * Indexes Transfer events for Crayons ERC721 Base collections deployed by the Crayons Factory. + * Stores ownership in Token, movements in Transfer, per-collection Holder balances, and CollectionStat. + * + * Collection identifier: the on-chain collection address (lowercase string). + */ + +import { CrayonsCollection } from "generated"; + +import { processErc721Transfer } from "../lib/erc721-holders"; + +export const handleCrayonsErc721Transfer = CrayonsCollection.Transfer.handler( + async ({ event, context }) => { + await processErc721Transfer({ + event, + context, + collectionAddress: event.srcAddress.toLowerCase(), + }); + } +); diff --git a/src/handlers/crayons.ts b/src/handlers/crayons.ts new file mode 100644 index 0000000..f943fd7 --- /dev/null +++ b/src/handlers/crayons.ts @@ -0,0 +1,26 @@ +import { CrayonsFactory, Transfer } from "generated"; + +// Skeleton handler for Crayons Factory emits. This records the discovery event. +// Follow-up work will add dynamic tracking of ERC721 Base collection transfers +// and populate Token/Transfer entities for holders/stats. + +export const handleCrayonsFactoryNewBase = CrayonsFactory.Factory__NewERC721Base.handler( + async ({ event, context }) => { + const { owner, erc721Base } = event.params; + + const transfer: Transfer = { + id: `${event.transaction.hash}_crayons_factory_${erc721Base.toLowerCase()}`, + tokenId: 0n, + from: owner.toLowerCase(), + to: erc721Base.toLowerCase(), + timestamp: BigInt(event.block.timestamp), + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash.toLowerCase(), + collection: "crayons_factory", + chainId: event.chainId, + }; + + context.Transfer.set(transfer); + } +); + diff --git a/src/handlers/fatbera.ts b/src/handlers/fatbera.ts new file mode 100644 index 0000000..32bed81 --- /dev/null +++ b/src/handlers/fatbera.ts @@ -0,0 +1,66 @@ +/* + * FatBera native deposit tracking. + * + * Captures Deposit events emitted by the fatBERA contract to record + * on-chain native BERA deposits and their minted share amount. + */ + +import { FatBera, FatBeraDeposit } from "generated"; + +import { recordAction } from "../lib/actions"; + +const COLLECTION_KEY = "fatbera_deposit"; + +export const handleFatBeraDeposit = FatBera.Deposit.handler( + async ({ event, context }) => { + const { from, to, amount, shares } = event.params; + + if (amount === 0n && shares === 0n) { + return; // skip zero-value deposits + } + + const depositor = from.toLowerCase(); + const recipient = to.toLowerCase(); + const transactionFrom = event.transaction.from + ? event.transaction.from.toLowerCase() + : undefined; + + const id = `${event.transaction.hash}_${event.logIndex}`; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + + const deposit: FatBeraDeposit = { + id, + collectionKey: COLLECTION_KEY, + depositor, + recipient, + amount, + shares, + transactionFrom, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }; + + context.FatBeraDeposit.set(deposit); + + recordAction(context, { + id, + actionType: "deposit", + actor: depositor, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: amount, + numeric2: shares, + context: { + recipient, + transactionFrom, + contract: event.srcAddress.toLowerCase(), + }, + }); + } +); diff --git a/src/handlers/friendtech.ts b/src/handlers/friendtech.ts new file mode 100644 index 0000000..28868f3 --- /dev/null +++ b/src/handlers/friendtech.ts @@ -0,0 +1,143 @@ +/* + * friend.tech key trading tracking on Base. + * + * Tracks Trade events for Mibera-related subjects (jani key, charlotte fang key). + * Only indexes trades for the specified subject addresses. + */ + +import { + FriendtechShares, + FriendtechTrade, + FriendtechHolder, + FriendtechSubjectStats, +} from "generated"; + +import { recordAction } from "../lib/actions"; +import { + MIBERA_SUBJECTS, + FRIENDTECH_COLLECTION_KEY, +} from "./friendtech/constants"; + +const COLLECTION_KEY = FRIENDTECH_COLLECTION_KEY; + +/** + * Handle Trade events from friend.tech + * Only tracks trades for Mibera-related subjects + */ +export const handleFriendtechTrade = FriendtechShares.Trade.handler( + async ({ event, context }) => { + const { + trader, + subject, + isBuy, + shareAmount, + ethAmount, + supply, + } = event.params; + + const subjectLower = subject.toLowerCase(); + const subjectKey = MIBERA_SUBJECTS[subjectLower]; + + // Only track Mibera-related subjects + if (!subjectKey) { + return; + } + + const traderLower = trader.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const tradeId = `${event.transaction.hash}_${event.logIndex}`; + const shareAmountBigInt = BigInt(shareAmount.toString()); + const ethAmountBigInt = BigInt(ethAmount.toString()); + const supplyBigInt = BigInt(supply.toString()); + + // Record individual trade event + const trade: FriendtechTrade = { + id: tradeId, + trader: traderLower, + subject: subjectLower, + subjectKey, + isBuy, + shareAmount: shareAmountBigInt, + ethAmount: ethAmountBigInt, + supply: supplyBigInt, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }; + + context.FriendtechTrade.set(trade); + + // Update holder balance + const holderId = `${subjectLower}_${traderLower}_${chainId}`; + const existingHolder = await context.FriendtechHolder.get(holderId); + const shareAmountInt = Number(shareAmountBigInt); + + const balanceDelta = isBuy ? shareAmountInt : -shareAmountInt; + const newBalance = (existingHolder?.balance ?? 0) + balanceDelta; + + const holder: FriendtechHolder = { + id: holderId, + subject: subjectLower, + subjectKey, + holder: traderLower, + balance: Math.max(0, newBalance), // Ensure non-negative + totalBought: (existingHolder?.totalBought ?? 0) + (isBuy ? shareAmountInt : 0), + totalSold: (existingHolder?.totalSold ?? 0) + (isBuy ? 0 : shareAmountInt), + firstTradeTime: existingHolder?.firstTradeTime ?? timestamp, + lastTradeTime: timestamp, + chainId, + }; + + context.FriendtechHolder.set(holder); + + // Update subject stats + const statsId = `${subjectLower}_${chainId}`; + const existingStats = await context.FriendtechSubjectStats.get(statsId); + + // Track unique holders (approximate - increment on first buy, decrement when balance goes to 0) + let uniqueHoldersDelta = 0; + if (isBuy && !existingHolder) { + uniqueHoldersDelta = 1; // New holder + } else if (!isBuy && existingHolder && existingHolder.balance > 0 && newBalance <= 0) { + uniqueHoldersDelta = -1; // Holder sold all + } + + const stats: FriendtechSubjectStats = { + id: statsId, + subject: subjectLower, + subjectKey, + totalSupply: supplyBigInt, + uniqueHolders: Math.max(0, (existingStats?.uniqueHolders ?? 0) + uniqueHoldersDelta), + totalTrades: (existingStats?.totalTrades ?? 0) + 1, + totalBuys: (existingStats?.totalBuys ?? 0) + (isBuy ? 1 : 0), + totalSells: (existingStats?.totalSells ?? 0) + (isBuy ? 0 : 1), + totalVolumeEth: (existingStats?.totalVolumeEth ?? 0n) + ethAmountBigInt, + lastTradeTime: timestamp, + chainId, + }; + + context.FriendtechSubjectStats.set(stats); + + // Record action for activity feed/missions + recordAction(context, { + id: tradeId, + actionType: isBuy ? "friendtech_buy" : "friendtech_sell", + actor: traderLower, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: shareAmountBigInt, + numeric2: ethAmountBigInt, + context: { + subject: subjectLower, + subjectKey, + supply: supplyBigInt.toString(), + newBalance, + }, + }); + } +); diff --git a/src/handlers/friendtech/constants.ts b/src/handlers/friendtech/constants.ts new file mode 100644 index 0000000..bcb8a80 --- /dev/null +++ b/src/handlers/friendtech/constants.ts @@ -0,0 +1,14 @@ +/* + * friend.tech constants for THJ indexer. + * + * Tracks Mibera-related subjects (keys) on Base chain. + */ + +// Mibera-related friend.tech subjects (lowercase address -> collection key) +export const MIBERA_SUBJECTS: Record = { + "0x1defc6b7320f9480f3b2d77e396a942f2803559d": "jani_key", + "0x956d9b56b20c28993b9baaed1465376ce996e3ed": "charlotte_fang_key", +}; + +// Collection key for action tracking +export const FRIENDTECH_COLLECTION_KEY = "friendtech"; diff --git a/src/handlers/henlo-vault.ts b/src/handlers/henlo-vault.ts new file mode 100644 index 0000000..65171b8 --- /dev/null +++ b/src/handlers/henlo-vault.ts @@ -0,0 +1,505 @@ +/* + * HenloVault Event Handlers + * + * Handles two systems: + * 1. HENLOCKED token mints - Tracks initial token distribution via TrackedTokenBalance + * 2. Henlocker vault system - Tracks rounds, deposits, balances, epochs, and stats + */ + +import { + TrackedTokenBalance, + HenloVault, + HenloVaultRound, + HenloVaultDeposit, + HenloVaultBalance, + HenloVaultEpoch, + HenloVaultStats, + HenloVaultUser, +} from "generated"; + +// Map strike values to HENLOCKED token addresses and keys +// Strike represents FDV target in thousands (e.g., 100000 = $100M FDV) +const STRIKE_TO_TOKEN: Record = { + "20000": { + address: "0x4c9c76d10b1fa7d8f93ba54ab48e890ff0a7660d", + key: "hlkd20m", + }, + "100000": { + address: "0x7bdf98ddeed209cfa26bd2352b470ac8b5485ec5", + key: "hlkd100m", + }, + "330000": { + address: "0x37dd8850919ebdca911c383211a70839a94b0539", + key: "hlkd330m", + }, + "420000": { + address: "0xf07fa3ece9741d408d643748ff85710bedef25ba", + key: "hlkd420m", + }, + "690000": { + address: "0x8ab854dc0672d7a13a85399a56cb628fb22102d6", + key: "hlkd690m", + }, + "1000000": { + address: "0xf0edfc3e122db34773293e0e5b2c3a58492e7338", + key: "hlkd1b", + }, +}; + +// ============================ +// Helper Functions +// ============================ + +// Map strike values to their epochIds (based on contract deployment order) +const STRIKE_TO_EPOCH: Record = { + "100000": 1, + "330000": 2, + "420000": 3, + "690000": 4, + "1000000": 5, + "20000": 6, +}; + +/** + * Find the active round for a given strike + * Uses the known strike-to-epoch mapping since each strike has one epoch + */ +async function findRoundByStrike( + context: any, + strike: bigint, + chainId: number +): Promise { + const strikeKey = strike.toString(); + const epochId = STRIKE_TO_EPOCH[strikeKey]; + + if (epochId === undefined) { + // Unknown strike, return undefined + return undefined; + } + + const roundId = `${strike}_${epochId}_${chainId}`; + return await context.HenloVaultRound.get(roundId); +} + +/** + * Get or create HenloVaultStats singleton for a chain + */ +async function getOrCreateStats( + context: any, + chainId: number, + timestamp: bigint +): Promise { + const statsId = chainId.toString(); + let stats = await context.HenloVaultStats.get(statsId); + + if (!stats) { + stats = { + id: statsId, + totalDeposits: BigInt(0), + totalUsers: 0, + totalRounds: 0, + totalEpochs: 0, + chainId, + }; + } + + return stats; +} + +/** + * Get or create HenloVaultUser for tracking unique depositors + */ +async function getOrCreateUser( + context: any, + user: string, + chainId: number, + timestamp: bigint +): Promise<{ vaultUser: HenloVaultUser; isNew: boolean }> { + const userId = `${user}_${chainId}`; + let vaultUser = await context.HenloVaultUser.get(userId); + const isNew = !vaultUser; + + if (!vaultUser) { + vaultUser = { + id: userId, + user, + firstDepositTime: timestamp, + lastActivityTime: timestamp, + chainId, + }; + } + + return { vaultUser, isNew }; +} + +// ============================ +// HENLOCKED Token Mint Handler +// ============================ + +/** + * Handles HenloVault Mint events + * Creates/updates TrackedTokenBalance for the user when they receive HENLOCKED tokens + * Also creates deposit records for the Henlocker vault system + */ +export const handleHenloVaultMint = HenloVault.Mint.handler( + async ({ event, context }) => { + const { user, strike, amount } = event.params; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const userLower = user.toLowerCase(); + + // Get token info from strike value + const strikeKey = strike.toString(); + const tokenInfo = STRIKE_TO_TOKEN[strikeKey]; + + if (!tokenInfo) { + // Unknown strike value, skip + context.log.warn(`Unknown HenloVault strike value: ${strikeKey}`); + return; + } + + const { address: tokenAddress, key: tokenKey } = tokenInfo; + + // 1. Update TrackedTokenBalance (HENLOCKED token tracking) + const balanceId = `${userLower}_${tokenAddress}_${chainId}`; + const existingBalance = await context.TrackedTokenBalance.get(balanceId); + + if (existingBalance) { + const updatedBalance: TrackedTokenBalance = { + ...existingBalance, + balance: existingBalance.balance + amount, + lastUpdated: timestamp, + }; + context.TrackedTokenBalance.set(updatedBalance); + } else { + const newBalance: TrackedTokenBalance = { + id: balanceId, + address: userLower, + tokenAddress, + tokenKey, + chainId, + balance: amount, + lastUpdated: timestamp, + }; + context.TrackedTokenBalance.set(newBalance); + } + + // 2. Create HenloVaultDeposit record + const depositId = `${event.transaction.hash}_${event.logIndex}`; + + // Find the round for this strike using the strike-to-epoch mapping + const round = await findRoundByStrike(context, strike, chainId); + const epochId = round ? round.epochId : BigInt(STRIKE_TO_EPOCH[strikeKey] || 0); + + const deposit: HenloVaultDeposit = { + id: depositId, + user: userLower, + strike: strike, + epochId: epochId, + amount: amount, + timestamp: timestamp, + transactionHash: event.transaction.hash, + chainId, + }; + context.HenloVaultDeposit.set(deposit); + + // 3. Update HenloVaultBalance + const vaultBalanceId = `${userLower}_${strike}_${chainId}`; + const existingVaultBalance = await context.HenloVaultBalance.get(vaultBalanceId); + + if (existingVaultBalance) { + const updatedVaultBalance: HenloVaultBalance = { + ...existingVaultBalance, + balance: existingVaultBalance.balance + amount, + lastUpdated: timestamp, + }; + context.HenloVaultBalance.set(updatedVaultBalance); + } else { + const newVaultBalance: HenloVaultBalance = { + id: vaultBalanceId, + user: userLower, + strike: strike, + balance: amount, + lastUpdated: timestamp, + chainId, + }; + context.HenloVaultBalance.set(newVaultBalance); + } + + // 4. Update HenloVaultRound (if exists) + if (round) { + const updatedRound: HenloVaultRound = { + ...round, + totalDeposits: round.totalDeposits + amount, + userDeposits: round.userDeposits + amount, + remainingCapacity: round.depositLimit - (round.totalDeposits + amount), + }; + context.HenloVaultRound.set(updatedRound); + } + + // 5. Update HenloVaultStats + const stats = await getOrCreateStats(context, chainId, timestamp); + const { vaultUser, isNew } = await getOrCreateUser(context, userLower, chainId, timestamp); + + const updatedStats: HenloVaultStats = { + ...stats, + totalDeposits: stats.totalDeposits + amount, + totalUsers: isNew ? stats.totalUsers + 1 : stats.totalUsers, + }; + context.HenloVaultStats.set(updatedStats); + + // Update user activity + const updatedUser: HenloVaultUser = { + ...vaultUser, + lastActivityTime: timestamp, + }; + context.HenloVaultUser.set(updatedUser); + } +); + +// ============================ +// Henlocker Vault Round Handlers +// ============================ + +/** + * Handles RoundOpened events - Creates a new vault round + */ +export const handleHenloVaultRoundOpened = HenloVault.RoundOpened.handler( + async ({ event, context }) => { + const { epochId, strike, depositLimit } = event.params; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + + const roundId = `${strike}_${epochId}_${chainId}`; + + const round: HenloVaultRound = { + id: roundId, + strike: BigInt(strike), + epochId: BigInt(epochId), + exists: true, + closed: false, + depositsPaused: false, + timestamp: timestamp, + depositLimit: depositLimit, + totalDeposits: BigInt(0), + whaleDeposits: BigInt(0), + userDeposits: BigInt(0), + remainingCapacity: depositLimit, + canRedeem: false, + chainId, + }; + + context.HenloVaultRound.set(round); + + // Update stats + const stats = await getOrCreateStats(context, chainId, timestamp); + const updatedStats: HenloVaultStats = { + ...stats, + totalRounds: stats.totalRounds + 1, + }; + context.HenloVaultStats.set(updatedStats); + } +); + +/** + * Handles RoundClosed events - Marks round as closed + */ +export const handleHenloVaultRoundClosed = HenloVault.RoundClosed.handler( + async ({ event, context }) => { + const { epochId, strike } = event.params; + const chainId = event.chainId; + + const roundId = `${strike}_${epochId}_${chainId}`; + const round = await context.HenloVaultRound.get(roundId); + + if (round) { + const updatedRound: HenloVaultRound = { + ...round, + closed: true, + canRedeem: true, + }; + context.HenloVaultRound.set(updatedRound); + } + } +); + +/** + * Handles DepositsPaused events + */ +export const handleHenloVaultDepositsPaused = HenloVault.DepositsPaused.handler( + async ({ event, context }) => { + const { epochId, strike } = event.params; + const chainId = event.chainId; + + const roundId = `${strike}_${epochId}_${chainId}`; + const round = await context.HenloVaultRound.get(roundId); + + if (round) { + const updatedRound: HenloVaultRound = { + ...round, + depositsPaused: true, + }; + context.HenloVaultRound.set(updatedRound); + } + + // Also update epoch + const epochEntityId = `${epochId}_${chainId}`; + const epoch = await context.HenloVaultEpoch.get(epochEntityId); + if (epoch) { + const updatedEpoch: HenloVaultEpoch = { + ...epoch, + depositsPaused: true, + }; + context.HenloVaultEpoch.set(updatedEpoch); + } + } +); + +/** + * Handles DepositsUnpaused events + */ +export const handleHenloVaultDepositsUnpaused = HenloVault.DepositsUnpaused.handler( + async ({ event, context }) => { + const { epochId, strike } = event.params; + const chainId = event.chainId; + + const roundId = `${strike}_${epochId}_${chainId}`; + const round = await context.HenloVaultRound.get(roundId); + + if (round) { + const updatedRound: HenloVaultRound = { + ...round, + depositsPaused: false, + }; + context.HenloVaultRound.set(updatedRound); + } + + // Also update epoch + const epochEntityId = `${epochId}_${chainId}`; + const epoch = await context.HenloVaultEpoch.get(epochEntityId); + if (epoch) { + const updatedEpoch: HenloVaultEpoch = { + ...epoch, + depositsPaused: false, + }; + context.HenloVaultEpoch.set(updatedEpoch); + } + } +); + +/** + * Handles MintFromReservoir events - Whale/reservoir deposits + */ +export const handleHenloVaultMintFromReservoir = HenloVault.MintFromReservoir.handler( + async ({ event, context }) => { + const { reservoir, strike, amount } = event.params; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + + // Find the round for this strike using the strike-to-epoch mapping + const round = await findRoundByStrike(context, strike, chainId); + + if (round) { + const updatedRound: HenloVaultRound = { + ...round, + totalDeposits: round.totalDeposits + amount, + whaleDeposits: round.whaleDeposits + amount, + remainingCapacity: round.depositLimit - (round.totalDeposits + amount), + }; + context.HenloVaultRound.set(updatedRound); + } + + // Update stats + const stats = await getOrCreateStats(context, chainId, timestamp); + const updatedStats: HenloVaultStats = { + ...stats, + totalDeposits: stats.totalDeposits + amount, + }; + context.HenloVaultStats.set(updatedStats); + } +); + +/** + * Handles Redeem events - User withdrawals + */ +export const handleHenloVaultRedeem = HenloVault.Redeem.handler( + async ({ event, context }) => { + const { user, strike, amount } = event.params; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const userLower = user.toLowerCase(); + + // Update HenloVaultBalance + const vaultBalanceId = `${userLower}_${strike}_${chainId}`; + const existingVaultBalance = await context.HenloVaultBalance.get(vaultBalanceId); + + if (existingVaultBalance) { + const newBalance = existingVaultBalance.balance - amount; + const updatedVaultBalance: HenloVaultBalance = { + ...existingVaultBalance, + balance: newBalance > BigInt(0) ? newBalance : BigInt(0), + lastUpdated: timestamp, + }; + context.HenloVaultBalance.set(updatedVaultBalance); + } + + // Update user activity + const userId = `${userLower}_${chainId}`; + const vaultUser = await context.HenloVaultUser.get(userId); + if (vaultUser) { + const updatedUser: HenloVaultUser = { + ...vaultUser, + lastActivityTime: timestamp, + }; + context.HenloVaultUser.set(updatedUser); + } + } +); + +/** + * Handles ReservoirSet events - Creates/updates epoch with reservoir + */ +export const handleHenloVaultReservoirSet = HenloVault.ReservoirSet.handler( + async ({ event, context }) => { + const { epochId, strike, reservoir } = event.params; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + + const epochEntityId = `${epochId}_${chainId}`; + let epoch = await context.HenloVaultEpoch.get(epochEntityId); + + if (!epoch) { + // Create new epoch + epoch = { + id: epochEntityId, + epochId: BigInt(epochId), + strike: BigInt(strike), + closed: false, + depositsPaused: false, + timestamp: timestamp, + depositLimit: BigInt(0), + totalDeposits: BigInt(0), + reservoir: reservoir.toLowerCase(), + totalWhitelistDeposit: BigInt(0), + totalMatched: BigInt(0), + chainId, + }; + + // Update stats + const stats = await getOrCreateStats(context, chainId, timestamp); + const updatedStats: HenloVaultStats = { + ...stats, + totalEpochs: stats.totalEpochs + 1, + }; + context.HenloVaultStats.set(updatedStats); + } else { + // Update existing epoch with reservoir + epoch = { + ...epoch, + reservoir: reservoir.toLowerCase(), + }; + } + + context.HenloVaultEpoch.set(epoch); + } +); diff --git a/src/handlers/honey-jar-nfts.ts b/src/handlers/honey-jar-nfts.ts new file mode 100644 index 0000000..b743dc8 --- /dev/null +++ b/src/handlers/honey-jar-nfts.ts @@ -0,0 +1,501 @@ +/* + * HoneyJar NFT Event Handlers + * Handles NFT transfers, mints, burns, and cross-chain tracking + */ + +import { + CollectionStat, + GlobalCollectionStat, + Holder, + HoneyJar, + HoneyJar2Eth, + HoneyJar3Eth, + HoneyJar4Eth, + HoneyJar5Eth, + Honeycomb, + Mint, + Token, + Transfer, + UserBalance, +} from "generated"; + +import { + ZERO_ADDRESS, + BERACHAIN_TESTNET_ID, + PROXY_CONTRACTS, + ADDRESS_TO_COLLECTION, + COLLECTION_TO_GENERATION, + HOME_CHAIN_IDS, +} from "./constants"; + +/** + * Main transfer handler for all HoneyJar NFT contracts + */ +export async function handleTransfer( + event: any, + context: any, + collectionOverride?: string +) { + const { from, to, tokenId } = event.params; + const contractAddress = event.srcAddress.toLowerCase(); + const collection = + collectionOverride || ADDRESS_TO_COLLECTION[contractAddress] || "Unknown"; + const generation = COLLECTION_TO_GENERATION[collection] ?? -1; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + + // Skip unknown collections + if (generation < 0) return; + + // Create transfer record + const transferId = `${event.transaction.hash}_${event.logIndex}`; + const transfer: Transfer = { + id: transferId, + tokenId: BigInt(tokenId.toString()), + from: from.toLowerCase(), + to: to.toLowerCase(), + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + collection, + chainId, + }; + + context.Transfer.set(transfer); + + // Handle mint (from zero address) + if (from.toLowerCase() === ZERO_ADDRESS.toLowerCase()) { + await handleMint(event, context, collection, to, tokenId, timestamp); + } + + // Handle burn (to zero address) + if (to.toLowerCase() === ZERO_ADDRESS.toLowerCase()) { + await handleBurn(context, collection, tokenId, chainId); + } + + // Update token ownership + await updateTokenOwnership( + context, + collection, + tokenId, + from, + to, + timestamp, + chainId + ); + + // Load holders once to avoid duplicate queries + const fromLower = from.toLowerCase(); + const toLower = to.toLowerCase(); + const fromHolderId = `${collection}_${chainId}_${fromLower}`; + const toHolderId = `${collection}_${chainId}_${toLower}`; + + let fromHolder = fromLower !== ZERO_ADDRESS.toLowerCase() + ? await context.Holder.get(fromHolderId) + : null; + let toHolder = toLower !== ZERO_ADDRESS.toLowerCase() + ? await context.Holder.get(toHolderId) + : null; + + // Update holder balances (returns updated holders) + const updatedHolders = await updateHolderBalances( + context, + collection, + fromHolder, + toHolder, + fromHolderId, + toHolderId, + fromLower, + toLower, + generation, + timestamp, + chainId + ); + + // Update collection statistics (uses updated holders) + await updateCollectionStats( + context, + collection, + fromLower, + toLower, + updatedHolders.fromHolder, + updatedHolders.toHolder, + timestamp, + chainId + ); + + // Update global collection statistics + await updateGlobalCollectionStat(context, collection, timestamp); +} + +/** + * Handles NFT mint events + */ +async function handleMint( + event: any, + context: any, + collection: string, + to: string, + tokenId: any, + timestamp: bigint +) { + const mintId = `${event.transaction.hash}_${event.logIndex}_mint`; + const mint: Mint = { + id: mintId, + tokenId: BigInt(tokenId.toString()), + to: to.toLowerCase(), + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + collection, + chainId: event.chainId, + }; + + context.Mint.set(mint); +} + +/** + * Handles NFT burn events + */ +async function handleBurn( + context: any, + collection: string, + tokenId: any, + chainId: number +) { + const tokenIdStr = `${collection}_${chainId}_${tokenId}`; + const token = await context.Token.get(tokenIdStr); + if (token) { + // Create updated token object (immutable update) + const updatedToken = { + ...token, + isBurned: true, + owner: ZERO_ADDRESS, + }; + context.Token.set(updatedToken); + } +} + +/** + * Updates token ownership records + */ +async function updateTokenOwnership( + context: any, + collection: string, + tokenId: any, + from: string, + to: string, + timestamp: bigint, + chainId: number +) { + const tokenIdStr = `${collection}_${chainId}_${tokenId}`; + let token = await context.Token.get(tokenIdStr); + + if (!token) { + token = { + id: tokenIdStr, + collection, + chainId, + tokenId: BigInt(tokenId.toString()), + owner: to.toLowerCase(), + isBurned: to.toLowerCase() === ZERO_ADDRESS.toLowerCase(), + mintedAt: from.toLowerCase() === ZERO_ADDRESS.toLowerCase() ? timestamp : BigInt(0), + lastTransferTime: timestamp, + }; + } else { + // Create updated token object (immutable update) + token = { + ...token, + owner: to.toLowerCase(), + isBurned: to.toLowerCase() === ZERO_ADDRESS.toLowerCase(), + lastTransferTime: timestamp, + }; + } + + context.Token.set(token); +} + +/** + * Updates holder balance records + * Now accepts pre-loaded holders to avoid duplicate queries + */ +async function updateHolderBalances( + context: any, + collection: string, + fromHolder: any | null, + toHolder: any | null, + fromHolderId: string, + toHolderId: string, + fromLower: string, + toLower: string, + generation: number, + timestamp: bigint, + chainId: number +): Promise<{ fromHolder: any | null; toHolder: any | null }> { + const isMint = fromLower === ZERO_ADDRESS.toLowerCase(); + const isBurn = toLower === ZERO_ADDRESS.toLowerCase(); + + // Update 'from' holder (if not zero address) + if (!isMint && fromHolder) { + if (fromHolder.balance > 0) { + // Create updated holder object (immutable update) + const updatedFromHolder = { + ...fromHolder, + balance: fromHolder.balance - 1, + lastActivityTime: timestamp, + }; + context.Holder.set(updatedFromHolder); + fromHolder = updatedFromHolder; // Update reference for caller + } + + // Update user balance + await updateUserBalance( + context, + fromLower, + generation, + chainId, + -1, + false, + timestamp + ); + } + + // Update 'to' holder (if not zero address) + if (!isBurn) { + if (!toHolder) { + toHolder = { + id: toHolderId, + address: toLower, + balance: 0, + totalMinted: 0, + lastActivityTime: timestamp, + firstMintTime: isMint ? timestamp : undefined, + collection, + chainId, + }; + } + + // Create updated holder object (immutable update) + const updatedToHolder = { + ...toHolder, + balance: toHolder.balance + 1, + lastActivityTime: timestamp, + totalMinted: isMint ? toHolder.totalMinted + 1 : toHolder.totalMinted, + firstMintTime: isMint && !toHolder.firstMintTime ? timestamp : toHolder.firstMintTime, + }; + + context.Holder.set(updatedToHolder); + toHolder = updatedToHolder; // Update reference for caller + + // Update user balance + await updateUserBalance( + context, + toLower, + generation, + chainId, + 1, + isMint, + timestamp + ); + } + + return { fromHolder, toHolder }; +} + +/** + * Updates user balance across all chains + */ +async function updateUserBalance( + context: any, + address: string, + generation: number, + chainId: number, + balanceDelta: number, + isMint: boolean, + timestamp: bigint +) { + const userBalanceId = `${generation}_${address}`; + let userBalance = await context.UserBalance.get(userBalanceId); + + if (!userBalance) { + userBalance = { + id: userBalanceId, + address, + generation, + balanceHomeChain: 0, + balanceEthereum: 0, + balanceBerachain: 0, + balanceTotal: 0, + mintedHomeChain: 0, + mintedEthereum: 0, + mintedBerachain: 0, + mintedTotal: 0, + lastActivityTime: timestamp, + firstMintTime: isMint ? timestamp : undefined, + }; + } + + // Update balances based on chain + const homeChainId = HOME_CHAIN_IDS[generation]; + + // Create updated user balance object (immutable update) + const updatedUserBalance = { + ...userBalance, + balanceHomeChain: + chainId === homeChainId + ? Math.max(0, userBalance.balanceHomeChain + balanceDelta) + : userBalance.balanceHomeChain, + balanceEthereum: + chainId === 1 + ? Math.max(0, userBalance.balanceEthereum + balanceDelta) + : userBalance.balanceEthereum, + balanceBerachain: + chainId === BERACHAIN_TESTNET_ID + ? Math.max(0, userBalance.balanceBerachain + balanceDelta) + : userBalance.balanceBerachain, + balanceTotal: Math.max(0, userBalance.balanceTotal + balanceDelta), + mintedHomeChain: + chainId === homeChainId && isMint + ? userBalance.mintedHomeChain + 1 + : userBalance.mintedHomeChain, + mintedEthereum: + chainId === 1 && isMint + ? userBalance.mintedEthereum + 1 + : userBalance.mintedEthereum, + mintedBerachain: + chainId === BERACHAIN_TESTNET_ID && isMint + ? userBalance.mintedBerachain + 1 + : userBalance.mintedBerachain, + mintedTotal: isMint ? userBalance.mintedTotal + 1 : userBalance.mintedTotal, + firstMintTime: + isMint && !userBalance.firstMintTime + ? timestamp + : userBalance.firstMintTime, + lastActivityTime: timestamp, + }; + + context.UserBalance.set(updatedUserBalance); +} + +/** + * Updates collection statistics + * Now accepts pre-loaded holders to avoid duplicate queries + */ +async function updateCollectionStats( + context: any, + collection: string, + fromLower: string, + toLower: string, + fromHolder: any | null, + toHolder: any | null, + timestamp: bigint, + chainId: number +) { + const statsId = `${collection}_${chainId}`; + let stats = await context.CollectionStat.get(statsId); + + if (!stats) { + stats = { + id: statsId, + collection, + totalSupply: 0, + totalMinted: 0, + totalBurned: 0, + uniqueHolders: 0, + lastMintTime: undefined, + chainId, + }; + } + + const isMint = fromLower === ZERO_ADDRESS.toLowerCase(); + const isBurn = toLower === ZERO_ADDRESS.toLowerCase(); + + // Update unique holders count based on transfer + // We track this incrementally using the pre-loaded holders + let uniqueHoldersAdjustment = 0; + + // If this is a transfer TO a new holder + // Note: toHolder.balance is BEFORE the transfer, so balance === 0 means new holder + if (!isBurn && toHolder && toHolder.balance === 0) { + uniqueHoldersAdjustment += 1; + } + + // If this is a transfer FROM a holder that will become empty + // Note: fromHolder.balance is BEFORE the transfer, so balance === 1 means will be empty + if (!isMint && fromHolder && fromHolder.balance === 1) { + uniqueHoldersAdjustment -= 1; + } + + // Create updated stats object (immutable update) + const updatedStats = { + ...stats, + totalSupply: isMint ? stats.totalSupply + 1 : isBurn ? stats.totalSupply - 1 : stats.totalSupply, + totalMinted: isMint ? stats.totalMinted + 1 : stats.totalMinted, + totalBurned: isBurn ? stats.totalBurned + 1 : stats.totalBurned, + lastMintTime: isMint ? timestamp : stats.lastMintTime, + uniqueHolders: Math.max(0, stats.uniqueHolders + uniqueHoldersAdjustment), + }; + + context.CollectionStat.set(updatedStats); +} + +/** + * Updates global collection statistics across all chains + */ +export async function updateGlobalCollectionStat( + context: any, + collection: string, + timestamp: bigint +) { + const generation = COLLECTION_TO_GENERATION[collection] ?? -1; + if (generation < 0) return; + + const homeChainId = HOME_CHAIN_IDS[generation]; + const proxyAddress = PROXY_CONTRACTS[collection]?.toLowerCase(); + + // For now, we'll skip aggregating from all chains + // This would require maintaining running totals in the global stat itself + // TODO: Implement incremental updates to global stats + return; + + // Implementation removed due to getMany limitations + // This functionality would need to be handled differently in Envio + // Consider using a separate aggregation service or maintaining running totals +} + +// Export individual handlers for each contract +export const handleHoneyJarTransfer = HoneyJar.Transfer.handler( + async ({ event, context }) => { + await handleTransfer(event, context); + } +); + +export const handleHoneycombTransfer = Honeycomb.Transfer.handler( + async ({ event, context }) => { + await handleTransfer(event, context); + } +); + +export const handleHoneyJar2EthTransfer = HoneyJar2Eth.Transfer.handler( + async ({ event, context }) => { + await handleTransfer(event, context, "HoneyJar2"); + } +); + +export const handleHoneyJar3EthTransfer = HoneyJar3Eth.Transfer.handler( + async ({ event, context }) => { + await handleTransfer(event, context, "HoneyJar3"); + } +); + +export const handleHoneyJar4EthTransfer = HoneyJar4Eth.Transfer.handler( + async ({ event, context }) => { + await handleTransfer(event, context, "HoneyJar4"); + } +); + +export const handleHoneyJar5EthTransfer = HoneyJar5Eth.Transfer.handler( + async ({ event, context }) => { + await handleTransfer(event, context, "HoneyJar5"); + } +); \ No newline at end of file diff --git a/src/handlers/marketplaces/constants.ts b/src/handlers/marketplaces/constants.ts new file mode 100644 index 0000000..cb416ec --- /dev/null +++ b/src/handlers/marketplaces/constants.ts @@ -0,0 +1,76 @@ +/* + * NFT Marketplace contract addresses for secondary sale detection + * + * These addresses are used to identify when a transfer goes through + * a known marketplace (vs direct transfer or airdrop). + * + * Note: Most of these are cross-chain (same address on all EVM chains). + * Chain-specific addresses are noted where applicable. + */ + +// All known marketplace addresses in a single Set for efficient lookup +export const MARKETPLACE_ADDRESSES = new Set([ + // ============ OpenSea / Seaport Protocol ============ + // Seaport is used by OpenSea, Magic Eden, and others + "0x00000000006c3852cbef3e08e8df289169ede581", // Seaport 1.1 + "0x00000000000001ad428e4906ae43d8f9852d0dd6", // Seaport 1.4 + "0x00000000000000adc04c56bf30ac9d3c0aaf14dc", // Seaport 1.5 + "0x0000000000000068f116a894984e2db1123eb395", // Seaport 1.6 + "0x1e0049783f008a0085193e00003d00cd54003c71", // OpenSea Conduit (handles token transfers) + + // ============ Blur ============ + "0x000000000000ad05ccc4f10045630fb830b95127", // Blur: Marketplace + "0x39da41747a83aee658334415666f3ef92dd0d541", // Blur: Marketplace 2 (BlurSwap) + "0xb2ecfe4e4d61f8790bbb9de2d1259b9e2410cea5", // Blur: Marketplace 3 + "0x29469395eaf6f95920e59f858042f0e28d98a20b", // Blur: Blend (Lending/NFT-backed loans) + + // ============ LooksRare ============ + "0x59728544b08ab483533076417fbbb2fd0b17ce3a", // LooksRare: Exchange + "0x0000000000e655fae4d56241588680f86e3b2377", // LooksRare: Exchange V2 + + // ============ X2Y2 ============ + "0x6d7812d41a08bc2a910b562d8b56411964a4ed88", // X2Y2: Main Exchange (X2Y2_r1) + "0x74312363e45dcaba76c59ec49a7aa8a65a67eed3", // X2Y2: Exchange Proxy + + // ============ Rarible ============ + "0xcd4ec7b66fbc029c116ba9ffb3e59351c20b5b06", // Rarible: Exchange V1 + "0x9757f2d2b135150bbeb65308d4a91804107cd8d6", // Rarible: Exchange V2 + + // ============ Foundation ============ + "0xcda72070e455bb31c7690a170224ce43623d0b6f", // Foundation: Market + + // ============ SuperRare ============ + "0x65b49f7aee40347f5a90b714be4ef086f3fe5e2c", // SuperRare: Bazaar + "0x8c9f364bf7a56ed058fc63ef81c6cf09c833e656", // SuperRare: Marketplace + + // ============ Zora ============ + "0x76744367ae5a056381868f716bdf0b13ae1aeaa3", // Zora: Module Manager + "0x6170b3c3a54c3d8c854934cbc314ed479b2b29a3", // Zora: Asks V1.1 + + // ============ NFTX ============ + "0x0fc584529a2aefa997697fafacba5831fac0c22d", // NFTX: Marketplace Zap + + // ============ Sudoswap ============ + "0x2b2e8cda09bba9660dca5cb6233787738ad68329", // Sudoswap: LSSVMPairFactory + "0xa020d57ab0448ef74115c112d18a9c231cc86000", // Sudoswap: LSSVMRouter + + // ============ Gem / Genie (Aggregators, now part of OpenSea/Uniswap) ============ + "0x83c8f28c26bf6aaca652df1dbbe0e1b56f8baba2", // Gem: Swap + "0x0000000035634b55f3d99b071b5a354f48e10bef", // Gem: Swap 2 + "0x0a267cf51ef038fc00e71801f5a524aec06e4f07", // Genie: Swap +]); + +// Legacy export for backwards compatibility +export const SEAPORT_ADDRESSES = new Set([ + "0x00000000006c3852cbef3e08e8df289169ede581", + "0x00000000000001ad428e4906ae43d8f9852d0dd6", + "0x00000000000000adc04c56bf30ac9d3c0aaf14dc", + "0x0000000000000068f116a894984e2db1123eb395", +]); + +/** + * Check if an address is a known marketplace operator/contract + */ +export function isMarketplaceAddress(address: string): boolean { + return MARKETPLACE_ADDRESSES.has(address.toLowerCase()); +} diff --git a/src/handlers/mibera-collection.ts b/src/handlers/mibera-collection.ts new file mode 100644 index 0000000..67a2caf --- /dev/null +++ b/src/handlers/mibera-collection.ts @@ -0,0 +1,138 @@ +/** + * Mibera Collection Transfer Handler + * + * Tracks NFT transfers (including mints and burns) for activity feeds + * Used to replace /api/activity endpoint that fetches from mibera-squid + */ + +import { MiberaCollection } from "generated"; +import type { MiberaTransfer, MintActivity, NftBurn, NftBurnStats } from "generated"; +import { recordAction } from "../lib/actions"; +import { isMintFromZero, isBurnTransfer } from "../lib/mint-detection"; +import { BERACHAIN_ID } from "./constants"; + +const MIBERA_COLLECTION_ADDRESS = "0x6666397dfe9a8c469bf65dc744cb1c733416c420"; +const MIBERA_COLLECTION_KEY = "mibera"; + +/** + * Handle Transfer - Track all NFT transfers including mints and burns + * Event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + */ +export const handleMiberaCollectionTransfer = MiberaCollection.Transfer.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const from = event.params.from.toLowerCase(); + const to = event.params.to.toLowerCase(); + const tokenId = event.params.tokenId; + const txHash = event.transaction.hash; + const blockNumber = BigInt(event.block.number); + + const isMint = isMintFromZero(from); + const isBurn = isBurnTransfer(from, to); + + // Get transaction value (BERA paid) for mints + // Note: transaction.value is available because we added it to field_selection in config + const txValue = (event.transaction as any).value; + const amountPaid = txValue ? BigInt(txValue.toString()) : 0n; + + // Create transfer record + const transferId = `${txHash}_${event.logIndex}`; + const transfer: MiberaTransfer = { + id: transferId, + from, + to, + tokenId, + isMint, + timestamp, + blockNumber, + transactionHash: txHash, + chainId: BERACHAIN_ID, + }; + context.MiberaTransfer.set(transfer); + + // Create MintActivity record for mints (for unified activity feed) + if (isMint) { + const mintActivityId = `${txHash}_${tokenId}_${to}_MINT`; + const mintActivity: MintActivity = { + id: mintActivityId, + user: to, + contract: MIBERA_COLLECTION_ADDRESS, + tokenStandard: "ERC721", + tokenId, + quantity: 1n, + amountPaid, + activityType: "MINT", + timestamp, + blockNumber, + transactionHash: txHash, + operator: undefined, + chainId: BERACHAIN_ID, + }; + context.MintActivity.set(mintActivity); + + recordAction(context, { + actionType: "mibera_mint", + actor: to, + primaryCollection: MIBERA_COLLECTION_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: tokenId, + }); + } else if (isBurn) { + // Record burn event + const burnId = `${txHash}_${event.logIndex}`; + const burn: NftBurn = { + id: burnId, + collectionKey: MIBERA_COLLECTION_KEY, + tokenId, + from, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId: BERACHAIN_ID, + }; + context.NftBurn.set(burn); + + // Update burn stats + const statsId = `${BERACHAIN_ID}_${MIBERA_COLLECTION_KEY}`; + const existingStats = await context.NftBurnStats.get(statsId); + + const stats: NftBurnStats = { + id: statsId, + chainId: BERACHAIN_ID, + collectionKey: MIBERA_COLLECTION_KEY, + totalBurned: (existingStats?.totalBurned ?? 0) + 1, + uniqueBurners: existingStats?.uniqueBurners ?? 1, // TODO: Track unique burners properly + lastBurnTime: timestamp, + firstBurnTime: existingStats?.firstBurnTime ?? timestamp, + }; + context.NftBurnStats.set(stats); + + // Record action for activity feeds + recordAction(context, { + actionType: "mibera_burn", + actor: from, + primaryCollection: MIBERA_COLLECTION_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: tokenId, + }); + } else { + recordAction(context, { + actionType: "mibera_transfer", + actor: from, + primaryCollection: MIBERA_COLLECTION_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: tokenId, + context: { to }, + }); + } + } +); diff --git a/src/handlers/mibera-premint.ts b/src/handlers/mibera-premint.ts new file mode 100644 index 0000000..2874d98 --- /dev/null +++ b/src/handlers/mibera-premint.ts @@ -0,0 +1,207 @@ +/* + * Mibera Premint tracking handlers. + * + * Tracks participation and refund events from the Mibera premint contract. + * Records individual events plus aggregates user and phase-level statistics. + */ + +import { + MiberaPremint, + PremintParticipation, + PremintRefund, + PremintUser, + PremintPhaseStats, +} from "generated"; + +import { recordAction } from "../lib/actions"; + +const COLLECTION_KEY = "mibera_premint"; + +/** + * Handle Participated events - user contributed to premint + */ +export const handlePremintParticipated = MiberaPremint.Participated.handler( + async ({ event, context }) => { + const { phase, user, amount } = event.params; + + if (amount === 0n) { + return; // skip zero-amount participations + } + + const userAddress = user.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const id = `${event.transaction.hash}_${event.logIndex}`; + + // Record individual participation event + const participation: PremintParticipation = { + id, + phase, + user: userAddress, + amount, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }; + + context.PremintParticipation.set(participation); + + // Update user aggregate stats + const userId = `${userAddress}_${chainId}`; + const existingUser = await context.PremintUser.get(userId); + + const premintUser: PremintUser = { + id: userId, + user: userAddress, + totalContributed: (existingUser?.totalContributed ?? 0n) + amount, + totalRefunded: existingUser?.totalRefunded ?? 0n, + netContribution: + (existingUser?.totalContributed ?? 0n) + + amount - + (existingUser?.totalRefunded ?? 0n), + participationCount: (existingUser?.participationCount ?? 0) + 1, + refundCount: existingUser?.refundCount ?? 0, + firstParticipationTime: + existingUser?.firstParticipationTime ?? timestamp, + lastActivityTime: timestamp, + chainId, + }; + + context.PremintUser.set(premintUser); + + // Update phase stats + const phaseId = `${phase}_${chainId}`; + const existingPhase = await context.PremintPhaseStats.get(phaseId); + const isNewParticipant = !existingUser; + + const phaseStats: PremintPhaseStats = { + id: phaseId, + phase, + totalContributed: (existingPhase?.totalContributed ?? 0n) + amount, + totalRefunded: existingPhase?.totalRefunded ?? 0n, + netContribution: + (existingPhase?.totalContributed ?? 0n) + + amount - + (existingPhase?.totalRefunded ?? 0n), + uniqueParticipants: + (existingPhase?.uniqueParticipants ?? 0) + (isNewParticipant ? 1 : 0), + participationCount: (existingPhase?.participationCount ?? 0) + 1, + refundCount: existingPhase?.refundCount ?? 0, + chainId, + }; + + context.PremintPhaseStats.set(phaseStats); + + // Record action for activity feed/missions + recordAction(context, { + id, + actionType: "premint_participate", + actor: userAddress, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: amount, + numeric2: phase, + context: { + phase: phase.toString(), + contract: event.srcAddress.toLowerCase(), + }, + }); + } +); + +/** + * Handle Refunded events - user received refund from premint + */ +export const handlePremintRefunded = MiberaPremint.Refunded.handler( + async ({ event, context }) => { + const { phase, user, amount } = event.params; + + if (amount === 0n) { + return; // skip zero-amount refunds + } + + const userAddress = user.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const id = `${event.transaction.hash}_${event.logIndex}`; + + // Record individual refund event + const refund: PremintRefund = { + id, + phase, + user: userAddress, + amount, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }; + + context.PremintRefund.set(refund); + + // Update user aggregate stats + const userId = `${userAddress}_${chainId}`; + const existingUser = await context.PremintUser.get(userId); + + const premintUser: PremintUser = { + id: userId, + user: userAddress, + totalContributed: existingUser?.totalContributed ?? 0n, + totalRefunded: (existingUser?.totalRefunded ?? 0n) + amount, + netContribution: + (existingUser?.totalContributed ?? 0n) - + (existingUser?.totalRefunded ?? 0n) - + amount, + participationCount: existingUser?.participationCount ?? 0, + refundCount: (existingUser?.refundCount ?? 0) + 1, + firstParticipationTime: existingUser?.firstParticipationTime ?? undefined, + lastActivityTime: timestamp, + chainId, + }; + + context.PremintUser.set(premintUser); + + // Update phase stats + const phaseId = `${phase}_${chainId}`; + const existingPhase = await context.PremintPhaseStats.get(phaseId); + + const phaseStats: PremintPhaseStats = { + id: phaseId, + phase, + totalContributed: existingPhase?.totalContributed ?? 0n, + totalRefunded: (existingPhase?.totalRefunded ?? 0n) + amount, + netContribution: + (existingPhase?.totalContributed ?? 0n) - + (existingPhase?.totalRefunded ?? 0n) - + amount, + uniqueParticipants: existingPhase?.uniqueParticipants ?? 0, + participationCount: existingPhase?.participationCount ?? 0, + refundCount: (existingPhase?.refundCount ?? 0) + 1, + chainId, + }; + + context.PremintPhaseStats.set(phaseStats); + + // Record action for activity feed/missions + recordAction(context, { + id, + actionType: "premint_refund", + actor: userAddress, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: amount, + numeric2: phase, + context: { + phase: phase.toString(), + contract: event.srcAddress.toLowerCase(), + }, + }); + } +); diff --git a/src/handlers/mibera-sets.ts b/src/handlers/mibera-sets.ts new file mode 100644 index 0000000..17c0d7f --- /dev/null +++ b/src/handlers/mibera-sets.ts @@ -0,0 +1,241 @@ +/* + * Mibera Sets ERC-1155 tracking on Optimism. + * + * Tracks: + * - Mints: transfers from zero address OR distribution wallet (airdrops) + * - Transfers: all other transfers between users + * + * Token IDs: + * - 8, 9, 10, 11: Strong Set + * - 12: Super Set + */ + +import { MiberaSets, Erc1155MintEvent } from "generated"; + +import { recordAction } from "../lib/actions"; +import { isMintOrAirdrop } from "../lib/mint-detection"; +import { isMarketplaceAddress } from "./marketplaces/constants"; + +// Distribution wallet that airdropped Sets (transfers FROM this address = mints) +const DISTRIBUTION_WALLET = "0x4a8c9a29b23c4eac0d235729d5e0d035258cdfa7"; +const AIRDROP_WALLETS = new Set([DISTRIBUTION_WALLET]); + +// Collection key for action tracking +const COLLECTION_KEY = "mibera_sets"; + +// Token ID classifications +const STRONG_SET_TOKEN_IDS = [8n, 9n, 10n, 11n]; +const SUPER_SET_TOKEN_ID = 12n; + +/** + * Get the set tier based on token ID + */ +function getSetTier(tokenId: bigint): string { + if (STRONG_SET_TOKEN_IDS.includes(tokenId)) { + return "strong"; + } + if (tokenId === SUPER_SET_TOKEN_ID) { + return "super"; + } + return "unknown"; +} + +/** + * Handle TransferSingle events + * Tracks mints (from zero/distribution) and transfers (between users) + */ +export const handleMiberaSetsSingle = MiberaSets.TransferSingle.handler( + async ({ event, context }) => { + const { operator, from, to, id, value } = event.params; + const fromLower = from.toLowerCase(); + const toLower = to.toLowerCase(); + + const tokenId = BigInt(id.toString()); + const quantity = BigInt(value.toString()); + + if (quantity === 0n) { + return; + } + + const contractAddress = event.srcAddress.toLowerCase(); + const operatorLower = operator.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const eventId = `${event.transaction.hash}_${event.logIndex}`; + const setTier = getSetTier(tokenId); + + // Check if this is a mint or a transfer + const isMintEvent = isMintOrAirdrop(fromLower, AIRDROP_WALLETS); + + if (isMintEvent) { + // Create mint event record + const mintEvent: Erc1155MintEvent = { + id: eventId, + collectionKey: COLLECTION_KEY, + tokenId, + value: quantity, + minter: toLower, + operator: operatorLower, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }; + + context.Erc1155MintEvent.set(mintEvent); + + // Record mint action + recordAction(context, { + id: eventId, + actionType: "mint1155", + actor: toLower, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: quantity, + numeric2: tokenId, + context: { + tokenId: tokenId.toString(), + setTier, + operator: operatorLower, + contract: contractAddress, + from: fromLower, + }, + }); + } else { + // Record transfer action (secondary market / user-to-user) + recordAction(context, { + id: eventId, + actionType: "transfer1155", + actor: toLower, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: quantity, + numeric2: tokenId, + context: { + tokenId: tokenId.toString(), + setTier, + from: fromLower, + to: toLower, + operator: operatorLower, + contract: contractAddress, + isSecondary: true, + viaMarketplace: isMarketplaceAddress(operatorLower), + }, + }); + } + } +); + +/** + * Handle TransferBatch events + * Tracks mints (from zero/distribution) and transfers (between users) + */ +export const handleMiberaSetsBatch = MiberaSets.TransferBatch.handler( + async ({ event, context }) => { + const { operator, from, to, ids, values } = event.params; + const fromLower = from.toLowerCase(); + const toLower = to.toLowerCase(); + + const contractAddress = event.srcAddress.toLowerCase(); + const operatorLower = operator.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const txHash = event.transaction.hash; + + const idsArray = Array.from(ids); + const valuesArray = Array.from(values); + const length = Math.min(idsArray.length, valuesArray.length); + + // Check if this is a mint or a transfer + const isMintEvent = isMintOrAirdrop(fromLower, AIRDROP_WALLETS); + + for (let index = 0; index < length; index += 1) { + const rawId = idsArray[index]; + const rawValue = valuesArray[index]; + + if (rawId === undefined || rawValue === undefined || rawValue === null) { + continue; + } + + const quantity = BigInt(rawValue.toString()); + if (quantity === 0n) { + continue; + } + + const tokenId = BigInt(rawId.toString()); + const eventId = `${txHash}_${event.logIndex}_${index}`; + const setTier = getSetTier(tokenId); + + if (isMintEvent) { + // Create mint event record + const mintEvent: Erc1155MintEvent = { + id: eventId, + collectionKey: COLLECTION_KEY, + tokenId, + value: quantity, + minter: toLower, + operator: operatorLower, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId, + }; + + context.Erc1155MintEvent.set(mintEvent); + + // Record mint action + recordAction(context, { + id: eventId, + actionType: "mint1155", + actor: toLower, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash, + logIndex: event.logIndex, + numeric1: quantity, + numeric2: tokenId, + context: { + tokenId: tokenId.toString(), + setTier, + operator: operatorLower, + contract: contractAddress, + from: fromLower, + batchIndex: index, + }, + }); + } else { + // Record transfer action (secondary market / user-to-user) + recordAction(context, { + id: eventId, + actionType: "transfer1155", + actor: toLower, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash, + logIndex: event.logIndex, + numeric1: quantity, + numeric2: tokenId, + context: { + tokenId: tokenId.toString(), + setTier, + from: fromLower, + to: toLower, + operator: operatorLower, + contract: contractAddress, + batchIndex: index, + isSecondary: true, + viaMarketplace: isMarketplaceAddress(operatorLower), + }, + }); + } + } + } +); diff --git a/src/handlers/mibera-staking.ts b/src/handlers/mibera-staking.ts new file mode 100644 index 0000000..174d438 --- /dev/null +++ b/src/handlers/mibera-staking.ts @@ -0,0 +1,186 @@ +import { MiberaStaking } from "generated"; +import type { + handlerContext, + MiberaStakedToken as MiberaStakedTokenEntity, + MiberaStaker as MiberaStakerEntity, +} from "generated"; + +import { ZERO_ADDRESS } from "./constants"; +import { STAKING_CONTRACT_KEYS } from "./mibera-staking/constants"; + +const ZERO = ZERO_ADDRESS.toLowerCase(); + +/** + * Handles Mibera NFT transfers to/from PaddleFi and Jiko staking contracts + * Deposits: Transfer(user, stakingContract, tokenId) + * Withdrawals: Transfer(stakingContract, user, tokenId) + */ +export const handleMiberaStakingTransfer = MiberaStaking.Transfer.handler( + async ({ event, context }) => { + const from = event.params.from.toLowerCase(); + const to = event.params.to.toLowerCase(); + const tokenId = event.params.tokenId; + const chainId = event.chainId; + const txHash = event.transaction.hash; + const blockNumber = BigInt(event.block.number); + const timestamp = BigInt(event.block.timestamp); + + // Check if this is a deposit (transfer TO a staking contract) + const depositContractKey = STAKING_CONTRACT_KEYS[to]; + if (depositContractKey && from !== ZERO) { + await handleDeposit({ + context, + stakingContract: depositContractKey, + stakingContractAddress: to, + userAddress: from, + tokenId, + chainId, + txHash, + blockNumber, + timestamp, + }); + return; + } + + // Check if this is a withdrawal (transfer FROM a staking contract) + const withdrawContractKey = STAKING_CONTRACT_KEYS[from]; + if (withdrawContractKey && to !== ZERO) { + await handleWithdrawal({ + context, + stakingContract: withdrawContractKey, + stakingContractAddress: from, + userAddress: to, + tokenId, + chainId, + txHash, + blockNumber, + timestamp, + }); + return; + } + + // Not a staking-related transfer, ignore + } +); + +interface DepositArgs { + context: handlerContext; + stakingContract: string; + stakingContractAddress: string; + userAddress: string; + tokenId: bigint; + chainId: number; + txHash: string; + blockNumber: bigint; + timestamp: bigint; +} + +async function handleDeposit({ + context, + stakingContract, + stakingContractAddress, + userAddress, + tokenId, + chainId, + txHash, + blockNumber, + timestamp, +}: DepositArgs) { + // Create staked token record + const stakedTokenId = `${stakingContract}_${tokenId}`; + const stakedToken: MiberaStakedTokenEntity = { + id: stakedTokenId, + stakingContract, + contractAddress: stakingContractAddress, + tokenId, + owner: userAddress, + isStaked: true, + depositedAt: timestamp, + depositTxHash: txHash, + depositBlockNumber: blockNumber, + withdrawnAt: undefined, + withdrawTxHash: undefined, + withdrawBlockNumber: undefined, + chainId, + }; + context.MiberaStakedToken.set(stakedToken); + + // Update staker stats + const stakerId = `${stakingContract}_${userAddress}`; + const existingStaker = await context.MiberaStaker.get(stakerId); + + const staker: MiberaStakerEntity = existingStaker + ? { + ...existingStaker, + currentStakedCount: existingStaker.currentStakedCount + 1, + totalDeposits: existingStaker.totalDeposits + 1, + lastActivityTime: timestamp, + } + : { + id: stakerId, + stakingContract, + contractAddress: stakingContractAddress, + address: userAddress, + currentStakedCount: 1, + totalDeposits: 1, + totalWithdrawals: 0, + firstDepositTime: timestamp, + lastActivityTime: timestamp, + chainId, + }; + + context.MiberaStaker.set(staker); +} + +interface WithdrawalArgs { + context: handlerContext; + stakingContract: string; + stakingContractAddress: string; + userAddress: string; + tokenId: bigint; + chainId: number; + txHash: string; + blockNumber: bigint; + timestamp: bigint; +} + +async function handleWithdrawal({ + context, + stakingContract, + stakingContractAddress, + userAddress, + tokenId, + chainId, + txHash, + blockNumber, + timestamp, +}: WithdrawalArgs) { + // Update staked token record + const stakedTokenId = `${stakingContract}_${tokenId}`; + const existingStakedToken = await context.MiberaStakedToken.get(stakedTokenId); + + if (existingStakedToken) { + const updatedStakedToken: MiberaStakedTokenEntity = { + ...existingStakedToken, + isStaked: false, + withdrawnAt: timestamp, + withdrawTxHash: txHash, + withdrawBlockNumber: blockNumber, + }; + context.MiberaStakedToken.set(updatedStakedToken); + } + + // Update staker stats + const stakerId = `${stakingContract}_${userAddress}`; + const existingStaker = await context.MiberaStaker.get(stakerId); + + if (existingStaker) { + const updatedStaker: MiberaStakerEntity = { + ...existingStaker, + currentStakedCount: Math.max(0, existingStaker.currentStakedCount - 1), + totalWithdrawals: existingStaker.totalWithdrawals + 1, + lastActivityTime: timestamp, + }; + context.MiberaStaker.set(updatedStaker); + } +} diff --git a/src/handlers/mibera-staking/constants.ts b/src/handlers/mibera-staking/constants.ts new file mode 100644 index 0000000..10b3d49 --- /dev/null +++ b/src/handlers/mibera-staking/constants.ts @@ -0,0 +1,19 @@ +/** + * Mibera NFT staking contract addresses and mappings + */ + +// Staking contract addresses (lowercase) +export const PADDLEFI_VAULT = "0x242b7126f3c4e4f8cbd7f62571293e63e9b0a4e1"; +export const JIKO_STAKING = "0x8778ca41cf0b5cd2f9967ae06b691daff11db246"; + +// Map contract addresses to human-readable keys +export const STAKING_CONTRACT_KEYS: Record = { + [PADDLEFI_VAULT]: "paddlefi", + [JIKO_STAKING]: "jiko", +}; + +// Reverse mapping for lookups +export const STAKING_CONTRACT_ADDRESSES: Record = { + paddlefi: PADDLEFI_VAULT, + jiko: JIKO_STAKING, +}; diff --git a/src/handlers/mibera-treasury.ts b/src/handlers/mibera-treasury.ts new file mode 100644 index 0000000..ce4d5b5 --- /dev/null +++ b/src/handlers/mibera-treasury.ts @@ -0,0 +1,684 @@ +/** + * Mibera Treasury Handlers + * + * Tracks treasury-owned NFTs, purchases, RFV updates, and loan lifecycle + * Enables real-time marketplace availability queries and loan tracking + */ + +import { MiberaTreasury } from "generated"; +import type { TreasuryItem, TreasuryStats, TreasuryActivity, MiberaLoan, MiberaLoanStats, DailyRfvSnapshot } from "generated"; +import { recordAction } from "../lib/actions"; + +const BERACHAIN_ID = 80094; +const TREASURY_ADDRESS = "0xaa04f13994a7fcd86f3bbbf4054d239b88f2744d"; +const SECONDS_PER_DAY = 86400; + +/** + * Helper: Get or create TreasuryStats singleton + */ +async function getOrCreateStats( + context: any +): Promise { + const statsId = `${BERACHAIN_ID}_global`; + const existing = await context.TreasuryStats.get(statsId); + + if (existing) return existing; + + return { + id: statsId, + totalItemsOwned: 0, + totalItemsEverOwned: 0, + totalItemsSold: 0, + realFloorValue: BigInt(0), + lastRfvUpdate: undefined, + lastActivityAt: BigInt(0), + chainId: BERACHAIN_ID, + }; +} + +/** + * Helper: Get or create MiberaLoanStats singleton + */ +async function getOrCreateLoanStats( + context: any +): Promise { + const statsId = `${BERACHAIN_ID}_global`; + const existing = await context.MiberaLoanStats.get(statsId); + + if (existing) return existing; + + return { + id: statsId, + totalActiveLoans: 0, + totalLoansCreated: 0, + totalLoansRepaid: 0, + totalLoansDefaulted: 0, + totalAmountLoaned: BigInt(0), + totalNftsWithLoans: 0, + chainId: BERACHAIN_ID, + }; +} + +/** + * Helper: Get day number from timestamp (days since epoch) + */ +function getDayFromTimestamp(timestamp: bigint): number { + return Math.floor(Number(timestamp) / SECONDS_PER_DAY); +} + +// ============================================================================ +// LOAN LIFECYCLE HANDLERS +// ============================================================================ + +/** + * Handle LoanReceived - User creates a backing loan (collateral-based) + * Event: LoanReceived(uint256 loanId, uint256[] ids, uint256 amount, uint256 expiry) + */ +export const handleLoanReceived = MiberaTreasury.LoanReceived.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const loanId = event.params.loanId; + const tokenIds = event.params.ids; + const amount = event.params.amount; + const expiry = event.params.expiry; + const txHash = event.transaction.hash; + const user = event.transaction.from?.toLowerCase() || ""; + + // Create loan entity + const loanEntityId = `${BERACHAIN_ID}_backing_${loanId.toString()}`; + const loan: MiberaLoan = { + id: loanEntityId, + loanId, + loanType: "backing", + user, + tokenIds: tokenIds.map(id => id), + amount, + expiry, + status: "ACTIVE", + createdAt: timestamp, + repaidAt: undefined, + defaultedAt: undefined, + transactionHash: txHash, + chainId: BERACHAIN_ID, + }; + context.MiberaLoan.set(loan); + + // Update loan stats + const loanStats = await getOrCreateLoanStats(context); + context.MiberaLoanStats.set({ + ...loanStats, + totalActiveLoans: loanStats.totalActiveLoans + 1, + totalLoansCreated: loanStats.totalLoansCreated + 1, + totalAmountLoaned: loanStats.totalAmountLoaned + amount, + totalNftsWithLoans: loanStats.totalNftsWithLoans + tokenIds.length, + }); + + // Record action + recordAction(context, { + actionType: "loan_received", + actor: user, + primaryCollection: TREASURY_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: loanId, + numeric2: amount, + context: { tokenIds: tokenIds.map(id => id.toString()), expiry: expiry.toString() }, + }); + } +); + +/** + * Handle BackingLoanPayedBack - User repays backing loan + * Event: BackingLoanPayedBack(uint256 loanId, uint256 newTotalBacking) + */ +export const handleBackingLoanPayedBack = MiberaTreasury.BackingLoanPayedBack.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const loanId = event.params.loanId; + const txHash = event.transaction.hash; + + // Update loan status + const loanEntityId = `${BERACHAIN_ID}_backing_${loanId.toString()}`; + const existingLoan = await context.MiberaLoan.get(loanEntityId); + + if (existingLoan) { + context.MiberaLoan.set({ + ...existingLoan, + status: "REPAID", + repaidAt: timestamp, + }); + + // Update loan stats + const loanStats = await getOrCreateLoanStats(context); + context.MiberaLoanStats.set({ + ...loanStats, + totalActiveLoans: Math.max(0, loanStats.totalActiveLoans - 1), + totalLoansRepaid: loanStats.totalLoansRepaid + 1, + totalNftsWithLoans: Math.max(0, loanStats.totalNftsWithLoans - existingLoan.tokenIds.length), + }); + } + + // Record action + recordAction(context, { + actionType: "loan_repaid", + actor: existingLoan?.user || TREASURY_ADDRESS, + primaryCollection: TREASURY_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: loanId, + }); + } +); + +/** + * Handle ItemLoaned - User takes an item loan (single NFT from treasury) + * Event: ItemLoaned(uint256 loanId, uint256 itemId, uint256 expiry) + */ +export const handleItemLoaned = MiberaTreasury.ItemLoaned.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const loanId = event.params.loanId; + const itemId = event.params.itemId; + const expiry = event.params.expiry; + const txHash = event.transaction.hash; + const user = event.transaction.from?.toLowerCase() || ""; + + // Create loan entity + const loanEntityId = `${BERACHAIN_ID}_item_${loanId.toString()}`; + const loan: MiberaLoan = { + id: loanEntityId, + loanId, + loanType: "item", + user, + tokenIds: [itemId], + amount: BigInt(0), // Item loans don't have an amount + expiry, + status: "ACTIVE", + createdAt: timestamp, + repaidAt: undefined, + defaultedAt: undefined, + transactionHash: txHash, + chainId: BERACHAIN_ID, + }; + context.MiberaLoan.set(loan); + + // Update loan stats + const loanStats = await getOrCreateLoanStats(context); + context.MiberaLoanStats.set({ + ...loanStats, + totalActiveLoans: loanStats.totalActiveLoans + 1, + totalLoansCreated: loanStats.totalLoansCreated + 1, + totalNftsWithLoans: loanStats.totalNftsWithLoans + 1, + }); + + // Record action + recordAction(context, { + actionType: "item_loaned", + actor: user, + primaryCollection: TREASURY_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: loanId, + numeric2: itemId, + context: { expiry: expiry.toString() }, + }); + } +); + +/** + * Handle LoanItemSentBack - User returns item loan + * Event: LoanItemSentBack(uint256 loanId, uint256 newTotalBacking) + */ +export const handleLoanItemSentBack = MiberaTreasury.LoanItemSentBack.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const loanId = event.params.loanId; + const txHash = event.transaction.hash; + + // Update loan status + const loanEntityId = `${BERACHAIN_ID}_item_${loanId.toString()}`; + const existingLoan = await context.MiberaLoan.get(loanEntityId); + + if (existingLoan) { + context.MiberaLoan.set({ + ...existingLoan, + status: "REPAID", + repaidAt: timestamp, + }); + + // Update loan stats + const loanStats = await getOrCreateLoanStats(context); + context.MiberaLoanStats.set({ + ...loanStats, + totalActiveLoans: Math.max(0, loanStats.totalActiveLoans - 1), + totalLoansRepaid: loanStats.totalLoansRepaid + 1, + totalNftsWithLoans: Math.max(0, loanStats.totalNftsWithLoans - 1), + }); + } + + // Record action + recordAction(context, { + actionType: "item_loan_returned", + actor: existingLoan?.user || TREASURY_ADDRESS, + primaryCollection: TREASURY_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: loanId, + }); + } +); + +// ============================================================================ +// LOAN DEFAULT HANDLERS (existing handlers updated) +// ============================================================================ + +/** + * Handle BackingLoanExpired - NFT(s) become treasury-owned when backing loan defaults + * Event: BackingLoanExpired(uint256 loanId, uint256 newTotalBacking) + * + * Note: BackingLoanExpired involves collateral NFTs from a loan, not a single tokenId. + * The loan contains multiple collateral items. We record the event but can't determine + * specific tokenIds without querying the contract. + */ +export const handleBackingLoanExpired = MiberaTreasury.BackingLoanExpired.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const loanId = event.params.loanId; + const newTotalBacking = event.params.newTotalBacking; + const txHash = event.transaction.hash; + + // Update loan status to DEFAULTED + const loanEntityId = `${BERACHAIN_ID}_backing_${loanId.toString()}`; + const existingLoan = await context.MiberaLoan.get(loanEntityId); + + if (existingLoan) { + context.MiberaLoan.set({ + ...existingLoan, + status: "DEFAULTED", + defaultedAt: timestamp, + }); + + // Update loan stats + const loanStats = await getOrCreateLoanStats(context); + context.MiberaLoanStats.set({ + ...loanStats, + totalActiveLoans: Math.max(0, loanStats.totalActiveLoans - 1), + totalLoansDefaulted: loanStats.totalLoansDefaulted + 1, + totalNftsWithLoans: Math.max(0, loanStats.totalNftsWithLoans - existingLoan.tokenIds.length), + }); + } + + // Record activity (we don't know specific tokenIds for backing loans) + const activityId = `${txHash}_${event.logIndex}`; + const activity: TreasuryActivity = { + id: activityId, + activityType: "backing_loan_defaulted", + tokenId: undefined, + user: existingLoan?.user, + amount: newTotalBacking, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId: BERACHAIN_ID, + }; + context.TreasuryActivity.set(activity); + + // Update stats - we can't know exact count increase without contract query + const stats = await getOrCreateStats(context); + context.TreasuryStats.set({ + ...stats, + lastActivityAt: timestamp, + }); + + // Record action for activity feed + recordAction(context, { + actionType: "treasury_backing_loan_expired", + actor: TREASURY_ADDRESS, + primaryCollection: TREASURY_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: loanId, + numeric2: newTotalBacking, + }); + } +); + +/** + * Handle ItemLoanExpired - NFT becomes treasury-owned when item loan defaults + * Event: ItemLoanExpired(uint256 loanId, uint256 newTotalBacking) + * + * For item loans, the loanId can be used to look up the specific itemId. + * The item that was loaned now belongs to the treasury. + */ +export const handleItemLoanExpired = MiberaTreasury.ItemLoanExpired.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const loanId = event.params.loanId; + const newTotalBacking = event.params.newTotalBacking; + const txHash = event.transaction.hash; + + // Update loan status to DEFAULTED + const loanEntityId = `${BERACHAIN_ID}_item_${loanId.toString()}`; + const existingLoan = await context.MiberaLoan.get(loanEntityId); + + if (existingLoan) { + context.MiberaLoan.set({ + ...existingLoan, + status: "DEFAULTED", + defaultedAt: timestamp, + }); + + // Update loan stats + const loanStats = await getOrCreateLoanStats(context); + context.MiberaLoanStats.set({ + ...loanStats, + totalActiveLoans: Math.max(0, loanStats.totalActiveLoans - 1), + totalLoansDefaulted: loanStats.totalLoansDefaulted + 1, + totalNftsWithLoans: Math.max(0, loanStats.totalNftsWithLoans - 1), + }); + } + + // For item loans, we use loanId as tokenId (based on contract structure) + // The itemLoanDetails function uses loanId to track the item + const itemIdStr = loanId.toString(); + const existingItem = await context.TreasuryItem.get(itemIdStr); + + const treasuryItem: TreasuryItem = existingItem + ? { + ...existingItem, + isTreasuryOwned: true, + acquiredAt: timestamp, + acquiredVia: "item_loan_default", + acquiredTxHash: txHash, + // Clear purchase fields if item is being re-acquired + purchasedAt: undefined, + purchasedBy: undefined, + purchasedTxHash: undefined, + purchasePrice: undefined, + } + : { + id: itemIdStr, + tokenId: loanId, + isTreasuryOwned: true, + acquiredAt: timestamp, + acquiredVia: "item_loan_default", + acquiredTxHash: txHash, + purchasedAt: undefined, + purchasedBy: undefined, + purchasedTxHash: undefined, + purchasePrice: undefined, + chainId: BERACHAIN_ID, + }; + context.TreasuryItem.set(treasuryItem); + + // Update stats + const stats = await getOrCreateStats(context); + const wasAlreadyOwned = existingItem?.isTreasuryOwned === true; + context.TreasuryStats.set({ + ...stats, + totalItemsOwned: stats.totalItemsOwned + (wasAlreadyOwned ? 0 : 1), + totalItemsEverOwned: stats.totalItemsEverOwned + (wasAlreadyOwned ? 0 : 1), + lastActivityAt: timestamp, + }); + + // Record activity + const activityId = `${txHash}_${event.logIndex}`; + context.TreasuryActivity.set({ + id: activityId, + activityType: "item_acquired", + tokenId: loanId, + user: undefined, + amount: newTotalBacking, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId: BERACHAIN_ID, + }); + + recordAction(context, { + actionType: "treasury_item_acquired", + actor: TREASURY_ADDRESS, + primaryCollection: TREASURY_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: loanId, + context: { source: "item_loan_default" }, + }); + } +); + +/** + * Handle ItemPurchased - NFT purchased from treasury + * Event: ItemPurchased(uint256 itemId, uint256 newTotalBacking) + */ +export const handleItemPurchased = MiberaTreasury.ItemPurchased.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const itemId = event.params.itemId; + const newTotalBacking = event.params.newTotalBacking; + const txHash = event.transaction.hash; + const buyer = event.transaction.from?.toLowerCase(); + + // Update treasury item + const itemIdStr = itemId.toString(); + const existingItem = await context.TreasuryItem.get(itemIdStr); + + // Get current RFV for purchase price recording + const stats = await getOrCreateStats(context); + + if (existingItem) { + context.TreasuryItem.set({ + ...existingItem, + isTreasuryOwned: false, + purchasedAt: timestamp, + purchasedBy: buyer, + purchasedTxHash: txHash, + purchasePrice: stats.realFloorValue, + }); + } else { + // Item exists on-chain but wasn't indexed yet (historical case) + context.TreasuryItem.set({ + id: itemIdStr, + tokenId: itemId, + isTreasuryOwned: false, + acquiredAt: undefined, + acquiredVia: undefined, + acquiredTxHash: undefined, + purchasedAt: timestamp, + purchasedBy: buyer, + purchasedTxHash: txHash, + purchasePrice: stats.realFloorValue, + chainId: BERACHAIN_ID, + }); + } + + // Update stats + const wasOwned = existingItem?.isTreasuryOwned === true; + context.TreasuryStats.set({ + ...stats, + totalItemsOwned: Math.max(0, stats.totalItemsOwned - (wasOwned ? 1 : 0)), + totalItemsSold: stats.totalItemsSold + 1, + lastActivityAt: timestamp, + }); + + // Record activity + const activityId = `${txHash}_${event.logIndex}`; + context.TreasuryActivity.set({ + id: activityId, + activityType: "item_purchased", + tokenId: itemId, + user: buyer, + amount: stats.realFloorValue, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId: BERACHAIN_ID, + }); + + recordAction(context, { + actionType: "treasury_purchase", + actor: buyer || TREASURY_ADDRESS, + primaryCollection: TREASURY_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: itemId, + numeric2: stats.realFloorValue, + }); + } +); + +/** + * Handle ItemRedeemed - NFT deposited into treasury + * Event: ItemRedeemed(uint256 itemId, uint256 newTotalBacking) + */ +export const handleItemRedeemed = MiberaTreasury.ItemRedeemed.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const itemId = event.params.itemId; + const newTotalBacking = event.params.newTotalBacking; + const txHash = event.transaction.hash; + const depositor = event.transaction.from?.toLowerCase(); + + // Create/update treasury item + const itemIdStr = itemId.toString(); + const existingItem = await context.TreasuryItem.get(itemIdStr); + + const treasuryItem: TreasuryItem = existingItem + ? { + ...existingItem, + isTreasuryOwned: true, + acquiredAt: timestamp, + acquiredVia: "redemption", + acquiredTxHash: txHash, + // Clear purchase fields if item is being re-acquired + purchasedAt: undefined, + purchasedBy: undefined, + purchasedTxHash: undefined, + purchasePrice: undefined, + } + : { + id: itemIdStr, + tokenId: itemId, + isTreasuryOwned: true, + acquiredAt: timestamp, + acquiredVia: "redemption", + acquiredTxHash: txHash, + purchasedAt: undefined, + purchasedBy: undefined, + purchasedTxHash: undefined, + purchasePrice: undefined, + chainId: BERACHAIN_ID, + }; + context.TreasuryItem.set(treasuryItem); + + // Update stats + const stats = await getOrCreateStats(context); + const wasAlreadyOwned = existingItem?.isTreasuryOwned === true; + context.TreasuryStats.set({ + ...stats, + totalItemsOwned: stats.totalItemsOwned + (wasAlreadyOwned ? 0 : 1), + totalItemsEverOwned: stats.totalItemsEverOwned + (wasAlreadyOwned ? 0 : 1), + lastActivityAt: timestamp, + }); + + // Record activity + const activityId = `${txHash}_${event.logIndex}`; + context.TreasuryActivity.set({ + id: activityId, + activityType: "item_acquired", + tokenId: itemId, + user: depositor, + amount: newTotalBacking, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId: BERACHAIN_ID, + }); + + recordAction(context, { + actionType: "treasury_item_redeemed", + actor: depositor || TREASURY_ADDRESS, + primaryCollection: TREASURY_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: itemId, + numeric2: newTotalBacking, + }); + } +); + +/** + * Handle RFVChanged - Real Floor Value updated + * Event: RFVChanged(uint256 indexed newRFV) + * + * Also creates daily RFV snapshots for historical charting + */ +export const handleRFVChanged = MiberaTreasury.RFVChanged.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const newRFV = event.params.newRFV; + const txHash = event.transaction.hash; + + // Update stats with new RFV + const stats = await getOrCreateStats(context); + context.TreasuryStats.set({ + ...stats, + realFloorValue: newRFV, + lastRfvUpdate: timestamp, + lastActivityAt: timestamp, + }); + + // Create/update daily RFV snapshot (one per day, always latest RFV for that day) + const day = getDayFromTimestamp(timestamp); + const snapshotId = `${BERACHAIN_ID}_${day}`; + const snapshot: DailyRfvSnapshot = { + id: snapshotId, + day, + rfv: newRFV, + timestamp, + chainId: BERACHAIN_ID, + }; + context.DailyRfvSnapshot.set(snapshot); + + // Record activity + const activityId = `${txHash}_${event.logIndex}`; + context.TreasuryActivity.set({ + id: activityId, + activityType: "rfv_updated", + tokenId: undefined, + user: undefined, + amount: newRFV, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId: BERACHAIN_ID, + }); + + recordAction(context, { + actionType: "treasury_rfv_updated", + actor: TREASURY_ADDRESS, + primaryCollection: TREASURY_ADDRESS, + timestamp, + chainId: BERACHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: newRFV, + }); + } +); diff --git a/src/handlers/mibera-zora.ts b/src/handlers/mibera-zora.ts new file mode 100644 index 0000000..b98e22d --- /dev/null +++ b/src/handlers/mibera-zora.ts @@ -0,0 +1,207 @@ +/* + * Mibera Zora ERC-1155 tracking on Optimism. + * + * Tracks: + * - Mints: transfers from zero address + * - Transfers: all other transfers between users + * + * This is a Zora platform ERC-1155 collection. + */ + +import { MiberaZora1155, Erc1155MintEvent } from "generated"; + +import { recordAction } from "../lib/actions"; +import { isMintFromZero } from "../lib/mint-detection"; + +// Collection key for action tracking +const COLLECTION_KEY = "mibera_zora"; + +/** + * Handle TransferSingle events + * Tracks mints (from zero) and transfers (between users) + */ +export const handleMiberaZoraSingle = MiberaZora1155.TransferSingle.handler( + async ({ event, context }) => { + const { operator, from, to, id, value } = event.params; + const fromLower = from.toLowerCase(); + const toLower = to.toLowerCase(); + + const tokenId = BigInt(id.toString()); + const quantity = BigInt(value.toString()); + + if (quantity === 0n) { + return; + } + + const contractAddress = event.srcAddress.toLowerCase(); + const operatorLower = operator.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const eventId = `${event.transaction.hash}_${event.logIndex}`; + + // Check if this is a mint or a transfer + const isMintEvent = isMintFromZero(fromLower); + + if (isMintEvent) { + // Create mint event record + const mintEvent: Erc1155MintEvent = { + id: eventId, + collectionKey: COLLECTION_KEY, + tokenId, + value: quantity, + minter: toLower, + operator: operatorLower, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }; + + context.Erc1155MintEvent.set(mintEvent); + + // Record mint action + recordAction(context, { + id: eventId, + actionType: "mint1155", + actor: toLower, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: quantity, + numeric2: tokenId, + context: { + tokenId: tokenId.toString(), + operator: operatorLower, + contract: contractAddress, + from: fromLower, + }, + }); + } else { + // Record transfer action (secondary market / user-to-user) + recordAction(context, { + id: eventId, + actionType: "transfer1155", + actor: toLower, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: quantity, + numeric2: tokenId, + context: { + tokenId: tokenId.toString(), + from: fromLower, + to: toLower, + operator: operatorLower, + contract: contractAddress, + }, + }); + } + } +); + +/** + * Handle TransferBatch events + * Tracks mints (from zero) and transfers (between users) + */ +export const handleMiberaZoraBatch = MiberaZora1155.TransferBatch.handler( + async ({ event, context }) => { + const { operator, from, to, ids, values } = event.params; + const fromLower = from.toLowerCase(); + const toLower = to.toLowerCase(); + + const contractAddress = event.srcAddress.toLowerCase(); + const operatorLower = operator.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const txHash = event.transaction.hash; + + const idsArray = Array.from(ids); + const valuesArray = Array.from(values); + const length = Math.min(idsArray.length, valuesArray.length); + + // Check if this is a mint or a transfer + const isMintEvent = isMintFromZero(fromLower); + + for (let index = 0; index < length; index += 1) { + const rawId = idsArray[index]; + const rawValue = valuesArray[index]; + + if (rawId === undefined || rawValue === undefined || rawValue === null) { + continue; + } + + const quantity = BigInt(rawValue.toString()); + if (quantity === 0n) { + continue; + } + + const tokenId = BigInt(rawId.toString()); + const eventId = `${txHash}_${event.logIndex}_${index}`; + + if (isMintEvent) { + // Create mint event record + const mintEvent: Erc1155MintEvent = { + id: eventId, + collectionKey: COLLECTION_KEY, + tokenId, + value: quantity, + minter: toLower, + operator: operatorLower, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId, + }; + + context.Erc1155MintEvent.set(mintEvent); + + // Record mint action + recordAction(context, { + id: eventId, + actionType: "mint1155", + actor: toLower, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash, + logIndex: event.logIndex, + numeric1: quantity, + numeric2: tokenId, + context: { + tokenId: tokenId.toString(), + operator: operatorLower, + contract: contractAddress, + from: fromLower, + batchIndex: index, + }, + }); + } else { + // Record transfer action (secondary market / user-to-user) + recordAction(context, { + id: eventId, + actionType: "transfer1155", + actor: toLower, + primaryCollection: COLLECTION_KEY, + timestamp, + chainId, + txHash, + logIndex: event.logIndex, + numeric1: quantity, + numeric2: tokenId, + context: { + tokenId: tokenId.toString(), + from: fromLower, + to: toLower, + operator: operatorLower, + contract: contractAddress, + batchIndex: index, + }, + }); + } + } + } +); diff --git a/src/handlers/milady-collection.ts b/src/handlers/milady-collection.ts new file mode 100644 index 0000000..efc2358 --- /dev/null +++ b/src/handlers/milady-collection.ts @@ -0,0 +1,75 @@ +/** + * Milady Collection Transfer Handler + * + * Tracks NFT burns for the Milady Maker collection on Ethereum mainnet. + * Only records transfers to burn addresses (zero or dead address). + */ + +import { MiladyCollection } from "generated"; +import type { NftBurn, NftBurnStats } from "generated"; +import { recordAction } from "../lib/actions"; +import { isBurnTransfer } from "../lib/mint-detection"; + +const MILADY_COLLECTION_ADDRESS = "0x5af0d9827e0c53e4799bb226655a1de152a425a5"; +const MILADY_COLLECTION_KEY = "milady"; +const ETHEREUM_CHAIN_ID = 1; + +/** + * Handle Transfer - Track NFT burns (transfers to zero/dead address) + * Event: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) + */ +export const handleMiladyCollectionTransfer = MiladyCollection.Transfer.handler( + async ({ event, context }) => { + const timestamp = BigInt(event.block.timestamp); + const from = event.params.from.toLowerCase(); + const to = event.params.to.toLowerCase(); + const tokenId = event.params.tokenId; + const txHash = event.transaction.hash; + + const isBurn = isBurnTransfer(from, to); + + // Only track burns for Milady - we don't need full transfer history + if (isBurn) { + // Record burn event + const burnId = `${txHash}_${event.logIndex}`; + const burn: NftBurn = { + id: burnId, + collectionKey: MILADY_COLLECTION_KEY, + tokenId, + from, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId: ETHEREUM_CHAIN_ID, + }; + context.NftBurn.set(burn); + + // Update burn stats + const statsId = `${ETHEREUM_CHAIN_ID}_${MILADY_COLLECTION_KEY}`; + const existingStats = await context.NftBurnStats.get(statsId); + + const stats: NftBurnStats = { + id: statsId, + chainId: ETHEREUM_CHAIN_ID, + collectionKey: MILADY_COLLECTION_KEY, + totalBurned: (existingStats?.totalBurned ?? 0) + 1, + uniqueBurners: existingStats?.uniqueBurners ?? 1, // TODO: Track unique burners properly + lastBurnTime: timestamp, + firstBurnTime: existingStats?.firstBurnTime ?? timestamp, + }; + context.NftBurnStats.set(stats); + + // Record action for activity feeds + recordAction(context, { + actionType: "milady_burn", + actor: from, + primaryCollection: MILADY_COLLECTION_ADDRESS, + timestamp, + chainId: ETHEREUM_CHAIN_ID, + txHash, + logIndex: event.logIndex, + numeric1: tokenId, + }); + } + } +); diff --git a/src/handlers/mints.ts b/src/handlers/mints.ts new file mode 100644 index 0000000..40ce54c --- /dev/null +++ b/src/handlers/mints.ts @@ -0,0 +1,64 @@ +/* + * Generalized ERC721 mint tracking handler. + * + * Captures Transfer events where the token is minted (from zero address) + * and stores normalized MintEvent entities for downstream consumers. + */ + +import { GeneralMints, MintEvent } from "generated"; + +import { recordAction } from "../lib/actions"; + +import { ZERO_ADDRESS } from "./constants"; +import { MINT_COLLECTION_KEYS } from "./mints/constants"; + +const ZERO = ZERO_ADDRESS.toLowerCase(); + +export const handleGeneralMintTransfer = GeneralMints.Transfer.handler( + async ({ event, context }) => { + const { from, to, tokenId } = event.params; + + const fromLower = from.toLowerCase(); + if (fromLower !== ZERO) { + return; // Skip non-mint transfers + } + + const contractAddress = event.srcAddress.toLowerCase(); + const collectionKey = + MINT_COLLECTION_KEYS[contractAddress] ?? contractAddress; + + const id = `${event.transaction.hash}_${event.logIndex}`; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const minter = to.toLowerCase(); + const mintEvent: MintEvent = { + id, + collectionKey, + tokenId: BigInt(tokenId.toString()), + minter, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + encodedTraits: undefined, // Will be populated by VM Minted handler if applicable + }; + + context.MintEvent.set(mintEvent); + + recordAction(context, { + id, + actionType: "mint", + actor: minter, + primaryCollection: collectionKey, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: 1n, + context: { + tokenId: tokenId.toString(), + contract: contractAddress, + }, + }); + } +); diff --git a/src/handlers/mints/constants.ts b/src/handlers/mints/constants.ts new file mode 100644 index 0000000..9e8329f --- /dev/null +++ b/src/handlers/mints/constants.ts @@ -0,0 +1,16 @@ +/* + * Collection metadata for generalized mint tracking + * + * Maps contract address (lowercase) to a friendly collection key. + */ + +export const MINT_COLLECTION_KEYS: Record = { + "0x048327a187b944ddac61c6e202bfccd20d17c008": "mibera_vm", + "0x80283fbf2b8e50f6ddf9bfc4a90a8336bc90e38f": "mibera_drugs", + "0xeca03517c5195f1edd634da6d690d6c72407c40c": "mibera_drugs", + "0x230945e0ed56ef4de871a6c0695de265de23d8d8": "mibera_gif", + "0x4b08a069381efbb9f08c73d6b2e975c9be3c4684": "mibera_tarot", +}; + +export const CANDIES_MARKET_ADDRESS = + "0x80283fbf2b8e50f6ddf9bfc4a90a8336bc90e38f"; diff --git a/src/handlers/mints1155.ts b/src/handlers/mints1155.ts new file mode 100644 index 0000000..9a7fc67 --- /dev/null +++ b/src/handlers/mints1155.ts @@ -0,0 +1,199 @@ +/* + * ERC1155 mint tracking for Candies Market collections. + * Also tracks orders (non-mint transfers) for SilkRoad marketplace. + */ + +import { CandiesMarket1155, Erc1155MintEvent, CandiesInventory, MiberaOrder } from "generated"; + +import { ZERO_ADDRESS, BERACHAIN_ID } from "./constants"; +import { MINT_COLLECTION_KEYS } from "./mints/constants"; +import { recordAction } from "../lib/actions"; + +const ZERO = ZERO_ADDRESS.toLowerCase(); + +// SilkRoad marketplace address - only create orders for this contract +const SILKROAD_ADDRESS = "0x80283fbf2b8e50f6ddf9bfc4a90a8336bc90e38f"; + +const getCollectionKey = (address: string): string => { + const key = MINT_COLLECTION_KEYS[address.toLowerCase()]; + return key ?? address.toLowerCase(); +}; + +export const handleCandiesMintSingle = CandiesMarket1155.TransferSingle.handler( + async ({ event, context }) => { + const { operator, from, to, id, value } = event.params; + const fromLower = from.toLowerCase(); + const contractAddress = event.srcAddress.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const tokenId = BigInt(id.toString()); + const quantity = BigInt(value.toString()); + + // Track orders for SilkRoad marketplace (non-mint transfers on Berachain) + if (fromLower !== ZERO && contractAddress === SILKROAD_ADDRESS && chainId === BERACHAIN_ID) { + const orderId = `${chainId}_${event.transaction.hash}_${event.logIndex}`; + const order: MiberaOrder = { + id: orderId, + user: to.toLowerCase(), + tokenId, + amount: quantity, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }; + context.MiberaOrder.set(order); + } + + // Skip mint processing if not a mint + if (fromLower !== ZERO) { + return; + } + + const collectionKey = getCollectionKey(contractAddress); + const mintId = `${event.transaction.hash}_${event.logIndex}`; + const minter = to.toLowerCase(); + const operatorLower = operator.toLowerCase(); + + const mintEvent: Erc1155MintEvent = { + id: mintId, + collectionKey, + tokenId, + value: quantity, + minter, + operator: operatorLower, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }; + + context.Erc1155MintEvent.set(mintEvent); + + // Update CandiesInventory tracking + const inventoryId = `${contractAddress}_${tokenId}`; + const existingInventory = await context.CandiesInventory.get(inventoryId); + + const inventoryUpdate: CandiesInventory = { + id: inventoryId, + contract: contractAddress, + tokenId, + currentSupply: existingInventory + ? existingInventory.currentSupply + quantity + : quantity, + mintCount: existingInventory ? existingInventory.mintCount + 1 : 1, + lastMintTime: timestamp, + chainId, + }; + + context.CandiesInventory.set(inventoryUpdate); + + recordAction(context, { + id: mintId, + actionType: "mint1155", + actor: minter, + primaryCollection: collectionKey, + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: quantity, + context: { + tokenId: tokenId.toString(), + operator: operatorLower, + contract: contractAddress, + }, + }); + } +); + +export const handleCandiesMintBatch = CandiesMarket1155.TransferBatch.handler( + async ({ event, context }) => { + const { operator, from, to, ids, values } = event.params; + + if (from.toLowerCase() !== ZERO) { + return; + } + + const contractAddress = event.srcAddress.toLowerCase(); + const collectionKey = getCollectionKey(contractAddress); + const operatorLower = operator.toLowerCase(); + const minterLower = to.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const txHash = event.transaction.hash; + + const idsArray = Array.from(ids); + const valuesArray = Array.from(values); + + const length = Math.min(idsArray.length, valuesArray.length); + + for (let index = 0; index < length; index += 1) { + const rawId = idsArray[index]; + const rawValue = valuesArray[index]; + + if (rawId === undefined || rawValue === undefined || rawValue === null) { + continue; + } + + const quantity = BigInt(rawValue.toString()); + if (quantity === 0n) { + continue; + } + + const tokenId = BigInt(rawId.toString()); + const mintId = `${event.transaction.hash}_${event.logIndex}_${index}`; + + const mintEvent: Erc1155MintEvent = { + id: mintId, + collectionKey, + tokenId, + value: quantity, + minter: minterLower, + operator: operatorLower, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: txHash, + chainId, + }; + + context.Erc1155MintEvent.set(mintEvent); + + // Update CandiesInventory tracking + const inventoryId = `${contractAddress}_${tokenId}`; + const existingInventory = await context.CandiesInventory.get(inventoryId); + + const inventoryUpdate: CandiesInventory = { + id: inventoryId, + contract: contractAddress, + tokenId, + currentSupply: existingInventory + ? existingInventory.currentSupply + quantity + : quantity, + mintCount: existingInventory ? existingInventory.mintCount + 1 : 1, + lastMintTime: timestamp, + chainId, + }; + + context.CandiesInventory.set(inventoryUpdate); + + recordAction(context, { + id: mintId, + actionType: "mint1155", + actor: minterLower, + primaryCollection: collectionKey, + timestamp, + chainId, + txHash, + logIndex: event.logIndex, + numeric1: quantity, + context: { + tokenId: tokenId.toString(), + operator: operatorLower, + contract: contractAddress, + batchIndex: index, + }, + }); + } + } +); diff --git a/src/handlers/moneycomb-vault.ts b/src/handlers/moneycomb-vault.ts new file mode 100644 index 0000000..203d9c7 --- /dev/null +++ b/src/handlers/moneycomb-vault.ts @@ -0,0 +1,335 @@ +/* + * MoneycombVault Event Handlers + * Handles vault operations including account management, burns, shares, and rewards + */ + +import { + MoneycombVault, + UserVaultSummary, + Vault, + VaultActivity, +} from "generated"; + +/** + * Handles vault account opening events + */ +export const handleAccountOpened = MoneycombVault.AccountOpened.handler( + async ({ event, context }) => { + const { user, accountIndex, honeycombId } = event.params; + const userLower = user.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + + // Create vault record + const vaultId = `${userLower}_${accountIndex}`; + const vault: Vault = { + id: vaultId, + user: userLower, + accountIndex: Number(accountIndex), + honeycombId: BigInt(honeycombId.toString()), + isActive: true, + shares: BigInt(0), + totalBurned: 0, + burnedGen1: false, + burnedGen2: false, + burnedGen3: false, + burnedGen4: false, + burnedGen5: false, + burnedGen6: false, + createdAt: timestamp, + closedAt: undefined, + lastActivityTime: timestamp, + }; + + context.Vault.set(vault); + + // Create activity record + const activityId = `${event.transaction.hash}_${event.logIndex}`; + const activity: VaultActivity = { + id: activityId, + user: userLower, + accountIndex: Number(accountIndex), + activityType: "ACCOUNT_OPENED", + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + honeycombId: BigInt(honeycombId.toString()), + hjGen: undefined, + shares: undefined, + reward: undefined, + }; + + context.VaultActivity.set(activity); + + // Update user summary + await updateUserVaultSummary( + context, + userLower, + timestamp, + "ACCOUNT_OPENED" + ); + } +); + +/** + * Handles vault account closing events + */ +export const handleAccountClosed = MoneycombVault.AccountClosed.handler( + async ({ event, context }) => { + const { user, accountIndex, honeycombId } = event.params; + const userLower = user.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + + // Update vault record + const vaultId = `${userLower}_${accountIndex}`; + const vault = await context.Vault.get(vaultId); + + if (vault) { + // Create updated vault object (immutable update) + const updatedVault = { + ...vault, + isActive: false, + closedAt: timestamp, + lastActivityTime: timestamp, + }; + context.Vault.set(updatedVault); + } + + // Create activity record + const activityId = `${event.transaction.hash}_${event.logIndex}`; + const activity: VaultActivity = { + id: activityId, + user: userLower, + accountIndex: Number(accountIndex), + activityType: "ACCOUNT_CLOSED", + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + honeycombId: BigInt(honeycombId.toString()), + hjGen: undefined, + shares: undefined, + reward: undefined, + }; + + context.VaultActivity.set(activity); + + // Update user summary + await updateUserVaultSummary( + context, + userLower, + timestamp, + "ACCOUNT_CLOSED" + ); + } +); + +/** + * Handles HoneyJar NFT burn events for vault + */ +export const handleHJBurned = MoneycombVault.HJBurned.handler( + async ({ event, context }) => { + const { user, accountIndex, hjGen } = event.params; + const userLower = user.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + const generation = Number(hjGen); + + // Update vault record + const vaultId = `${userLower}_${accountIndex}`; + const vault = await context.Vault.get(vaultId); + + if (vault) { + // Create updated vault object (immutable update) + const updatedVault = { + ...vault, + totalBurned: vault.totalBurned + 1, + burnedGen1: generation === 1 ? true : vault.burnedGen1, + burnedGen2: generation === 2 ? true : vault.burnedGen2, + burnedGen3: generation === 3 ? true : vault.burnedGen3, + burnedGen4: generation === 4 ? true : vault.burnedGen4, + burnedGen5: generation === 5 ? true : vault.burnedGen5, + burnedGen6: generation === 6 ? true : vault.burnedGen6, + lastActivityTime: timestamp, + }; + context.Vault.set(updatedVault); + } + + // Create activity record + const activityId = `${event.transaction.hash}_${event.logIndex}`; + const activity: VaultActivity = { + id: activityId, + user: userLower, + accountIndex: Number(accountIndex), + activityType: "HJ_BURNED", + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + honeycombId: undefined, + hjGen: generation, + shares: undefined, + reward: undefined, + }; + + context.VaultActivity.set(activity); + + // Update user summary + await updateUserVaultSummary( + context, + userLower, + timestamp, + "HJ_BURNED" + ); + } +); + +/** + * Handles shares minting events + */ +export const handleSharesMinted = MoneycombVault.SharesMinted.handler( + async ({ event, context }) => { + const { user, accountIndex, shares } = event.params; + const userLower = user.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + + // Update vault record + const vaultId = `${userLower}_${accountIndex}`; + const vault = await context.Vault.get(vaultId); + + if (vault) { + // Create updated vault object (immutable update) + const updatedVault = { + ...vault, + shares: vault.shares + BigInt(shares.toString()), + lastActivityTime: timestamp, + }; + context.Vault.set(updatedVault); + } + + // Create activity record + const activityId = `${event.transaction.hash}_${event.logIndex}`; + const activity: VaultActivity = { + id: activityId, + user: userLower, + accountIndex: Number(accountIndex), + activityType: "SHARES_MINTED", + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + honeycombId: undefined, + hjGen: undefined, + shares: BigInt(shares.toString()), + reward: undefined, + }; + + context.VaultActivity.set(activity); + + // Update user summary + await updateUserVaultSummary( + context, + userLower, + timestamp, + "SHARES_MINTED", + BigInt(shares.toString()) + ); + } +); + +/** + * Handles reward claim events + */ +export const handleRewardClaimed = MoneycombVault.RewardClaimed.handler( + async ({ event, context }) => { + const { user, reward } = event.params; + const userLower = user.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + + // Create activity record + const activityId = `${event.transaction.hash}_${event.logIndex}`; + const activity: VaultActivity = { + id: activityId, + user: userLower, + accountIndex: -1, // Reward claims don't specify account + activityType: "REWARD_CLAIMED", + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + honeycombId: undefined, + hjGen: undefined, + shares: undefined, + reward: BigInt(reward.toString()), + }; + + context.VaultActivity.set(activity); + + // Update user summary + await updateUserVaultSummary( + context, + userLower, + timestamp, + "REWARD_CLAIMED", + undefined, + BigInt(reward.toString()) + ); + } +); + +/** + * Updates user vault summary statistics + */ +async function updateUserVaultSummary( + context: any, + user: string, + timestamp: bigint, + activityType: string, + shares?: bigint, + reward?: bigint +) { + const summaryId = user; + let summary = await context.UserVaultSummary.get(summaryId); + + if (!summary) { + summary = { + id: summaryId, + user, + totalVaults: 0, + activeVaults: 0, + totalShares: BigInt(0), + totalRewardsClaimed: BigInt(0), + totalHJsBurned: 0, + firstVaultTime: timestamp, + lastActivityTime: timestamp, + }; + } + + // Create updated summary object (immutable update) + const updatedSummary = { + ...summary, + totalVaults: + activityType === "ACCOUNT_OPENED" + ? summary.totalVaults + 1 + : summary.totalVaults, + activeVaults: + activityType === "ACCOUNT_OPENED" + ? summary.activeVaults + 1 + : activityType === "ACCOUNT_CLOSED" + ? Math.max(0, summary.activeVaults - 1) + : summary.activeVaults, + totalHJsBurned: + activityType === "HJ_BURNED" + ? summary.totalHJsBurned + 1 + : summary.totalHJsBurned, + totalShares: + activityType === "SHARES_MINTED" && shares + ? summary.totalShares + shares + : summary.totalShares, + totalRewardsClaimed: + activityType === "REWARD_CLAIMED" && reward + ? summary.totalRewardsClaimed + reward + : summary.totalRewardsClaimed, + firstVaultTime: + activityType === "ACCOUNT_OPENED" && !summary.firstVaultTime + ? timestamp + : summary.firstVaultTime, + lastActivityTime: timestamp, + }; + + context.UserVaultSummary.set(updatedSummary); +} \ No newline at end of file diff --git a/src/handlers/paddlefi.ts b/src/handlers/paddlefi.ts new file mode 100644 index 0000000..d40765f --- /dev/null +++ b/src/handlers/paddlefi.ts @@ -0,0 +1,222 @@ +/* + * PaddleFi Lending Protocol Handler + * + * Tracks: + * - Mint (Supply BERA): Lenders deposit BERA into the lending pool + * - Pawn: Borrowers deposit Mibera NFTs as collateral + * + * Contract: 0x242b7126F3c4E4F8CbD7f62571293e63E9b0a4E1 (Berachain) + */ + +import { PaddleFi } from "generated"; +import type { + handlerContext, + PaddleSupply as PaddleSupplyEntity, + PaddlePawn as PaddlePawnEntity, + PaddleSupplier as PaddleSupplierEntity, + PaddleBorrower as PaddleBorrowerEntity, +} from "generated"; + +import { recordAction } from "../lib/actions"; + +/** + * Handle Mint events (Supply BERA) + * Emitted when a lender deposits BERA into the lending pool + */ +export const handlePaddleMint = PaddleFi.Mint.handler( + async ({ event, context }) => { + const minter = event.params.minter.toLowerCase(); + const mintAmount = event.params.mintAmount; + const mintTokens = event.params.mintTokens; + const chainId = event.chainId; + const txHash = event.transaction.hash; + const logIndex = event.logIndex; + const timestamp = BigInt(event.block.timestamp); + const blockNumber = BigInt(event.block.number); + + const eventId = `${txHash}_${logIndex}`; + + // Create supply event record + const supplyEvent: PaddleSupplyEntity = { + id: eventId, + minter, + mintAmount, + mintTokens, + timestamp, + blockNumber, + transactionHash: txHash, + chainId, + }; + context.PaddleSupply.set(supplyEvent); + + // Update supplier aggregate stats + await updateSupplierStats({ + context, + address: minter, + mintAmount, + mintTokens, + timestamp, + chainId, + }); + + // Record action for activity feed + recordAction(context, { + id: eventId, + actionType: "paddle_supply", + actor: minter, + primaryCollection: "paddlefi", + timestamp, + chainId, + txHash, + logIndex: Number(logIndex), + numeric1: mintAmount, + numeric2: mintTokens, + context: { + type: "supply_bera", + mintAmount: mintAmount.toString(), + pTokensReceived: mintTokens.toString(), + }, + }); + } +); + +/** + * Handle Pawn events (Deposit NFT as collateral) + * Emitted when a borrower deposits Mibera NFTs to take a loan + */ +export const handlePaddlePawn = PaddleFi.Pawn.handler( + async ({ event, context }) => { + const borrower = event.params.borrower.toLowerCase(); + const nftIds = event.params.nftIds.map((id) => BigInt(id.toString())); + const chainId = event.chainId; + const txHash = event.transaction.hash; + const logIndex = event.logIndex; + const timestamp = BigInt(event.block.timestamp); + const blockNumber = BigInt(event.block.number); + + const eventId = `${txHash}_${logIndex}`; + + // Create pawn event record + const pawnEvent: PaddlePawnEntity = { + id: eventId, + borrower, + nftIds, + timestamp, + blockNumber, + transactionHash: txHash, + chainId, + }; + context.PaddlePawn.set(pawnEvent); + + // Update borrower aggregate stats + await updateBorrowerStats({ + context, + address: borrower, + nftCount: nftIds.length, + timestamp, + chainId, + }); + + // Record action for activity feed + recordAction(context, { + id: eventId, + actionType: "paddle_pawn", + actor: borrower, + primaryCollection: "paddlefi", + timestamp, + chainId, + txHash, + logIndex: Number(logIndex), + numeric1: BigInt(nftIds.length), + context: { + type: "pawn_nft", + nftIds: nftIds.map((id) => id.toString()), + nftCount: nftIds.length, + }, + }); + } +); + +// Helper functions + +interface UpdateSupplierArgs { + context: handlerContext; + address: string; + mintAmount: bigint; + mintTokens: bigint; + timestamp: bigint; + chainId: number; +} + +async function updateSupplierStats({ + context, + address, + mintAmount, + mintTokens, + timestamp, + chainId, +}: UpdateSupplierArgs) { + const supplierId = address; + const existing = await context.PaddleSupplier.get(supplierId); + + const supplier: PaddleSupplierEntity = existing + ? { + ...existing, + totalSupplied: existing.totalSupplied + mintAmount, + totalPTokens: existing.totalPTokens + mintTokens, + supplyCount: existing.supplyCount + 1, + lastActivityTime: timestamp, + } + : { + id: supplierId, + address, + totalSupplied: mintAmount, + totalPTokens: mintTokens, + supplyCount: 1, + firstSupplyTime: timestamp, + lastActivityTime: timestamp, + chainId, + }; + + context.PaddleSupplier.set(supplier); +} + +interface UpdateBorrowerArgs { + context: handlerContext; + address: string; + nftCount: number; + timestamp: bigint; + chainId: number; +} + +async function updateBorrowerStats({ + context, + address, + nftCount, + timestamp, + chainId, +}: UpdateBorrowerArgs) { + const borrowerId = address; + const existing = await context.PaddleBorrower.get(borrowerId); + + const borrower: PaddleBorrowerEntity = existing + ? { + ...existing, + totalNftsPawned: existing.totalNftsPawned + nftCount, + currentNftsPawned: existing.currentNftsPawned + nftCount, + pawnCount: existing.pawnCount + 1, + lastActivityTime: timestamp, + } + : { + id: borrowerId, + address, + totalNftsPawned: nftCount, + currentNftsPawned: nftCount, + pawnCount: 1, + firstPawnTime: timestamp, + lastActivityTime: timestamp, + chainId, + }; + + context.PaddleBorrower.set(borrower); +} diff --git a/src/handlers/seaport.ts b/src/handlers/seaport.ts new file mode 100644 index 0000000..facfd87 --- /dev/null +++ b/src/handlers/seaport.ts @@ -0,0 +1,129 @@ +/** + * Seaport Handler - Tracks marketplace trades for activity feed + * + * Creates MintActivity records for both SALE and PURCHASE events + * Used to track secondary market activity contributing to liquid backing + */ + +import { Seaport } from "generated"; +import type { MintActivity } from "generated"; + +const BERACHAIN_ID = 80094; +const MIBERA_CONTRACT = "0x6666397dfe9a8c469bf65dc744cb1c733416c420"; +const WBERA_CONTRACT = "0x6969696969696969696969696969696969696969"; + +// Tuple indices for offer: [itemType, token, identifier, amount] +const OFFER_ITEM_TYPE = 0; +const OFFER_TOKEN = 1; +const OFFER_IDENTIFIER = 2; +const OFFER_AMOUNT = 3; + +// Tuple indices for consideration: [itemType, token, identifier, amount, recipient] +const CONS_ITEM_TYPE = 0; +const CONS_TOKEN = 1; +const CONS_IDENTIFIER = 2; +const CONS_AMOUNT = 3; + +/** + * Handle OrderFulfilled - Track Seaport marketplace trades + * Creates both SALE (for seller) and PURCHASE (for buyer) activity records + */ +export const handleSeaportOrderFulfilled = Seaport.OrderFulfilled.handler( + async ({ event, context }) => { + const { offerer, recipient, offer, consideration } = event.params; + const timestamp = BigInt(event.block.timestamp); + const blockNumber = BigInt(event.block.number); + const txHash = event.transaction.hash; + + const offererLower = offerer.toLowerCase(); + const recipientLower = recipient.toLowerCase(); + + // Skip if offerer and recipient are the same (self-trade) + if (offererLower === recipientLower) { + return; + } + + // Check if offer array has items + if (!offer || offer.length === 0) { + return; + } + + const firstOffer = offer[0]; + const firstOfferToken = String(firstOffer[OFFER_TOKEN]).toLowerCase(); + + let amountPaid = 0n; + let tokenId: bigint | undefined; + let seller: string | undefined; + let buyer: string | undefined; + + // Scenario 1: WBERA offered (offerer is buyer paying BERA, recipient is seller) + if (firstOfferToken === WBERA_CONTRACT) { + amountPaid = BigInt(firstOffer[OFFER_AMOUNT].toString()); + + // Check if Mibera NFT is in consideration + if ( + consideration && + consideration.length > 0 && + String(consideration[0][CONS_TOKEN]).toLowerCase() === MIBERA_CONTRACT + ) { + tokenId = BigInt(consideration[0][CONS_IDENTIFIER].toString()); + buyer = offererLower; + seller = recipientLower; + } + } + // Scenario 2: Mibera NFT offered (offerer is seller, recipient is buyer) + else if (firstOfferToken === MIBERA_CONTRACT) { + tokenId = BigInt(firstOffer[OFFER_IDENTIFIER].toString()); + seller = offererLower; + buyer = recipientLower; + + // Sum up native token payments from consideration (itemType 0 = native ETH/BERA) + for (const item of consideration) { + if (Number(item[CONS_ITEM_TYPE]) === 0) { + amountPaid += BigInt(item[CONS_AMOUNT].toString()); + } + } + } + + // If we found a valid Mibera trade, create activity records + if (tokenId !== undefined && seller && buyer && amountPaid > 0n) { + // Create SALE record for seller + const saleId = `${txHash}_${tokenId}_${seller}_SALE`; + const saleActivity: MintActivity = { + id: saleId, + user: seller, + contract: MIBERA_CONTRACT, + tokenStandard: "ERC721", + tokenId, + quantity: 1n, + amountPaid, + activityType: "SALE", + timestamp, + blockNumber, + transactionHash: txHash, + operator: undefined, + chainId: BERACHAIN_ID, + }; + context.MintActivity.set(saleActivity); + + // Create PURCHASE record for buyer + const purchaseId = `${txHash}_${tokenId}_${buyer}_PURCHASE`; + const purchaseActivity: MintActivity = { + id: purchaseId, + user: buyer, + contract: MIBERA_CONTRACT, + tokenStandard: "ERC721", + tokenId, + quantity: 1n, + amountPaid, + activityType: "PURCHASE", + timestamp, + blockNumber, + transactionHash: txHash, + operator: undefined, + chainId: BERACHAIN_ID, + }; + context.MintActivity.set(purchaseActivity); + } + } +); diff --git a/src/handlers/sf-vaults.ts b/src/handlers/sf-vaults.ts new file mode 100644 index 0000000..ea10503 --- /dev/null +++ b/src/handlers/sf-vaults.ts @@ -0,0 +1,907 @@ +/** + * Set & Forgetti Vault Handlers + * + * Tracks ERC4626 vault deposits/withdrawals and MultiRewards staking/claiming + * Maintains stateful position tracking and vault-level statistics + * Supports dynamic strategy migrations with historical tracking + * + * RPC: Uses ENVIO_RPC_URL env var for strategy lookups + */ + +import { + SFVaultERC4626, + SFMultiRewards, + SFPosition, + SFVaultStats, + SFVaultStrategy, + SFMultiRewardsPosition, +} from "generated"; + +import { experimental_createEffect, S } from "envio"; +import { createPublicClient, http, parseAbi, defineChain } from "viem"; + +import { recordAction } from "../lib/actions"; + +// Define Berachain since it may not be in viem/chains yet +const berachain = defineChain({ + id: 80094, + name: "Berachain", + nativeCurrency: { + decimals: 18, + name: "BERA", + symbol: "BERA", + }, + rpcUrls: { + default: { + http: ["https://rpc.berachain.com"], + }, + }, + blockExplorers: { + default: { name: "Berascan", url: "https://berascan.com" }, + }, +}); + +const BERACHAIN_ID = 80094; + +/** + * Vault Configuration Mapping + * Maps vault addresses to their initial (first) strategy, MultiRewards contract, and metadata + * These are the original deployments - subsequent strategies are tracked via StrategyUpdated events + */ +interface VaultConfig { + vault: string; + multiRewards: string; + kitchenToken: string; + kitchenTokenSymbol: string; + strategy: string; +} + +const VAULT_CONFIGS: Record = { + // HLKD1B + "0x4b8e4c84901c8404f4cfe438a33ee9ef72f345d1": { + vault: "0x4b8e4c84901c8404f4cfe438a33ee9ef72f345d1", + multiRewards: "0xbfda8746f8abee58a58f87c1d2bb2d9eee6e3554", + kitchenToken: "0xf0edfc3e122db34773293e0e5b2c3a58492e7338", + kitchenTokenSymbol: "HLKD1B", + strategy: "0x9e9a8aa97991d4aa2e5d7fed2b19fa24f2e95eed", + }, + // HLKD690M + "0x962d17044fb34abbf523f6bff93d05c0214d7bb3": { + vault: "0x962d17044fb34abbf523f6bff93d05c0214d7bb3", + multiRewards: "0x01c1c9c333ea81e422e421db63030e882851eb3d", + kitchenToken: "0x8ab854dc0672d7a13a85399a56cb628fb22102d6", + kitchenTokenSymbol: "HLKD690M", + strategy: "0xafbcc65965e355667e67e3d98389c46227aefdf0", + }, + // HLKD420M + "0xa51dd612f0a03cbc81652078f631fb5f7081ff0f": { + vault: "0xa51dd612f0a03cbc81652078f631fb5f7081ff0f", + multiRewards: "0x4eedee17cdfbd9910c421ecc9d3401c70c0bf624", + kitchenToken: "0xf07fa3ece9741d408d643748ff85710bedef25ba", + kitchenTokenSymbol: "HLKD420M", + strategy: "0x70a637ecfc0bb266627021530c5a08c86d4f0c7a", + }, + // HLKD330M + "0xb7411dde748fb6d13ce04b9aac5e1fea8ad264dd": { + vault: "0xb7411dde748fb6d13ce04b9aac5e1fea8ad264dd", + multiRewards: "0xec204cb71d69f1b4d334c960d16a68364b604857", + kitchenToken: "0x37dd8850919ebdca911c383211a70839a94b0539", + kitchenTokenSymbol: "HLKD330M", + strategy: "0x2a23627a52fc2efee0452648fbdbe9dba4c0bee8", + }, + // HLKD100M + "0x6552e503dfc5103bb31a3fe96ac3c3a092607f36": { + vault: "0x6552e503dfc5103bb31a3fe96ac3c3a092607f36", + multiRewards: "0x00192ce353151563b3bd8664327d882c7ac45cb8", + kitchenToken: "0x7bdf98ddeed209cfa26bd2352b470ac8b5485ec5", + kitchenTokenSymbol: "HLKD100M", + strategy: "0x15a0172c3b37a7d93a54bf762d6442b51408c0f2", + }, +}; + +/** + * Lookup table mapping strategy addresses to their known multiRewards addresses + * Used as fallback when RPC calls fail (e.g., contract doesn't exist at historical block) + */ +const STRATEGY_TO_MULTI_REWARDS: Record = { + "0x9e9a8aa97991d4aa2e5d7fed2b19fa24f2e95eed": "0xbfda8746f8abee58a58f87c1d2bb2d9eee6e3554", // HLKD1B + "0xafbcc65965e355667e67e3d98389c46227aefdf0": "0x01c1c9c333ea81e422e421db63030e882851eb3d", // HLKD690M + "0x70a637ecfc0bb266627021530c5a08c86d4f0c7a": "0x4eedee17cdfbd9910c421ecc9d3401c70c0bf624", // HLKD420M + "0x2a23627a52fc2efee0452648fbdbe9dba4c0bee8": "0xec204cb71d69f1b4d334c960d16a68364b604857", // HLKD330M + "0x15a0172c3b37a7d93a54bf762d6442b51408c0f2": "0x00192ce353151563b3bd8664327d882c7ac45cb8", // HLKD100M +}; + +/** + * Effect to query multiRewardsAddress from a strategy contract at a specific block + * Used when handling StrategyUpdated events to get the new MultiRewards address + * Falls back to hardcoded mapping if RPC call fails + */ +export const getMultiRewardsAddress = experimental_createEffect( + { + name: "getMultiRewardsAddress", + input: { + strategyAddress: S.string, + blockNumber: S.bigint, + }, + output: S.string, + cache: true, + }, + async ({ input, context }) => { + const strategyLower = input.strategyAddress.toLowerCase(); + + // First try RPC call + const rpcUrl = process.env.ENVIO_RPC_URL || "https://rpc.berachain.com"; + const client = createPublicClient({ + chain: berachain, + transport: http(rpcUrl), + }); + + try { + const multiRewards = await client.readContract({ + address: input.strategyAddress as `0x${string}`, + abi: parseAbi(["function multiRewardsAddress() view returns (address)"]), + functionName: "multiRewardsAddress", + blockNumber: input.blockNumber, + }); + + return (multiRewards as string).toLowerCase(); + } catch (error) { + // Fallback to hardcoded mapping if RPC fails + const fallback = STRATEGY_TO_MULTI_REWARDS[strategyLower]; + if (fallback) { + context.log.warn(`RPC call failed for strategy ${strategyLower}, using fallback multiRewards: ${fallback}`); + return fallback; + } + + context.log.error(`Failed to get multiRewardsAddress for strategy ${input.strategyAddress} at block ${input.blockNumber}: ${error}`); + throw error; + } + } +); + +/** + * Helper function to get vault info from a MultiRewards address + * Searches through SFVaultStrategy records and falls back to hardcoded configs + */ +async function getVaultFromMultiRewards( + context: any, + multiRewardsAddress: string +): Promise<{ vault: string; config: VaultConfig } | null> { + // First check hardcoded configs (for initial MultiRewards) + for (const [vaultAddr, config] of Object.entries(VAULT_CONFIGS)) { + if (config.multiRewards === multiRewardsAddress) { + return { vault: vaultAddr, config }; + } + } + + // Then search SFVaultStrategy records for dynamically registered MultiRewards + const strategies = await context.SFVaultStrategy.getWhere.multiRewards.eq(multiRewardsAddress); + + if (strategies && strategies.length > 0) { + const strategyRecord = strategies[0]; + const baseConfig = VAULT_CONFIGS[strategyRecord.vault]; + if (baseConfig) { + return { + vault: strategyRecord.vault, + config: { + ...baseConfig, + strategy: strategyRecord.strategy, + multiRewards: strategyRecord.multiRewards, + }, + }; + } + } + + return null; +} + +/** + * Helper function to ensure initial strategy record exists for a vault + * Called on first deposit to bootstrap the SFVaultStrategy table + */ +async function ensureInitialStrategy( + context: any, + vaultAddress: string, +): Promise { + const config = VAULT_CONFIGS[vaultAddress]; + if (!config) return; + + const strategyId = `${BERACHAIN_ID}_${vaultAddress}_${config.strategy}`; + const existing = await context.SFVaultStrategy.get(strategyId); + + if (!existing) { + context.SFVaultStrategy.set({ + id: strategyId, + vault: vaultAddress, + strategy: config.strategy, + multiRewards: config.multiRewards, + kitchenToken: config.kitchenToken, + kitchenTokenSymbol: config.kitchenTokenSymbol, + activeFrom: BigInt(0), // Active from the beginning + activeTo: undefined, + isActive: true, + chainId: BERACHAIN_ID, + }); + } +} + +/** + * Helper function to get the current active strategy for a vault + */ +async function getActiveStrategy( + context: any, + vaultAddress: string +): Promise<{ strategy: string; multiRewards: string } | null> { + const config = VAULT_CONFIGS[vaultAddress]; + if (!config) return null; + + // Query for active strategy + const strategies = await context.SFVaultStrategy.getWhere.vault.eq(vaultAddress); + + if (strategies && strategies.length > 0) { + // Find the active one + for (const strategy of strategies) { + if (strategy.isActive) { + return { + strategy: strategy.strategy, + multiRewards: strategy.multiRewards, + }; + } + } + } + + // Fall back to hardcoded config + return { + strategy: config.strategy, + multiRewards: config.multiRewards, + }; +} + +/** + * Register new MultiRewards contracts dynamically when strategy is updated + */ +SFVaultERC4626.StrategyUpdated.contractRegister(async ({ event, context }) => { + const newStrategy = event.params.newStrategy.toLowerCase(); + + // First check if we have a hardcoded mapping (faster and more reliable) + const fallbackMultiRewards = STRATEGY_TO_MULTI_REWARDS[newStrategy]; + if (fallbackMultiRewards) { + context.addSFMultiRewards(fallbackMultiRewards); + return; + } + + // Query the new strategy's multiRewardsAddress at this block + // Note: contractRegister doesn't have access to context.effect, so we make direct RPC call + const rpcUrl = process.env.ENVIO_RPC_URL || "https://rpc.berachain.com"; + const client = createPublicClient({ + chain: berachain, + transport: http(rpcUrl), + }); + + try { + const multiRewards = await client.readContract({ + address: newStrategy as `0x${string}`, + abi: parseAbi(["function multiRewardsAddress() view returns (address)"]), + functionName: "multiRewardsAddress", + blockNumber: BigInt(event.block.number), + }); + + const newMultiRewards = (multiRewards as string).toLowerCase(); + + // Register the new MultiRewards contract for indexing + context.addSFMultiRewards(newMultiRewards); + } catch (error) { + context.log.error(`Failed to get multiRewardsAddress for strategy ${newStrategy}: ${error}`); + } +}); + +/** + * Handle StrategyUpdated events + * Event: StrategyUpdated(address indexed oldStrategy, address indexed newStrategy) + */ +export const handleSFVaultStrategyUpdated = SFVaultERC4626.StrategyUpdated.handler( + async ({ event, context }) => { + const vaultAddress = event.srcAddress.toLowerCase(); + const oldStrategy = event.params.oldStrategy.toLowerCase(); + const newStrategy = event.params.newStrategy.toLowerCase(); + const timestamp = BigInt(event.block.timestamp); + + const config = VAULT_CONFIGS[vaultAddress]; + if (!config) { + context.log.warn(`Unknown vault address: ${vaultAddress}`); + return; + } + + // Query the new strategy's multiRewardsAddress at this block + const newMultiRewards = await context.effect(getMultiRewardsAddress, { + strategyAddress: newStrategy, + blockNumber: BigInt(event.block.number), + }); + + // Mark old strategy as inactive + const oldStrategyId = `${BERACHAIN_ID}_${vaultAddress}_${oldStrategy}`; + const oldStrategyRecord = await context.SFVaultStrategy.get(oldStrategyId); + if (oldStrategyRecord) { + context.SFVaultStrategy.set({ + ...oldStrategyRecord, + activeTo: timestamp, + isActive: false, + }); + } + + // Create new strategy record + const newStrategyId = `${BERACHAIN_ID}_${vaultAddress}_${newStrategy}`; + context.SFVaultStrategy.set({ + id: newStrategyId, + vault: vaultAddress, + strategy: newStrategy, + multiRewards: newMultiRewards, + kitchenToken: config.kitchenToken, + kitchenTokenSymbol: config.kitchenTokenSymbol, + activeFrom: timestamp, + activeTo: undefined, + isActive: true, + chainId: BERACHAIN_ID, + }); + + // Update vault stats with new strategy + const statsId = `${BERACHAIN_ID}_${vaultAddress}`; + const stats = await context.SFVaultStats.get(statsId); + if (stats) { + context.SFVaultStats.set({ + ...stats, + strategy: newStrategy, + lastActivityAt: timestamp, + }); + } + + context.log.info( + `Strategy updated for vault ${vaultAddress}: ${oldStrategy} -> ${newStrategy} (MultiRewards: ${newMultiRewards})` + ); + + // Record action for activity feed + recordAction(context, { + actionType: "sf_strategy_updated", + actor: vaultAddress, + primaryCollection: vaultAddress, + timestamp, + chainId: BERACHAIN_ID, + txHash: event.transaction.hash, + logIndex: event.logIndex, + context: { + vault: vaultAddress, + oldStrategy, + newStrategy, + newMultiRewards, + kitchenTokenSymbol: config.kitchenTokenSymbol, + }, + }); + } +); + +/** + * Handle ERC4626 Deposit events + * Event: Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares) + */ +export const handleSFVaultDeposit = SFVaultERC4626.Deposit.handler( + async ({ event, context }) => { + const vaultAddress = event.srcAddress.toLowerCase(); + const config = VAULT_CONFIGS[vaultAddress]; + + if (!config) { + context.log.warn(`Unknown vault address: ${vaultAddress}`); + return; + } + + const timestamp = BigInt(event.block.timestamp); + const owner = event.params.owner.toLowerCase(); + const assets = event.params.assets; // Kitchen tokens deposited + const shares = event.params.shares; // Vault shares received + + // Ensure initial strategy record exists + await ensureInitialStrategy(context, vaultAddress); + + // Get the current active strategy for this vault + const activeStrategy = await getActiveStrategy(context, vaultAddress); + const strategyAddress = activeStrategy?.strategy || config.strategy; + const multiRewardsAddress = activeStrategy?.multiRewards || config.multiRewards; + + // Create position ID + const positionId = `${BERACHAIN_ID}_${owner}_${vaultAddress}`; + const statsId = `${BERACHAIN_ID}_${vaultAddress}`; + + // Fetch existing position and stats in parallel + const [position, stats] = await Promise.all([ + context.SFPosition.get(positionId), + context.SFVaultStats.get(statsId), + ]); + + // Update or create position + const isNewPosition = !position; + const positionToUpdate: SFPosition = position || { + id: positionId, + user: owner, + vault: vaultAddress, + multiRewards: multiRewardsAddress, + kitchenToken: config.kitchenToken, + strategy: strategyAddress, + kitchenTokenSymbol: config.kitchenTokenSymbol, + vaultShares: BigInt(0), + stakedShares: BigInt(0), + totalShares: BigInt(0), + totalDeposited: BigInt(0), + totalWithdrawn: BigInt(0), + totalClaimed: BigInt(0), + firstDepositAt: timestamp, + lastActivityAt: timestamp, + chainId: BERACHAIN_ID, + }; + + // When depositing, shares go to vault (not staked yet) + const newVaultShares = positionToUpdate.vaultShares + shares; + const newTotalShares = newVaultShares + positionToUpdate.stakedShares; + + const updatedPosition = { + ...positionToUpdate, + vaultShares: newVaultShares, + totalShares: newTotalShares, + totalDeposited: positionToUpdate.totalDeposited + assets, + lastActivityAt: timestamp, + // Update strategy/multiRewards to current active one + strategy: strategyAddress, + multiRewards: multiRewardsAddress, + // Set firstDepositAt on first deposit, or backfill if null + firstDepositAt: positionToUpdate.firstDepositAt || timestamp, + }; + + context.SFPosition.set(updatedPosition); + + // Update or create vault stats + const statsToUpdate: SFVaultStats = stats || { + id: statsId, + vault: vaultAddress, + kitchenToken: config.kitchenToken, + kitchenTokenSymbol: config.kitchenTokenSymbol, + strategy: strategyAddress, + totalDeposited: BigInt(0), + totalWithdrawn: BigInt(0), + totalStaked: BigInt(0), + totalUnstaked: BigInt(0), + totalClaimed: BigInt(0), + uniqueDepositors: 0, + activePositions: 0, + depositCount: 0, + withdrawalCount: 0, + claimCount: 0, + firstDepositAt: timestamp, + lastActivityAt: timestamp, + chainId: BERACHAIN_ID, + }; + + // Check if this deposit creates a new active position + const previousTotalShares = position ? (position.vaultShares + position.stakedShares) : BigInt(0); + const isNewActivePosition = previousTotalShares === BigInt(0) && newTotalShares > BigInt(0); + + const updatedStats = { + ...statsToUpdate, + totalDeposited: statsToUpdate.totalDeposited + assets, + depositCount: statsToUpdate.depositCount + 1, + lastActivityAt: timestamp, + // Increment unique depositors if this is a new position + uniqueDepositors: statsToUpdate.uniqueDepositors + (isNewPosition ? 1 : 0), + // Increment active positions if totalShares went from 0 to non-zero + activePositions: statsToUpdate.activePositions + (isNewActivePosition ? 1 : 0), + }; + + context.SFVaultStats.set(updatedStats); + + // Record action for activity feed + recordAction(context, { + actionType: "sf_vault_deposit", + actor: owner, + primaryCollection: vaultAddress, + timestamp, + chainId: BERACHAIN_ID, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: assets, // Kitchen token amount + numeric2: shares, // Vault shares received + context: { + vault: vaultAddress, + kitchenToken: config.kitchenToken, + kitchenTokenSymbol: config.kitchenTokenSymbol, + sender: event.params.sender.toLowerCase(), + }, + }); + } +); + +/** + * Handle ERC4626 Withdraw events + * Event: Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares) + */ +export const handleSFVaultWithdraw = SFVaultERC4626.Withdraw.handler( + async ({ event, context }) => { + const vaultAddress = event.srcAddress.toLowerCase(); + const config = VAULT_CONFIGS[vaultAddress]; + + if (!config) { + context.log.warn(`Unknown vault address: ${vaultAddress}`); + return; + } + + const timestamp = BigInt(event.block.timestamp); + const owner = event.params.owner.toLowerCase(); + const assets = event.params.assets; // Kitchen tokens withdrawn + const shares = event.params.shares; // Vault shares burned + + // Create position ID + const positionId = `${BERACHAIN_ID}_${owner}_${vaultAddress}`; + const statsId = `${BERACHAIN_ID}_${vaultAddress}`; + + // Fetch existing position and stats in parallel + const [position, stats] = await Promise.all([ + context.SFPosition.get(positionId), + context.SFVaultStats.get(statsId), + ]); + + // Update position if it exists + if (position) { + // When withdrawing, shares are burned from vault balance + let newVaultShares = position.vaultShares - shares; + + // Ensure vaultShares doesn't go negative + if (newVaultShares < BigInt(0)) { + newVaultShares = BigInt(0); + } + + const newTotalShares = newVaultShares + position.stakedShares; + + const updatedPosition = { + ...position, + vaultShares: newVaultShares, + totalShares: newTotalShares, + totalWithdrawn: position.totalWithdrawn + assets, + lastActivityAt: timestamp, + }; + context.SFPosition.set(updatedPosition); + } + + // Update vault stats + if (stats && position) { + // Check if this withdrawal closes the position (totalShares -> 0) + const previousTotalShares = position.totalShares; + const newTotalShares = (position.vaultShares - shares) + position.stakedShares; + const closedPosition = previousTotalShares > BigInt(0) && newTotalShares === BigInt(0); + + const updatedStats = { + ...stats, + totalWithdrawn: stats.totalWithdrawn + assets, + withdrawalCount: stats.withdrawalCount + 1, + // Decrement active positions if totalShares went to 0 + activePositions: stats.activePositions - (closedPosition ? 1 : 0), + lastActivityAt: timestamp, + }; + context.SFVaultStats.set(updatedStats); + } + + // Record action for activity feed + recordAction(context, { + actionType: "sf_vault_withdraw", + actor: owner, + primaryCollection: vaultAddress, + timestamp, + chainId: BERACHAIN_ID, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: assets, // Kitchen token amount + numeric2: shares, // Vault shares burned + context: { + vault: vaultAddress, + kitchenToken: config.kitchenToken, + kitchenTokenSymbol: config.kitchenTokenSymbol, + receiver: event.params.receiver.toLowerCase(), + }, + }); + } +); + +/** + * Handle MultiRewards Staked events + * Event: Staked(address indexed user, uint256 amount) + */ +export const handleSFMultiRewardsStaked = SFMultiRewards.Staked.handler( + async ({ event, context }) => { + const multiRewardsAddress = event.srcAddress.toLowerCase(); + + // Look up vault from MultiRewards address + const vaultInfo = await getVaultFromMultiRewards(context, multiRewardsAddress); + + if (!vaultInfo) { + context.log.warn(`Unknown MultiRewards address: ${multiRewardsAddress}`); + return; + } + + const { vault: vaultAddress, config } = vaultInfo; + const timestamp = BigInt(event.block.timestamp); + const user = event.params.user.toLowerCase(); + const amount = event.params.amount; // Vault shares staked + + // Create position ID + const positionId = `${BERACHAIN_ID}_${user}_${vaultAddress}`; + const statsId = `${BERACHAIN_ID}_${vaultAddress}`; + + // Fetch existing position and stats in parallel + const [position, stats] = await Promise.all([ + context.SFPosition.get(positionId), + context.SFVaultStats.get(statsId), + ]); + + // Update position + if (position) { + const newStakedShares = position.stakedShares + amount; + + // When staking, shares move from vault to staked + let newVaultShares = position.vaultShares - amount; + + // Ensure vaultShares doesn't go negative + if (newVaultShares < BigInt(0)) { + newVaultShares = BigInt(0); + } + + // totalShares remains the same (just moving between buckets) + const newTotalShares = newVaultShares + newStakedShares; + + const updatedPosition = { + ...position, + vaultShares: newVaultShares, + stakedShares: newStakedShares, + totalShares: newTotalShares, + lastActivityAt: timestamp, + }; + context.SFPosition.set(updatedPosition); + + // Update stats + if (stats) { + const updatedStats = { + ...stats, + totalStaked: stats.totalStaked + amount, + lastActivityAt: timestamp, + }; + context.SFVaultStats.set(updatedStats); + } + } + + // Track per-MultiRewards position + const multiRewardsPositionId = `${BERACHAIN_ID}_${user}_${multiRewardsAddress}`; + const multiRewardsPosition = await context.SFMultiRewardsPosition.get(multiRewardsPositionId); + + const updatedMultiRewardsPosition = multiRewardsPosition ? { + ...multiRewardsPosition, + stakedShares: multiRewardsPosition.stakedShares + amount, + totalStaked: multiRewardsPosition.totalStaked + amount, + lastActivityAt: timestamp, + } : { + id: multiRewardsPositionId, + user, + vault: vaultAddress, + multiRewards: multiRewardsAddress, + stakedShares: amount, + totalStaked: amount, + totalUnstaked: BigInt(0), + totalClaimed: BigInt(0), + firstStakeAt: timestamp, + lastActivityAt: timestamp, + chainId: BERACHAIN_ID, + }; + + context.SFMultiRewardsPosition.set(updatedMultiRewardsPosition); + + // Record action for activity feed + recordAction(context, { + actionType: "sf_rewards_stake", + actor: user, + primaryCollection: vaultAddress, + timestamp, + chainId: BERACHAIN_ID, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: amount, // Shares staked + context: { + vault: vaultAddress, + multiRewards: multiRewardsAddress, + kitchenTokenSymbol: config.kitchenTokenSymbol, + }, + }); + } +); + +/** + * Handle MultiRewards Withdrawn events + * Event: Withdrawn(address indexed user, uint256 amount) + */ +export const handleSFMultiRewardsWithdrawn = SFMultiRewards.Withdrawn.handler( + async ({ event, context }) => { + const multiRewardsAddress = event.srcAddress.toLowerCase(); + + // Look up vault from MultiRewards address + const vaultInfo = await getVaultFromMultiRewards(context, multiRewardsAddress); + + if (!vaultInfo) { + context.log.warn(`Unknown MultiRewards address: ${multiRewardsAddress}`); + return; + } + + const { vault: vaultAddress, config } = vaultInfo; + const timestamp = BigInt(event.block.timestamp); + const user = event.params.user.toLowerCase(); + const amount = event.params.amount; // Vault shares unstaked + + // Create position ID + const positionId = `${BERACHAIN_ID}_${user}_${vaultAddress}`; + const statsId = `${BERACHAIN_ID}_${vaultAddress}`; + + // Fetch existing position and stats in parallel + const [position, stats] = await Promise.all([ + context.SFPosition.get(positionId), + context.SFVaultStats.get(statsId), + ]); + + // Update position + if (position) { + let newStakedShares = position.stakedShares - amount; + + // Ensure stakedShares doesn't go negative + if (newStakedShares < BigInt(0)) { + newStakedShares = BigInt(0); + } + + // When unstaking, shares move from staked to vault + const newVaultShares = position.vaultShares + amount; + + // totalShares remains the same (just moving between buckets) + const newTotalShares = newVaultShares + newStakedShares; + + const updatedPosition = { + ...position, + vaultShares: newVaultShares, + stakedShares: newStakedShares, + totalShares: newTotalShares, + lastActivityAt: timestamp, + }; + context.SFPosition.set(updatedPosition); + + // Update stats + if (stats) { + const updatedStats = { + ...stats, + totalUnstaked: stats.totalUnstaked + amount, + lastActivityAt: timestamp, + }; + context.SFVaultStats.set(updatedStats); + } + } + + // Track per-MultiRewards position + const multiRewardsPositionId = `${BERACHAIN_ID}_${user}_${multiRewardsAddress}`; + const multiRewardsPosition = await context.SFMultiRewardsPosition.get(multiRewardsPositionId); + + if (multiRewardsPosition) { + let newStakedShares = multiRewardsPosition.stakedShares - amount; + if (newStakedShares < BigInt(0)) { + newStakedShares = BigInt(0); + } + + const updatedMultiRewardsPosition = { + ...multiRewardsPosition, + stakedShares: newStakedShares, + totalUnstaked: multiRewardsPosition.totalUnstaked + amount, + lastActivityAt: timestamp, + }; + context.SFMultiRewardsPosition.set(updatedMultiRewardsPosition); + } + + // Record action for activity feed + recordAction(context, { + actionType: "sf_rewards_unstake", + actor: user, + primaryCollection: vaultAddress, + timestamp, + chainId: BERACHAIN_ID, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: amount, // Shares unstaked + context: { + vault: vaultAddress, + multiRewards: multiRewardsAddress, + kitchenTokenSymbol: config.kitchenTokenSymbol, + }, + }); + } +); + +/** + * Handle MultiRewards RewardPaid events + * Event: RewardPaid(address indexed user, address indexed rewardsToken, uint256 reward) + */ +export const handleSFMultiRewardsRewardPaid = SFMultiRewards.RewardPaid.handler( + async ({ event, context }) => { + const multiRewardsAddress = event.srcAddress.toLowerCase(); + + // Look up vault from MultiRewards address + const vaultInfo = await getVaultFromMultiRewards(context, multiRewardsAddress); + + if (!vaultInfo) { + context.log.warn(`Unknown MultiRewards address: ${multiRewardsAddress}`); + return; + } + + const { vault: vaultAddress, config } = vaultInfo; + const timestamp = BigInt(event.block.timestamp); + const user = event.params.user.toLowerCase(); + const rewardsToken = event.params.rewardsToken.toLowerCase(); + const reward = event.params.reward; // HENLO amount claimed + + // Create position ID + const positionId = `${BERACHAIN_ID}_${user}_${vaultAddress}`; + const statsId = `${BERACHAIN_ID}_${vaultAddress}`; + + // Fetch existing position and stats in parallel + const [position, stats] = await Promise.all([ + context.SFPosition.get(positionId), + context.SFVaultStats.get(statsId), + ]); + + // Update position's total claimed + if (position) { + const updatedPosition = { + ...position, + totalClaimed: position.totalClaimed + reward, + lastActivityAt: timestamp, + }; + context.SFPosition.set(updatedPosition); + } + + // Update vault stats total claimed (income metric!) + if (stats) { + const updatedStats = { + ...stats, + totalClaimed: stats.totalClaimed + reward, + claimCount: stats.claimCount + 1, + lastActivityAt: timestamp, + }; + context.SFVaultStats.set(updatedStats); + } + + // Track per-MultiRewards position claims + const multiRewardsPositionId = `${BERACHAIN_ID}_${user}_${multiRewardsAddress}`; + const multiRewardsPosition = await context.SFMultiRewardsPosition.get(multiRewardsPositionId); + + if (multiRewardsPosition) { + const updatedMultiRewardsPosition = { + ...multiRewardsPosition, + totalClaimed: multiRewardsPosition.totalClaimed + reward, + lastActivityAt: timestamp, + }; + context.SFMultiRewardsPosition.set(updatedMultiRewardsPosition); + } + + // Record action for activity feed + recordAction(context, { + actionType: "sf_rewards_claim", + actor: user, + primaryCollection: vaultAddress, + timestamp, + chainId: BERACHAIN_ID, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: reward, // HENLO claimed + context: { + vault: vaultAddress, + multiRewards: multiRewardsAddress, + rewardsToken, + kitchenTokenSymbol: config.kitchenTokenSymbol, + }, + }); + } +); diff --git a/src/handlers/tracked-erc20.ts b/src/handlers/tracked-erc20.ts new file mode 100644 index 0000000..c64855b --- /dev/null +++ b/src/handlers/tracked-erc20.ts @@ -0,0 +1,142 @@ +/* + * Unified ERC-20 Token Handler + * Tracks token balances for HENLO and HENLOCKED tier tokens + * Also handles burn tracking and holder stats for HENLO token + */ + +import { TrackedTokenBalance, TrackedErc20 } from "generated"; +import { TOKEN_CONFIGS } from "./tracked-erc20/token-config"; +import { isBurnTransfer, trackBurn, ZERO_ADDRESS } from "./tracked-erc20/burn-tracking"; +import { updateHolderBalances, updateHolderStats } from "./tracked-erc20/holder-stats"; + +/** + * Handles ERC-20 Transfer events for tracked tokens + * Routes to appropriate feature handlers based on token config + */ +export const handleTrackedErc20Transfer = TrackedErc20.Transfer.handler( + async ({ event, context }) => { + const { from, to, value } = event.params; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const tokenAddress = event.srcAddress.toLowerCase(); + + // Get token config from address + const config = TOKEN_CONFIGS[tokenAddress]; + if (!config) { + // Token not in our tracked list, skip + return; + } + + // Normalize addresses + const fromLower = from.toLowerCase(); + const toLower = to.toLowerCase(); + const zeroAddress = ZERO_ADDRESS.toLowerCase(); + + // 1. Balance tracking (ALL tokens) + await updateBalance( + context, + tokenAddress, + config.key, + chainId, + fromLower, + toLower, + value, + timestamp, + zeroAddress + ); + + // 2. Holder stats (if enabled - HENLO only) + if (config.holderStats) { + try { + const { holderDelta, supplyDelta } = await updateHolderBalances(event, context, config); + + // Update holder statistics if there were changes + if (holderDelta !== 0 || supplyDelta !== BigInt(0)) { + await updateHolderStats(context, chainId, holderDelta, supplyDelta, timestamp); + } + } catch (error) { + console.error('[TrackedErc20] Holder stats error:', tokenAddress, error); + } + } + + // 3. Burn tracking (if enabled + is burn) + if (config.burnTracking && isBurnTransfer(toLower)) { + try { + await trackBurn(event, context, config, fromLower, toLower); + } catch (error) { + console.error('[TrackedErc20] Burn tracking error:', tokenAddress, error); + } + } + } +); + +/** + * Updates TrackedTokenBalance records for sender and receiver + */ +async function updateBalance( + context: any, + tokenAddress: string, + tokenKey: string, + chainId: number, + fromLower: string, + toLower: string, + value: bigint, + timestamp: bigint, + zeroAddress: string +) { + // Build IDs + const fromId = fromLower !== zeroAddress ? `${fromLower}_${tokenAddress}_${chainId}` : null; + const toId = toLower !== zeroAddress ? `${toLower}_${tokenAddress}_${chainId}` : null; + + // Pre-load balances in parallel for better performance + const [fromBalance, toBalance] = await Promise.all([ + fromId ? context.TrackedTokenBalance.get(fromId) : null, + toId ? context.TrackedTokenBalance.get(toId) : null, + ]); + + // Handle sender (decrease balance) - skip if mint (from zero address) + if (fromId) { + if (fromBalance) { + // Floor at 0 to prevent negative balances (can happen if indexer started after token distribution) + const newBalance = fromBalance.balance - value; + const flooredBalance = newBalance < BigInt(0) ? BigInt(0) : newBalance; + const updatedFromBalance: TrackedTokenBalance = { + ...fromBalance, + balance: flooredBalance, + lastUpdated: timestamp, + }; + context.TrackedTokenBalance.set(updatedFromBalance); + } else { + // No existing record - user received tokens before indexer started + // Skip creating a record with negative/zero balance to avoid polluting data + // The correct fix is to re-index from the token distribution block + console.warn(`[TrackedErc20] Transfer OUT with no prior balance record: ${fromLower} token=${tokenKey}`); + } + } + + // Handle receiver (increase balance) - skip if burn (to zero address) + // Note: We still track burns in TrackedTokenBalance for completeness + if (toId) { + if (toBalance) { + const newBalance = toBalance.balance + value; + const updatedToBalance: TrackedTokenBalance = { + ...toBalance, + balance: newBalance, + lastUpdated: timestamp, + }; + context.TrackedTokenBalance.set(updatedToBalance); + } else { + // Create new record for first-time holder + const newToBalance: TrackedTokenBalance = { + id: toId, + address: toLower, + tokenAddress, + tokenKey, + chainId, + balance: value, + lastUpdated: timestamp, + }; + context.TrackedTokenBalance.set(newToBalance); + } + } +} diff --git a/src/handlers/tracked-erc20/burn-tracking.ts b/src/handlers/tracked-erc20/burn-tracking.ts new file mode 100644 index 0000000..3116fea --- /dev/null +++ b/src/handlers/tracked-erc20/burn-tracking.ts @@ -0,0 +1,334 @@ +/* + * Burn Tracking Module + * Handles HENLO burn record creation and statistics updates + */ + +import { + HenloBurn, + HenloBurnStats, + HenloGlobalBurnStats, +} from "generated"; + +import { recordAction } from "../../lib/actions"; +import { TokenConfig } from "./token-config"; + +export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +export const DEAD_ADDRESS = "0x000000000000000000000000000000000000dead"; +const BERACHAIN_MAINNET_ID = 80094; + +type ExtendedHenloBurnStats = HenloBurnStats & { uniqueBurners?: number }; +type ExtendedHenloGlobalBurnStats = HenloGlobalBurnStats & { + incineratorUniqueBurners?: number; +}; + +/** + * Checks if a transfer is a burn (to zero or dead address) + */ +export function isBurnTransfer(to: string): boolean { + const toLower = to.toLowerCase(); + return ( + toLower === ZERO_ADDRESS.toLowerCase() || + toLower === DEAD_ADDRESS.toLowerCase() + ); +} + +/** + * Tracks a burn event and updates all statistics + */ +export async function trackBurn( + event: any, + context: any, + config: TokenConfig, + fromLower: string, + toLower: string +) { + const { value } = event.params; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + const transactionFromLower = event.transaction.from?.toLowerCase(); + const transactionToLower = event.transaction.to?.toLowerCase(); + const burnSources = config.burnSources || {}; + + // Determine burn source by checking both token holder and calling contract + const sourceMatchAddress = + (fromLower && burnSources[fromLower] ? fromLower : undefined) ?? + (transactionToLower && burnSources[transactionToLower] + ? transactionToLower + : undefined); + const source = sourceMatchAddress + ? burnSources[sourceMatchAddress] + : "user"; + + // Identify the unique wallet that initiated the burn + const burnerAddress = + source !== "user" + ? transactionFromLower ?? fromLower + : fromLower; + const burnerId = burnerAddress; + + // Create burn record + const burnId = `${event.transaction.hash}_${event.logIndex}`; + const burn: HenloBurn = { + id: burnId, + amount: value, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + from: burnerAddress, + source, + chainId, + }; + + context.HenloBurn.set(burn); + + recordAction(context, { + id: burnId, + actionType: "burn", + actor: burnerAddress ?? fromLower, + primaryCollection: "henlo_incinerator", + timestamp, + chainId, + txHash: event.transaction.hash, + logIndex: event.logIndex, + numeric1: value, + context: { + from: fromLower, + transactionFrom: transactionFromLower, + transactionTo: transactionToLower, + source, + rawTo: toLower, + token: event.srcAddress.toLowerCase(), + }, + }); + + // Track unique burners at global, chain, and source scope + const extendedContext = context as any; + const chainBurnerId = `${chainId}_${burnerId}`; + const sourceBurnerId = `${chainId}_${source}_${burnerId}`; + + const [existingBurner, existingChainBurner, existingSourceBurner] = await Promise.all([ + context.HenloBurner.get(burnerId), + extendedContext?.HenloChainBurner?.get(chainBurnerId), + extendedContext?.HenloSourceBurner?.get(sourceBurnerId), + ]); + + const isNewGlobalBurner = !existingBurner; + if (isNewGlobalBurner) { + const burner = { + id: burnerId, + address: burnerAddress, + firstBurnTime: timestamp, + chainId, + }; + context.HenloBurner.set(burner); + } + + const chainBurnerStore = extendedContext?.HenloChainBurner; + const isNewChainBurner = !existingChainBurner; + if (isNewChainBurner && chainBurnerStore) { + const chainBurner = { + id: chainBurnerId, + chainId, + address: burnerAddress, + firstBurnTime: timestamp, + }; + chainBurnerStore.set(chainBurner); + } + + const sourceBurnerStore = extendedContext?.HenloSourceBurner; + const isNewSourceBurner = !existingSourceBurner; + if (isNewSourceBurner && sourceBurnerStore) { + const sourceBurner = { + id: sourceBurnerId, + chainId, + source, + address: burnerAddress, + firstBurnTime: timestamp, + }; + sourceBurnerStore.set(sourceBurner); + } + + if (isNewGlobalBurner || (isNewSourceBurner && source === "incinerator")) { + let globalStats = (await context.HenloGlobalBurnStats.get( + "global" + )) as ExtendedHenloGlobalBurnStats | undefined; + if (!globalStats) { + globalStats = { + id: "global", + totalBurnedAllChains: BigInt(0), + totalBurnedMainnet: BigInt(0), + totalBurnedTestnet: BigInt(0), + burnCountAllChains: 0, + incineratorBurns: BigInt(0), + overunderBurns: BigInt(0), + beratrackrBurns: BigInt(0), + userBurns: BigInt(0), + uniqueBurners: 0, + incineratorUniqueBurners: 0, + lastUpdateTime: timestamp, + } as ExtendedHenloGlobalBurnStats; + } + + const updatedGlobalUniqueStats: ExtendedHenloGlobalBurnStats = { + ...globalStats, + uniqueBurners: + (globalStats.uniqueBurners ?? 0) + (isNewGlobalBurner ? 1 : 0), + incineratorUniqueBurners: + (globalStats.incineratorUniqueBurners ?? 0) + + (source === "incinerator" && isNewSourceBurner ? 1 : 0), + lastUpdateTime: timestamp, + }; + context.HenloGlobalBurnStats.set( + updatedGlobalUniqueStats as HenloGlobalBurnStats + ); + } + + // Update chain-specific burn stats with unique burner increments + const sourceUniqueIncrement = isNewSourceBurner ? 1 : 0; + const totalUniqueIncrement = isNewChainBurner ? 1 : 0; + await updateChainBurnStats( + context, + chainId, + source, + value, + timestamp, + sourceUniqueIncrement, + totalUniqueIncrement + ); + + // Update global burn stats + await updateGlobalBurnStats(context, chainId, source, value, timestamp); +} + +/** + * Updates burn statistics for a specific chain and source + */ +async function updateChainBurnStats( + context: any, + chainId: number, + source: string, + amount: bigint, + timestamp: bigint, + sourceUniqueIncrement: number, + totalUniqueIncrement: number +) { + const statsId = `${chainId}_${source}`; + const totalStatsId = `${chainId}_total`; + + const [stats, totalStats] = await Promise.all([ + context.HenloBurnStats.get(statsId) as Promise, + context.HenloBurnStats.get(totalStatsId) as Promise, + ]); + + // Create or update source-specific stats + const statsToUpdate = stats || { + id: statsId, + chainId, + source, + totalBurned: BigInt(0), + burnCount: 0, + uniqueBurners: 0, + lastBurnTime: timestamp, + firstBurnTime: timestamp, + } as ExtendedHenloBurnStats; + + const updatedStats: ExtendedHenloBurnStats = { + ...statsToUpdate, + totalBurned: statsToUpdate.totalBurned + amount, + burnCount: statsToUpdate.burnCount + 1, + uniqueBurners: (statsToUpdate.uniqueBurners ?? 0) + sourceUniqueIncrement, + lastBurnTime: timestamp, + }; + + // Create or update total stats + const totalStatsToUpdate = totalStats || { + id: totalStatsId, + chainId, + source: "total", + totalBurned: BigInt(0), + burnCount: 0, + uniqueBurners: 0, + lastBurnTime: timestamp, + firstBurnTime: timestamp, + } as ExtendedHenloBurnStats; + + const updatedTotalStats: ExtendedHenloBurnStats = { + ...totalStatsToUpdate, + totalBurned: totalStatsToUpdate.totalBurned + amount, + burnCount: totalStatsToUpdate.burnCount + 1, + uniqueBurners: (totalStatsToUpdate.uniqueBurners ?? 0) + totalUniqueIncrement, + lastBurnTime: timestamp, + }; + + // Set both stats + context.HenloBurnStats.set(updatedStats as HenloBurnStats); + context.HenloBurnStats.set(updatedTotalStats as HenloBurnStats); +} + +/** + * Updates global burn statistics across all chains + */ +async function updateGlobalBurnStats( + context: any, + chainId: number, + source: string, + amount: bigint, + timestamp: bigint +) { + let globalStats = (await context.HenloGlobalBurnStats.get( + "global" + )) as ExtendedHenloGlobalBurnStats | undefined; + + if (!globalStats) { + globalStats = { + id: "global", + totalBurnedAllChains: BigInt(0), + totalBurnedMainnet: BigInt(0), + totalBurnedTestnet: BigInt(0), + burnCountAllChains: 0, + incineratorBurns: BigInt(0), + overunderBurns: BigInt(0), + beratrackrBurns: BigInt(0), + userBurns: BigInt(0), + uniqueBurners: 0, + incineratorUniqueBurners: 0, + lastUpdateTime: timestamp, + } as ExtendedHenloGlobalBurnStats; + } + + // Create updated global stats object (immutable update) + const updatedGlobalStats: ExtendedHenloGlobalBurnStats = { + ...globalStats, + totalBurnedAllChains: globalStats.totalBurnedAllChains + amount, + totalBurnedMainnet: + chainId === BERACHAIN_MAINNET_ID + ? globalStats.totalBurnedMainnet + amount + : globalStats.totalBurnedMainnet, + totalBurnedTestnet: + chainId !== BERACHAIN_MAINNET_ID + ? globalStats.totalBurnedTestnet + amount + : globalStats.totalBurnedTestnet, + incineratorBurns: + source === "incinerator" + ? globalStats.incineratorBurns + amount + : globalStats.incineratorBurns, + overunderBurns: + source === "overunder" + ? globalStats.overunderBurns + amount + : globalStats.overunderBurns, + beratrackrBurns: + source === "beratrackr" + ? globalStats.beratrackrBurns + amount + : globalStats.beratrackrBurns, + userBurns: + source !== "incinerator" && source !== "overunder" && source !== "beratrackr" + ? globalStats.userBurns + amount + : globalStats.userBurns, + uniqueBurners: globalStats.uniqueBurners ?? 0, + incineratorUniqueBurners: globalStats.incineratorUniqueBurners ?? 0, + burnCountAllChains: globalStats.burnCountAllChains + 1, + lastUpdateTime: timestamp, + }; + + context.HenloGlobalBurnStats.set(updatedGlobalStats as HenloGlobalBurnStats); +} diff --git a/src/handlers/tracked-erc20/holder-stats.ts b/src/handlers/tracked-erc20/holder-stats.ts new file mode 100644 index 0000000..7578ac5 --- /dev/null +++ b/src/handlers/tracked-erc20/holder-stats.ts @@ -0,0 +1,148 @@ +/* + * Holder Stats Module + * Handles HENLO holder tracking and statistics updates + */ + +import { HenloHolder, HenloHolderStats } from "generated"; +import { TokenConfig } from "./token-config"; +import { ZERO_ADDRESS, DEAD_ADDRESS } from "./burn-tracking"; + +/** + * Updates holder balances and statistics for a token transfer + * Returns true if this is a burn transfer (to zero/dead address) + */ +export async function updateHolderBalances( + event: any, + context: any, + config: TokenConfig +): Promise<{ holderDelta: number; supplyDelta: bigint }> { + const { from, to, value } = event.params; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + + // Normalize addresses + const fromLower = from.toLowerCase(); + const toLower = to.toLowerCase(); + const zeroAddress = ZERO_ADDRESS.toLowerCase(); + const deadAddress = DEAD_ADDRESS.toLowerCase(); + + // Track changes in holder counts and supply + let holderDelta = 0; + let supplyDelta = BigInt(0); + + // Pre-load holders in parallel for better performance + const [fromHolder, toHolder] = await Promise.all([ + fromLower !== zeroAddress ? getOrCreateHolder(context, fromLower, chainId, timestamp) : null, + (toLower !== zeroAddress && toLower !== deadAddress) ? getOrCreateHolder(context, toLower, chainId, timestamp) : null, + ]); + + // Handle 'from' address (decrease balance) + if (fromLower !== zeroAddress && fromHolder) { + // Floor at 0 to prevent negative balances + const newFromBalance = fromHolder.balance - value; + const flooredBalance = newFromBalance < BigInt(0) ? BigInt(0) : newFromBalance; + + // Update holder record + const updatedFromHolder = { + ...fromHolder, + balance: flooredBalance, + lastActivityTime: timestamp, + }; + context.HenloHolder.set(updatedFromHolder); + + // If balance went to zero, decrease holder count + if (fromHolder.balance > BigInt(0) && flooredBalance === BigInt(0)) { + holderDelta--; + } + + // Supply decreases when tokens are burned + if (toLower === zeroAddress || toLower === deadAddress) { + supplyDelta -= value; + } + } else if (fromLower === zeroAddress) { + // Mint: supply increases + supplyDelta += value; + } + + // Handle 'to' address (increase balance) + if (toLower !== zeroAddress && toLower !== deadAddress && toHolder) { + const newToBalance = toHolder.balance + value; + + // Update holder record + const updatedToHolder = { + ...toHolder, + balance: newToBalance, + lastActivityTime: timestamp, + // Set firstTransferTime if this is their first time receiving tokens + firstTransferTime: toHolder.firstTransferTime || timestamp, + }; + context.HenloHolder.set(updatedToHolder); + + // If balance went from zero to positive, increase holder count + if (toHolder.balance === BigInt(0) && newToBalance > BigInt(0)) { + holderDelta++; + } + } + + return { holderDelta, supplyDelta }; +} + +/** + * Updates holder statistics for the chain + */ +export async function updateHolderStats( + context: any, + chainId: number, + holderDelta: number, + supplyDelta: bigint, + timestamp: bigint +) { + const statsId = chainId.toString(); + let stats = await context.HenloHolderStats.get(statsId); + + if (!stats) { + stats = { + id: statsId, + chainId, + uniqueHolders: 0, + totalSupply: BigInt(0), + lastUpdateTime: timestamp, + }; + } + + // Create updated stats object (immutable update) + const updatedStats = { + ...stats, + uniqueHolders: Math.max(0, stats.uniqueHolders + holderDelta), + totalSupply: stats.totalSupply + supplyDelta, + lastUpdateTime: timestamp, + }; + + context.HenloHolderStats.set(updatedStats); +} + +/** + * Gets an existing holder or creates a new one with zero balance + */ +async function getOrCreateHolder( + context: any, + address: string, + chainId: number, + timestamp: bigint +): Promise { + const holderId = address; // Use address as ID + let holder = await context.HenloHolder.get(holderId); + + if (!holder) { + holder = { + id: holderId, + address: address, + balance: BigInt(0), + firstTransferTime: undefined, + lastActivityTime: timestamp, + chainId, + }; + } + + return holder; +} diff --git a/src/handlers/tracked-erc20/token-config.ts b/src/handlers/tracked-erc20/token-config.ts new file mode 100644 index 0000000..2683479 --- /dev/null +++ b/src/handlers/tracked-erc20/token-config.ts @@ -0,0 +1,54 @@ +/* + * Per-Token Feature Configuration + * Enables feature flags for burn tracking, holder stats, etc. per token + */ + +export interface TokenConfig { + key: string; + burnTracking: boolean; + holderStats: boolean; + burnSources?: Record; // contract address -> source name +} + +// Henlo burn source addresses (Berachain mainnet) +export const HENLO_BURN_SOURCES: Record = { + "0xde81b20b6801d99efeaeced48a11ba025180b8cc": "incinerator", + // TODO: Add actual OverUnder contract address when available + // TODO: Add actual BeraTrackr contract address when available +}; + +export const TOKEN_CONFIGS: Record = { + // HENLO token - full tracking (burns + holder stats) + "0xb2f776e9c1c926c4b2e54182fac058da9af0b6a5": { + key: "henlo", + burnTracking: true, + holderStats: true, + burnSources: HENLO_BURN_SOURCES, + }, + // HENLOCKED tier tokens - balance tracking only + "0xf0edfc3e122db34773293e0e5b2c3a58492e7338": { + key: "hlkd1b", + burnTracking: false, + holderStats: false, + }, + "0x8ab854dc0672d7a13a85399a56cb628fb22102d6": { + key: "hlkd690m", + burnTracking: false, + holderStats: false, + }, + "0xf07fa3ece9741d408d643748ff85710bedef25ba": { + key: "hlkd420m", + burnTracking: false, + holderStats: false, + }, + "0x37dd8850919ebdca911c383211a70839a94b0539": { + key: "hlkd330m", + burnTracking: false, + holderStats: false, + }, + "0x7bdf98ddeed209cfa26bd2352b470ac8b5485ec5": { + key: "hlkd100m", + burnTracking: false, + holderStats: false, + }, +}; diff --git a/src/handlers/tracked-erc721.ts b/src/handlers/tracked-erc721.ts new file mode 100644 index 0000000..a3d84b6 --- /dev/null +++ b/src/handlers/tracked-erc721.ts @@ -0,0 +1,366 @@ +import { TrackedErc721 } from "generated"; +import type { + handlerContext, + TrackedHolder as TrackedHolderEntity, + MiberaStakedToken as MiberaStakedTokenEntity, + MiberaStaker as MiberaStakerEntity, +} from "generated"; + +import { ZERO_ADDRESS } from "./constants"; +import { + TRACKED_ERC721_COLLECTION_KEYS, + TRANSFER_TRACKED_COLLECTIONS, +} from "./tracked-erc721/constants"; +import { STAKING_CONTRACT_KEYS } from "./mibera-staking/constants"; +import { isMarketplaceAddress } from "./marketplaces/constants"; +import { recordAction } from "../lib/actions"; +import { isBurnAddress, isMintFromZero } from "../lib/mint-detection"; + +const ZERO = ZERO_ADDRESS.toLowerCase(); + +// Mibera NFT contract address (lowercase) +const MIBERA_CONTRACT = "0x6666397dfe9a8c469bf65dc744cb1c733416c420"; + +export const handleTrackedErc721Transfer = TrackedErc721.Transfer.handler( + async ({ event, context }) => { + const contractAddress = event.srcAddress.toLowerCase(); + const collectionKey = + TRACKED_ERC721_COLLECTION_KEYS[contractAddress] ?? contractAddress; + const from = event.params.from.toLowerCase(); + const to = event.params.to.toLowerCase(); + const tokenId = event.params.tokenId; + const chainId = event.chainId; + const txHash = event.transaction.hash; + const logIndex = Number(event.logIndex); + const timestamp = BigInt(event.block.timestamp); + const blockNumber = BigInt(event.block.number); + + // If this is a mint (from zero address), also create a mint action + if (from === ZERO) { + const mintActionId = `${txHash}_${logIndex}`; + recordAction(context, { + id: mintActionId, + actionType: "mint", + actor: to, + primaryCollection: collectionKey.toLowerCase(), + timestamp, + chainId, + txHash, + logIndex, + numeric1: 1n, + context: { + tokenId: tokenId.toString(), + contract: contractAddress, + }, + }); + } + + // If this is a burn (to zero or dead address), create a burn action + if (isBurnAddress(to) && from !== ZERO) { + const burnActionId = `${txHash}_${logIndex}_burn`; + recordAction(context, { + id: burnActionId, + actionType: "burn", + actor: from, + primaryCollection: collectionKey.toLowerCase(), + timestamp, + chainId, + txHash, + logIndex, + numeric1: 1n, + context: { + tokenId: tokenId.toString(), + contract: contractAddress, + burnAddress: to, + }, + }); + } + + // Track transfers for specific collections (non-mint, non-burn transfers) + if ( + TRANSFER_TRACKED_COLLECTIONS.has(collectionKey) && + from !== ZERO && + !isBurnAddress(to) + ) { + const transferActionId = `${txHash}_${logIndex}_transfer`; + recordAction(context, { + id: transferActionId, + actionType: "transfer", + actor: to, // Recipient is the actor (they received the NFT) + primaryCollection: collectionKey.toLowerCase(), + timestamp, + chainId, + txHash, + logIndex, + numeric1: BigInt(tokenId.toString()), + context: { + tokenId: tokenId.toString(), + contract: contractAddress, + from, + to, + isSecondary: true, + viaMarketplace: isMarketplaceAddress(from) || isMarketplaceAddress(to), + }, + }); + } + + // Check for Mibera staking transfers + const isMibera = contractAddress === MIBERA_CONTRACT; + const depositContractKey = STAKING_CONTRACT_KEYS[to]; + const withdrawContractKey = STAKING_CONTRACT_KEYS[from]; + + // Handle Mibera staking deposit (user → staking contract) + if (isMibera && depositContractKey && from !== ZERO) { + await handleMiberaStakeDeposit({ + context, + stakingContract: depositContractKey, + stakingContractAddress: to, + userAddress: from, + tokenId, + chainId, + txHash, + blockNumber, + timestamp, + }); + // Don't adjust holder counts - user still owns the NFT (it's staked) + return; + } + + // Handle Mibera staking withdrawal (staking contract → user) + if (isMibera && withdrawContractKey && to !== ZERO) { + await handleMiberaStakeWithdrawal({ + context, + stakingContract: withdrawContractKey, + stakingContractAddress: from, + userAddress: to, + tokenId, + chainId, + txHash, + blockNumber, + timestamp, + }); + // Don't adjust holder counts - they were never decremented on deposit + return; + } + + // Normal transfer handling - run in parallel for better performance + await Promise.all([ + adjustHolder({ + context, + contractAddress, + collectionKey, + chainId, + holderAddress: from, + delta: -1, + txHash, + logIndex, + timestamp, + direction: "out", + }), + adjustHolder({ + context, + contractAddress, + collectionKey, + chainId, + holderAddress: to, + delta: 1, + txHash, + logIndex, + timestamp, + direction: "in", + }), + ]); + } +); + +interface AdjustHolderArgs { + context: handlerContext; + contractAddress: string; + collectionKey: string; + chainId: number; + holderAddress: string; + delta: number; + txHash: string; + logIndex: number; + timestamp: bigint; + direction: "in" | "out"; +} + +async function adjustHolder({ + context, + contractAddress, + collectionKey, + chainId, + holderAddress, + delta, + txHash, + logIndex, + timestamp, + direction, +}: AdjustHolderArgs) { + if (delta === 0) { + return; + } + + const address = holderAddress.toLowerCase(); + if (address === ZERO) { + return; + } + + const id = `${contractAddress}_${chainId}_${address}`; + const existing = await context.TrackedHolder.get(id); + const currentCount = existing?.tokenCount ?? 0; + const nextCount = currentCount + delta; + + const actionId = `${txHash}_${logIndex}_${direction}`; + const normalizedCollection = collectionKey.toLowerCase(); + const tokenCount = Math.max(0, nextCount); + + recordAction(context, { + id: actionId, + actionType: "hold721", + actor: address, + primaryCollection: normalizedCollection, + timestamp, + chainId, + txHash, + logIndex, + numeric1: BigInt(tokenCount), + context: { + contract: contractAddress, + collectionKey: normalizedCollection, + tokenCount, + direction, + }, + }); + + if (nextCount <= 0) { + if (existing) { + context.TrackedHolder.deleteUnsafe(id); + } + return; + } + + const holder: TrackedHolderEntity = { + id, + contract: contractAddress, + collectionKey, + chainId, + address, + tokenCount: nextCount, + }; + + context.TrackedHolder.set(holder); +} + +// Mibera staking helper types and functions + +interface MiberaStakeArgs { + context: handlerContext; + stakingContract: string; + stakingContractAddress: string; + userAddress: string; + tokenId: bigint; + chainId: number; + txHash: string; + blockNumber: bigint; + timestamp: bigint; +} + +async function handleMiberaStakeDeposit({ + context, + stakingContract, + stakingContractAddress, + userAddress, + tokenId, + chainId, + txHash, + blockNumber, + timestamp, +}: MiberaStakeArgs) { + // Create staked token record + const stakedTokenId = `${stakingContract}_${tokenId}`; + const stakedToken: MiberaStakedTokenEntity = { + id: stakedTokenId, + stakingContract, + contractAddress: stakingContractAddress, + tokenId, + owner: userAddress, + isStaked: true, + depositedAt: timestamp, + depositTxHash: txHash, + depositBlockNumber: blockNumber, + withdrawnAt: undefined, + withdrawTxHash: undefined, + withdrawBlockNumber: undefined, + chainId, + }; + context.MiberaStakedToken.set(stakedToken); + + // Update staker stats + const stakerId = `${stakingContract}_${userAddress}`; + const existingStaker = await context.MiberaStaker.get(stakerId); + + const staker: MiberaStakerEntity = existingStaker + ? { + ...existingStaker, + currentStakedCount: existingStaker.currentStakedCount + 1, + totalDeposits: existingStaker.totalDeposits + 1, + lastActivityTime: timestamp, + } + : { + id: stakerId, + stakingContract, + contractAddress: stakingContractAddress, + address: userAddress, + currentStakedCount: 1, + totalDeposits: 1, + totalWithdrawals: 0, + firstDepositTime: timestamp, + lastActivityTime: timestamp, + chainId, + }; + + context.MiberaStaker.set(staker); +} + +async function handleMiberaStakeWithdrawal({ + context, + stakingContract, + stakingContractAddress, + userAddress, + tokenId, + chainId, + txHash, + blockNumber, + timestamp, +}: MiberaStakeArgs) { + // Update staked token record + const stakedTokenId = `${stakingContract}_${tokenId}`; + const existingStakedToken = await context.MiberaStakedToken.get(stakedTokenId); + + if (existingStakedToken) { + const updatedStakedToken: MiberaStakedTokenEntity = { + ...existingStakedToken, + isStaked: false, + withdrawnAt: timestamp, + withdrawTxHash: txHash, + withdrawBlockNumber: blockNumber, + }; + context.MiberaStakedToken.set(updatedStakedToken); + } + + // Update staker stats + const stakerId = `${stakingContract}_${userAddress}`; + const existingStaker = await context.MiberaStaker.get(stakerId); + + if (existingStaker) { + const updatedStaker: MiberaStakerEntity = { + ...existingStaker, + currentStakedCount: Math.max(0, existingStaker.currentStakedCount - 1), + totalWithdrawals: existingStaker.totalWithdrawals + 1, + lastActivityTime: timestamp, + }; + context.MiberaStaker.set(updatedStaker); + } +} diff --git a/src/handlers/tracked-erc721/constants.ts b/src/handlers/tracked-erc721/constants.ts new file mode 100644 index 0000000..ccc674d --- /dev/null +++ b/src/handlers/tracked-erc721/constants.ts @@ -0,0 +1,24 @@ +export const TRACKED_ERC721_COLLECTION_KEYS: Record = { + "0x6666397dfe9a8c469bf65dc744cb1c733416c420": "mibera", + "0x4b08a069381efbb9f08c73d6b2e975c9be3c4684": "mibera_tarot", + "0x86db98cf1b81e833447b12a077ac28c36b75c8e1": "miparcels", + "0x8d4972bd5d2df474e71da6676a365fb549853991": "miladies", + "0x144b27b1a267ee71989664b3907030da84cc4754": "mireveal_1_1", + "0x72db992e18a1bf38111b1936dd723e82d0d96313": "mireveal_2_2", + "0x3a00301b713be83ec54b7b4fb0f86397d087e6d3": "mireveal_3_3", + "0x419f25c4f9a9c730aacf58b8401b5b3e566fe886": "mireveal_4_20", + "0x81a27117bd894942ba6737402fb9e57e942c6058": "mireveal_5_5", + "0xaab7b4502251ae393d0590bab3e208e2d58f4813": "mireveal_6_6", + "0xc64126ea8dc7626c16daa2a29d375c33fcaa4c7c": "mireveal_7_7", + "0x24f4047d372139de8dacbe79e2fc576291ec3ffc": "mireveal_8_8", + // NOTE: mibera_zora is ERC-1155 (Zora platform), handled by MiberaZora1155 handler +}; + +/** + * Collections that should track all transfers (not just mints/burns) + * Used for timeline/activity tracking + */ +export const TRANSFER_TRACKED_COLLECTIONS = new Set([ + "mibera", + // NOTE: mibera_zora is ERC-1155, transfers tracked by mibera-zora.ts handler +]); diff --git a/src/handlers/vm-minted.ts b/src/handlers/vm-minted.ts new file mode 100644 index 0000000..97f0ab2 --- /dev/null +++ b/src/handlers/vm-minted.ts @@ -0,0 +1,46 @@ +/* + * VM Minted Event Handler + * + * Captures Minted(user, tokenId, traits) events from the VM contract. + * Enriches MintEvent entities with encoded trait data needed for metadata recovery. + * + * This handler captures the custom Minted event that includes the encoded_traits string, + * which is critical for regenerating VM metadata if it fails during the initial mint. + */ + +import { GeneralMints, MintEvent } from "generated"; + +export const handleVmMinted = GeneralMints.Minted.handler( + async ({ event, context }) => { + const { user, tokenId, traits } = event.params; + + const contractAddress = event.srcAddress.toLowerCase(); + const minter = user.toLowerCase(); + const id = `${event.transaction.hash}_${event.logIndex}`; + const timestamp = BigInt(event.block.timestamp); + const chainId = event.chainId; + + // Check if MintEvent already exists (from Transfer handler) + const existingMintEvent = await context.MintEvent.get(id); + + // Create new MintEvent with encoded traits + // If it already exists, spread its properties; otherwise create new + const mintEvent = { + ...(existingMintEvent || { + id, + collectionKey: "mibera_vm", // VM contract collection key + tokenId: BigInt(tokenId.toString()), + minter, + timestamp, + blockNumber: BigInt(event.block.number), + transactionHash: event.transaction.hash, + chainId, + }), + encodedTraits: traits, // Add or update encoded traits + }; + + context.MintEvent.set(mintEvent); + + console.log(`[VM Minted] Stored traits for tokenId ${tokenId}: ${traits}`); + } +); diff --git a/src/lib/actions.ts b/src/lib/actions.ts new file mode 100644 index 0000000..7dbc18d --- /dev/null +++ b/src/lib/actions.ts @@ -0,0 +1,128 @@ +import type { Action, handlerContext } from "generated"; + +type NumericInput = bigint | number | string | null | undefined; + +export interface NormalizedActionInput { + /** + * Unique identifier; defaults to `${txHash}_${logIndex}` when omitted. + */ + id?: string; + /** + * Mission/verifier friendly action type such as `mint`, `burn`, `swap`, `deposit`. + */ + actionType: string; + /** + * Wallet or contract that executed the action (expected to be lowercase already). + */ + actor: string; + /** + * Optional collection/pool identifier used for grouping. + */ + primaryCollection?: string | null; + /** + * Block timestamp (seconds). + */ + timestamp: bigint; + /** + * Chain/network identifier. + */ + chainId: number; + /** + * Transaction hash for traceability. + */ + txHash: string; + /** + * Optional log index for deterministic id generation. + */ + logIndex?: number | bigint; + /** + * Primary numeric metric (raw token amount, shares, etc.). + */ + numeric1?: NumericInput; + /** + * Secondary numeric metric (usd value, bonus points, etc.). + */ + numeric2?: NumericInput; + /** + * Arbitrary context serialised as JSON for downstream filters. + */ + context?: Record | Array | null; +} + +const toOptionalBigInt = (value: NumericInput): bigint | undefined => { + if (value === undefined || value === null) { + return undefined; + } + + if (typeof value === "bigint") { + return value; + } + + if (typeof value === "number") { + return BigInt(Math.trunc(value)); + } + + const trimmed = value.trim(); + if (trimmed.length === 0) { + return undefined; + } + + return BigInt(trimmed); +}; + +const serializeContext = ( + context: NormalizedActionInput["context"] +): string | undefined => { + if (!context) { + return undefined; + } + + try { + return JSON.stringify(context); + } catch (error) { + return undefined; + } +}; + +const resolveId = ( + input: Pick +): string => { + if (input.id) { + return input.id; + } + + if (input.logIndex === undefined) { + throw new Error( + `recordAction requires either an explicit id or logIndex for tx ${input.txHash}` + ); + } + + return `${input.txHash}_${input.logIndex.toString()}`; +}; + +export const recordAction = ( + context: Pick, + input: NormalizedActionInput +): void => { + const action: Action = { + id: resolveId(input), + actionType: input.actionType, + actor: input.actor, + primaryCollection: input.primaryCollection ?? undefined, + timestamp: input.timestamp, + chainId: input.chainId, + txHash: input.txHash, + numeric1: toOptionalBigInt(input.numeric1) ?? undefined, + numeric2: toOptionalBigInt(input.numeric2) ?? undefined, + context: serializeContext(input.context), + }; + + context.Action.set(action); +}; + +export const lowerCaseOrUndefined = (value?: string | null): string | undefined => { + if (!value) { + return undefined; + } + return value.toLowerCase(); +}; diff --git a/src/lib/erc721-holders.ts b/src/lib/erc721-holders.ts new file mode 100644 index 0000000..2dc167e --- /dev/null +++ b/src/lib/erc721-holders.ts @@ -0,0 +1,222 @@ +import { ZERO_ADDRESS } from "../handlers/constants"; +import type { + handlerContext, + Holder, + Token, + Transfer, + CollectionStat, +} from "generated"; + +export interface Erc721TransferEventLike { + readonly params: { + readonly from: string; + readonly to: string; + readonly tokenId: bigint; + }; + readonly srcAddress: string; + readonly transaction: { readonly hash: string }; + readonly block: { readonly timestamp: number; readonly number: number }; + readonly logIndex: number; + readonly chainId: number; +} + +export async function processErc721Transfer({ + event, + context, + collectionAddress, +}: { + event: Erc721TransferEventLike; + context: handlerContext; + collectionAddress?: string; +}) { + const { params, srcAddress, transaction, block, logIndex, chainId } = event; + const from = params.from.toLowerCase(); + const to = params.to.toLowerCase(); + const tokenId = params.tokenId; + const collection = (collectionAddress ?? srcAddress).toLowerCase(); + const zero = ZERO_ADDRESS.toLowerCase(); + const timestamp = BigInt(block.timestamp); + + const transferId = `${transaction.hash}_${logIndex}`; + const transfer: Transfer = { + id: transferId, + tokenId, + from, + to, + timestamp, + blockNumber: BigInt(block.number), + transactionHash: transaction.hash, + collection, + chainId, + }; + context.Transfer.set(transfer); + + const tokenKey = `${collection}_${chainId}_${tokenId}`; + const existingToken = await context.Token.get(tokenKey); + const updatedToken: Token = existingToken + ? { + ...existingToken, + owner: to, + isBurned: to === zero, + lastTransferTime: timestamp, + } + : { + id: tokenKey, + collection, + chainId, + tokenId, + owner: to, + isBurned: to === zero, + mintedAt: from === zero ? timestamp : BigInt(0), + lastTransferTime: timestamp, + }; + context.Token.set(updatedToken); + + const fromHolderId = `${collection}_${chainId}_${from}`; + const toHolderId = `${collection}_${chainId}_${to}`; + const fromHolderBefore = from === zero ? undefined : await context.Holder.get(fromHolderId); + const toHolderBefore = to === zero ? undefined : await context.Holder.get(toHolderId); + + await updateHolder( + context, + collection, + chainId, + from, + -1, + timestamp, + false, + zero, + fromHolderBefore + ); + await updateHolder( + context, + collection, + chainId, + to, + +1, + timestamp, + from === zero, + zero, + toHolderBefore + ); + + await updateCollectionStats({ + context, + collection, + chainId, + from, + to, + timestamp, + zero, + fromHolderBefore, + toHolderBefore, + }); +} + +async function updateHolder( + context: handlerContext, + collection: string, + chainId: number, + address: string, + delta: number, + timestamp: bigint, + isMint: boolean, + zero: string, + existingOverride?: Holder | undefined, +) { + if (address === zero) return; + + const holderId = `${collection}_${chainId}_${address}`; + const existing = existingOverride ?? (await context.Holder.get(holderId)); + + const balance = Math.max(0, (existing?.balance ?? 0) + delta); + const baseMinted = existing?.totalMinted ?? 0; + const totalMinted = isMint ? baseMinted + 1 : baseMinted; + const firstMintTime = existing?.firstMintTime ?? (isMint ? timestamp : undefined); + + const holder: Holder = { + id: holderId, + address, + balance, + totalMinted, + lastActivityTime: timestamp, + firstMintTime, + collection, + chainId, + }; + + context.Holder.set(holder); +} + +async function updateCollectionStats({ + context, + collection, + chainId, + from, + to, + timestamp, + zero, + fromHolderBefore, + toHolderBefore, +}: { + context: handlerContext; + collection: string; + chainId: number; + from: string; + to: string; + timestamp: bigint; + zero: string; + fromHolderBefore?: Holder; + toHolderBefore?: Holder; +}) { + const statsId = `${collection}_${chainId}`; + const existing = await context.CollectionStat.get(statsId); + + const totalSupply = existing?.totalSupply ?? 0; + const totalMinted = existing?.totalMinted ?? 0; + const totalBurned = existing?.totalBurned ?? 0; + const uniqueHolders = existing?.uniqueHolders ?? 0; + const lastMintTime = existing?.lastMintTime; + + let newTotalSupply = totalSupply; + let newTotalMinted = totalMinted; + let newTotalBurned = totalBurned; + let newLastMintTime = lastMintTime; + let uniqueAdjustment = 0; + + if (from === zero) { + newTotalSupply += 1; + newTotalMinted += 1; + newLastMintTime = timestamp; + } else if (to === zero) { + newTotalSupply = Math.max(0, newTotalSupply - 1); + newTotalBurned += 1; + } + + if (to !== zero) { + const hadBalanceBefore = (toHolderBefore?.balance ?? 0) > 0; + if (!hadBalanceBefore) { + uniqueAdjustment += 1; + } + } + + if (from !== zero) { + const balanceBefore = fromHolderBefore?.balance ?? 0; + if (balanceBefore === 1) { + uniqueAdjustment -= 1; + } + } + + const stats: CollectionStat = { + id: statsId, + collection, + totalSupply: Math.max(0, newTotalSupply), + totalMinted: newTotalMinted, + totalBurned: newTotalBurned, + uniqueHolders: Math.max(0, uniqueHolders + uniqueAdjustment), + lastMintTime: newLastMintTime, + chainId, + }; + + context.CollectionStat.set(stats); +} diff --git a/src/lib/mint-detection.ts b/src/lib/mint-detection.ts new file mode 100644 index 0000000..8fb1c98 --- /dev/null +++ b/src/lib/mint-detection.ts @@ -0,0 +1,49 @@ +/* + * Shared mint and burn detection utilities for THJ indexer. + * + * Centralizes logic for detecting mints, burns, and airdrops across + * ERC-721 and ERC-1155 handlers. + */ + +import { ZERO_ADDRESS } from "../handlers/constants"; + +// Common burn address used by many projects +export const DEAD_ADDRESS = "0x000000000000000000000000000000000000dead"; + +/** + * Check if transfer is a mint (from zero address) + */ +export function isMintFromZero(fromAddress: string): boolean { + return fromAddress.toLowerCase() === ZERO_ADDRESS; +} + +/** + * Check if transfer is a mint or airdrop (from zero OR from specified airdrop wallets) + * Use this when a collection has a distribution wallet that airdrops tokens. + */ +export function isMintOrAirdrop( + fromAddress: string, + airdropWallets?: Set +): boolean { + const lower = fromAddress.toLowerCase(); + if (lower === ZERO_ADDRESS) { + return true; + } + return airdropWallets?.has(lower) ?? false; +} + +/** + * Check if an address is a burn destination (zero or dead address) + */ +export function isBurnAddress(address: string): boolean { + const lower = address.toLowerCase(); + return lower === ZERO_ADDRESS || lower === DEAD_ADDRESS; +} + +/** + * Check if transfer is a burn (to burn address, not from zero) + * Excludes mints to burn address which would be unusual but technically possible. + */ +export function isBurnTransfer(fromAddress: string, toAddress: string): boolean { + return !isMintFromZero(fromAddress) && isBurnAddress(toAddress); +} diff --git a/test/Test.ts b/test/Test.ts deleted file mode 100644 index d3d8ead..0000000 --- a/test/Test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import assert from "assert"; -import { - TestHelpers, - HoneyJar_Approval -} from "generated"; -const { MockDb, HoneyJar } = TestHelpers; - -describe("HoneyJar contract Approval event tests", () => { - // Create mock db - const mockDb = MockDb.createMockDb(); - - // Creating mock for HoneyJar contract Approval event - const event = HoneyJar.Approval.createMockEvent({/* It mocks event fields with default values. You can overwrite them if you need */}); - - it("HoneyJar_Approval is created correctly", async () => { - // Processing the event - const mockDbUpdated = await HoneyJar.Approval.processEvent({ - event, - mockDb, - }); - - // Getting the actual entity from the mock database - let actualHoneyJarApproval = mockDbUpdated.entities.HoneyJar_Approval.get( - `${event.chainId}_${event.block.number}_${event.logIndex}` - ); - - // Creating the expected entity - const expectedHoneyJarApproval: HoneyJar_Approval = { - id: `${event.chainId}_${event.block.number}_${event.logIndex}`, - owner: event.params.owner, - approved: event.params.approved, - tokenId: event.params.tokenId, - }; - // Asserting that the entity in the mock database is the same as the expected entity - assert.deepEqual(actualHoneyJarApproval, expectedHoneyJarApproval, "Actual HoneyJarApproval should be the same as the expectedHoneyJarApproval"); - }); -}); diff --git a/verify-final-supplies.js b/verify-final-supplies.js new file mode 100644 index 0000000..1dc88ac --- /dev/null +++ b/verify-final-supplies.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +const EXPECTED_TOTALS = { + 'Honeycomb': 16420, + 'HoneyJar1': 10926, + 'HoneyJar2': 10089, + 'HoneyJar3': 9395, + 'HoneyJar4': 8677, + 'HoneyJar5': 8015, + 'HoneyJar6': 5898 +}; + +const ACTUAL_DATA = { + 'HoneyJar1': { circulating: 0, totalMinted: 2868, totalBurned: 11, correctCalc: 2857 }, + 'HoneyJar2': { circulating: 6909, totalMinted: 9575, totalBurned: 31, correctCalc: 9544 }, + 'HoneyJar3': { circulating: 7393, totalMinted: 9981, totalBurned: 8, correctCalc: 9973 }, + 'HoneyJar4': { circulating: 6434, totalMinted: 9022, totalBurned: 14, correctCalc: 9008 }, + 'HoneyJar5': { circulating: 6830, totalMinted: 9598, totalBurned: 22, correctCalc: 9576 }, + 'HoneyJar6': { circulating: 5898, totalMinted: 8426, totalBurned: 37, correctCalc: 8389 }, + 'Honeycomb': { circulating: 16420, totalMinted: 25611, totalBurned: 135, correctCalc: 25476 } +}; + +console.log('🔍 THJ Supply Verification - FINAL REPORT'); +console.log('=========================================\n'); + +console.log('Collection | Expected | Current | Should Be | Status'); +console.log('------------|----------|----------|-----------|--------'); + +Object.keys(EXPECTED_TOTALS).forEach(collection => { + const expected = EXPECTED_TOTALS[collection]; + const actual = ACTUAL_DATA[collection]; + const currentSupply = actual.circulating; + const shouldBe = actual.correctCalc; // totalMinted - totalBurned + const status = Math.abs(currentSupply - expected) <= 10 ? '✅' : '⚠️'; + + console.log( + `${collection.padEnd(11)} | ${String(expected).padEnd(8)} | ${String(currentSupply).padEnd(8)} | ${String(shouldBe).padEnd(9)} | ${status}` + ); +}); + +console.log('\n❌ Critical Issues Found:'); +console.log('------------------------'); +console.log('1. HoneyJar1: Still showing 0 supply (should be 10,926)'); +console.log(' - Only 2,868 mints tracked vs expected ~11,000'); +console.log(' - Missing 8,000+ mint events on Ethereum\n'); + +console.log('2. HoneyJar2-5: Still under-reporting by 2,000-3,000 tokens'); +console.log(' - Even with corrected L0 remint addresses'); +console.log(' - Suggests missing mint events or incorrect tracking\n'); + +console.log('3. The calculation formula is WRONG:'); +console.log(' - Currently using: homeChainSupply + ethereumSupply'); +console.log(' - Should be using: totalMinted - totalBurned\n'); + +console.log('📊 If we fix the formula:'); +console.log('-------------------------'); +Object.keys(EXPECTED_TOTALS).forEach(collection => { + const expected = EXPECTED_TOTALS[collection]; + const actual = ACTUAL_DATA[collection]; + const correctSupply = actual.correctCalc; + const diff = correctSupply - expected; + const status = Math.abs(diff) <= 10 ? '✅' : '⚠️'; + + console.log(`${collection}: ${correctSupply} (${diff >= 0 ? '+' : ''}${diff} from expected) ${status}`); +}); diff --git a/verify-supplies.js b/verify-supplies.js new file mode 100644 index 0000000..36bbfbb --- /dev/null +++ b/verify-supplies.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +console.log('🎯 THJ SUPPLY VERIFICATION - CURRENT STATUS'); +console.log('='.repeat(60)); + +// EXPECTED TOTALS from requirements +const EXPECTED = { + 'Honeycomb': 16420, + 'HoneyJar1': 10926, + 'HoneyJar2': 10089, + 'HoneyJar3': 9395, + 'HoneyJar4': 8677, + 'HoneyJar5': 8015, + 'HoneyJar6': 5898 +}; + +// CURRENT INDEXER (after double-counting fix) +const INDEXER = { + 'Honeycomb': 16420, + 'HoneyJar1': 7982, + 'HoneyJar2': 6909, + 'HoneyJar3': 7393, + 'HoneyJar4': 6434, + 'HoneyJar5': 6830, + 'HoneyJar6': 5898 +}; + +console.log('\nCollection | Expected | Indexer | Diff | Status'); +console.log('------------|----------|----------|---------|----------'); + +let perfectMatches = []; +let issues = []; + +Object.keys(EXPECTED).forEach(collection => { + const expected = EXPECTED[collection]; + const indexer = INDEXER[collection]; + const diff = indexer - expected; + + let status; + if (diff === 0) { + status = '✅ PERFECT'; + perfectMatches.push(collection); + } else { + status = '❌ Issue'; + issues.push({ collection, expected, indexer, diff }); + } + + console.log( + `${collection.padEnd(11)} | ${String(expected).padEnd(8)} | ${String(indexer).padEnd(8)} | ${String(diff).padStart(7)} | ${status}` + ); +}); + +console.log('\n📊 SUMMARY:'); +console.log('='.repeat(60)); + +console.log('\n✅ PERFECT MATCHES (2 collections):'); +perfectMatches.forEach(c => { + console.log(` • ${c}: ${INDEXER[c]} - Exactly matching expected!`); +}); + +console.log('\n❌ NOT MATCHING EXPECTED (5 collections):'); +issues.forEach(({ collection, expected, indexer, diff }) => { + console.log(` • ${collection}: Shows ${indexer}, expected ${expected} (missing ${Math.abs(diff)})`); +}); + +console.log('\n💡 TO ANSWER YOUR QUESTION:'); +console.log('-'.repeat(60)); +console.log('YES, these are PERFECTLY matching expected:'); +console.log(' ✅ HoneyJar6: 5,898'); +console.log(' ✅ Honeycomb: 16,420'); +console.log('\nNO, HoneyJar1 is NOT matching:'); +console.log(' ❌ HoneyJar1: Shows 7,982 (expected 10,926)'); +console.log('\nThe other collections (Gen 2-5) match on-chain reality'); +console.log('but not the "expected" values in this script.');