diff --git a/src/app.ts b/src/app.ts index ff7460ae29..57e9be1490 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,6 +20,7 @@ import { register0xRoutes } from './connectors/0x/0x.routes'; import { jupiterRoutes } from './connectors/jupiter/jupiter.routes'; import { meteoraRoutes } from './connectors/meteora/meteora.routes'; import { orcaRoutes } from './connectors/orca/orca.routes'; +import { oreConnectorRoutes } from './connectors/ore/ore.routes'; import { pancakeswapRoutes } from './connectors/pancakeswap/pancakeswap.routes'; import { pancakeswapSolRoutes } from './connectors/pancakeswap-sol/pancakeswap-sol.routes'; import { raydiumRoutes } from './connectors/raydium/raydium.routes'; @@ -108,6 +109,10 @@ const swaggerOptions = { name: '/connector/pancakeswap', description: 'PancakeSwap EVM connector endpoints', }, + { + name: '/connector/ore', + description: 'ORE mining game connector endpoints (experimental)', + }, ], components: { parameters: { @@ -277,6 +282,9 @@ const configureGatewayServer = () => { // PancakeSwap Solana routes app.register(pancakeswapSolRoutes, { prefix: '/connectors/pancakeswap-sol' }); + + // ORE mining game routes (experimental) + app.register(oreConnectorRoutes.ore, { prefix: '/connectors/ore/ore' }); }; // Register routes on main server diff --git a/src/connectors/ore/ORE_INTEGRATION_GUIDE.md b/src/connectors/ore/ORE_INTEGRATION_GUIDE.md new file mode 100644 index 0000000000..e66948ca3f --- /dev/null +++ b/src/connectors/ore/ORE_INTEGRATION_GUIDE.md @@ -0,0 +1,303 @@ +# ORE Program Integration Guide + +Reference documentation for building a Hummingbot Gateway connector for the ORE mining game on Solana. + +## Program Overview + +ORE v3 is a proof-of-work style mining game where participants deploy SOL to a 5x5 grid (25 squares). Each round, a winning square is determined by on-chain entropy, and participants who deployed to that square split the prize pool and earn ORE tokens. + +**Key URLs:** +- App: https://ore.supply/ +- Repository: https://github.com/regolith-labs/ore +- IDL: https://raw.githubusercontent.com/regolith-labs/ore/refs/heads/master/api/idl.json +- Rust Docs: https://docs.rs/ore-api/latest/ore_api/ + +## Program IDs + +``` +ORE Program: oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv +ORE Token Mint: oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp +Entropy Program: 3jSkUuYBoJzQPMEzTvkDFXCZUBksPamrVhrnHR9igu2X +``` + +## Framework Notes + +ORE v3 is built with **Steel** (https://github.com/regolith-labs/steel), a custom Solana framework by HardhatChad — NOT Anchor. Key differences: + +1. **Instruction discriminators** are single `u8` values (not 8-byte Anchor discriminators) +2. **Account discriminators** are 8 bytes but use simple numeric patterns like `[100, 0, 0, 0, 0, 0, 0, 0]` +3. **IDL format** is Anchor-compatible but generated by Steel +4. Standard Anchor client generation tools may need adjustment + +## IDL Structure (v3.7.20) + +The IDL contains: +- 19 instructions +- 7 account types +- 1 custom type (Numeric - fixed-point I80F48) +- 2 events (ResetEvent, BuryEvent) +- 2 error codes + +### Instruction Discriminators + +``` +automate = 0 +checkpoint = 2 +claimSol = 3 +claimOre = 4 +close = 5 +deploy = 6 +log = 8 +reset = 9 +deposit = 10 +withdraw = 11 +claimYield = 12 +bury = 13 +wrap = 14 +setAdmin = 15 +setFeeCollector = 16 +newVar = 17 +setBuffer = 18 +``` + +### Account Discriminators (first 8 bytes) + +``` +Automation = [100, 0, 0, 0, 0, 0, 0, 0] +Config = [101, 0, 0, 0, 0, 0, 0, 0] +Miner = [103, 0, 0, 0, 0, 0, 0, 0] +Treasury = [104, 0, 0, 0, 0, 0, 0, 0] +Board = [105, 0, 0, 0, 0, 0, 0, 0] +Stake = [108, 0, 0, 0, 0, 0, 0, 0] +Round = [109, 0, 0, 0, 0, 0, 0, 0] +``` + +## PDA Seeds + +| Account | Seeds | Notes | +|---------|-------|-------| +| Automation | `["automation", authority]` | Per-user automation config | +| Board | `["board"]` | Singleton, tracks current round | +| Config | `["config"]` | Singleton, program settings | +| Miner | `["miner", authority]` | Per-user mining state | +| Round | `["round", round_id (u64 LE)]` | Per-round state | +| Stake | `["stake", authority]` | Per-user staking state | +| Treasury | `["treasury"]` | Singleton, token vault | + +## Core Mining Flow + +### 1. Deploy SOL to Squares + +**Instruction:** `deploy` (discriminator: 6) + +**Args:** +- `amount: u64` — lamports to deploy +- `squares: u32` — bitmask of squares (0-24, so bits 0-24 represent squares) + +**Accounts:** +``` +signer [signer, writable] +authority [writable] # usually same as signer +automation [writable] # PDA: ["automation", authority] +board [writable] # PDA: ["board"] +config [writable] # PDA: ["config"] +miner [writable] # PDA: ["miner", authority] +round [writable] # PDA: ["round", board.round_id] +systemProgram [] # 11111111111111111111111111111111 +oreProgram [] # oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv +entropyVar [writable] # from config.var_address +entropyProgram [] # 3jSkUuYBoJzQPMEzTvkDFXCZUBksPamrVhrnHR9igu2X +``` + +**Square Bitmask Examples:** +- Square 0 only: `0b1` = `1` +- Square 12 (center): `0b1000000000000` = `4096` +- All 25 squares: `0b1111111111111111111111111` = `33554431` +- Squares 0, 5, 10, 15, 20 (diagonal): `0b100001000010000100001` = `1082401` + +### 2. Settle Rewards After Round Ends + +**Instruction:** `checkpoint` (discriminator: 2) + +**Args:** none + +**Accounts:** +``` +signer [signer, writable] +board [] # PDA: ["board"] +miner [writable] # PDA: ["miner", signer] +round [writable] # PDA: ["round", ] +treasury [writable] # PDA: ["treasury"] +systemProgram [] +``` + +Call this after a round ends to calculate and credit your winnings to the Miner account. + +### 3. Claim SOL Rewards + +**Instruction:** `claimSol` (discriminator: 3) + +**Args:** none + +**Accounts:** +``` +signer [signer, writable] +miner [writable] # PDA: ["miner", signer] +systemProgram [] +``` + +Withdraws `miner.rewards_sol` to the signer. + +### 4. Claim ORE Rewards + +**Instruction:** `claimOre` (discriminator: 4) + +**Args:** none + +**Accounts:** +``` +signer [signer, writable] +miner [writable] # PDA: ["miner", signer] +mint [] # oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp +recipient [writable] # signer's ORE ATA +treasury [writable] # PDA: ["treasury"] +treasuryTokens [writable] # treasury's ORE ATA +systemProgram [] +tokenProgram [] # TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA +associatedTokenProgram [] # ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL +``` + +Withdraws `miner.rewards_ore` as ORE tokens to recipient ATA. + +## Staking Flow + +Separate from mining — stake ORE tokens to earn yield. + +| Instruction | Discriminator | Purpose | +|-------------|---------------|---------| +| `deposit` | 10 | Stake ORE tokens | +| `withdraw` | 11 | Unstake ORE tokens | +| `claimYield` | 12 | Claim staking rewards | + +## Key Account Structures + +### Board (singleton) +``` +round_id: u64 # Current round number +start_slot: u64 # Round start slot +end_slot: u64 # Round end slot +epoch_id: u64 +``` + +### Miner (per user) +``` +authority: PublicKey +deployed: [u64; 25] # SOL deployed per square this round +cumulative: [u64; 25] # Historical deployments +checkpoint_fee: u64 +checkpoint_id: u64 +last_claim_ore_at: i64 +last_claim_sol_at: i64 +rewards_factor: Numeric # I80F48 fixed-point +rewards_sol: u64 # Claimable SOL +rewards_ore: u64 # Claimable ORE +refined_ore: u64 +round_id: u64 # Last active round +lifetime_rewards_sol: u64 +lifetime_rewards_ore: u64 +lifetime_deployed: u64 +``` + +### Round (per round) +``` +id: u64 +deployed: [u64; 25] # Total SOL per square +slot_hash: [u8; 32] # Entropy source +count: [u64; 25] # Number of deployers per square +expires_at: u64 +motherlode: u64 # Prize pool +rent_payer: PublicKey +top_miner: PublicKey +top_miner_reward: u64 +total_deployed: u64 +total_miners: u64 +total_vaulted: u64 +total_winnings: u64 +``` + +### Treasury (singleton) +``` +balance: u64 +motherlode: u64 +miner_rewards_factor: Numeric +stake_rewards_factor: Numeric +total_refined: u64 +total_staked: u64 +total_unclaimed: u64 +``` + +## Numeric Type + +Fixed-point number using I80F48 representation: +``` +bits: [u8; 16] # 128-bit fixed-point value +``` + +## Events + +### ResetEvent +Emitted when a round ends. Contains: +- `round_id`, `start_slot`, `end_slot` +- `winning_square` (0-24) +- `top_miner` (address) +- `motherlode`, `total_deployed`, `total_winnings`, `total_minted` +- `ts` (timestamp) + +### BuryEvent +Emitted during buyback-and-burn operations. + +## Typical Integration Workflow + +``` +1. Fetch Board account → get current round_id +2. Fetch Round account → see prize pool, time remaining, square deployments +3. Deploy SOL to chosen squares +4. Wait for round to end (board.end_slot passes) +5. Call checkpoint to settle rewards +6. Call claimSol and/or claimOre to withdraw +``` + +## Error Codes + +``` +0 = AmountTooSmall "Amount too small" +1 = NotAuthorized "Not authorized" +``` + +## Reference Implementation + +The official CLI provides working examples: +- https://github.com/regolith-labs/ore-cli + +Key files: +- `src/deploy.rs` — deploy instruction construction +- `src/checkpoint.rs` — settlement logic +- `src/claim.rs` — claim instructions + +## Dependencies for TypeScript Client + +```json +{ + "@solana/web3.js": "^1.98.0", + "@solana/spl-token": "^0.4.0", + "@coral-xyz/borsh": "^0.30.0" +} +``` + +## Serialization Notes + +1. All integers are little-endian +2. PublicKeys are 32 bytes +3. Instruction data format: `[discriminator (1 byte)] [args...]` +4. Account data format: `[discriminator (8 bytes)] [fields...]` +5. Arrays like `[u64; 25]` are consecutive u64 LE values (200 bytes total) diff --git a/src/connectors/ore/idl/ore.json b/src/connectors/ore/idl/ore.json new file mode 100644 index 0000000000..4cbeddbd22 --- /dev/null +++ b/src/connectors/ore/idl/ore.json @@ -0,0 +1,1410 @@ +{ + "version": "3.7.20", + "name": "ore", + "instructions": [ + { + "name": "automate", + "discriminant": { + "type": "u8", + "value": 0 + }, + "docs": [ + "Configures or closes a miner automation account.", + "Automation PDA seeds: [\"automation\", signer].", + "Miner PDA seeds: [\"miner\", signer]." + ], + "accounts": [ + { + "name": "signer", + "isMut": true, + "isSigner": true + }, + { + "name": "automation", + "isMut": true, + "isSigner": false + }, + { + "name": "executor", + "isMut": false, + "isSigner": false + }, + { + "name": "miner", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "deposit", + "type": "u64" + }, + { + "name": "fee", + "type": "u64" + }, + { + "name": "mask", + "type": "u64" + }, + { + "name": "strategy", + "type": "u8" + } + ] + }, + { + "name": "checkpoint", + "discriminant": { + "type": "u8", + "value": 2 + }, + "docs": ["Settles miner rewards for a completed round.", "Treasury PDA seeds: [\"treasury\"]."], + "accounts": [ + { + "name": "signer", + "isMut": true, + "isSigner": true + }, + { + "name": "board", + "isMut": false, + "isSigner": false + }, + { + "name": "miner", + "isMut": true, + "isSigner": false + }, + { + "name": "round", + "isMut": true, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "claimSol", + "discriminant": { + "type": "u8", + "value": 3 + }, + "docs": ["Claims SOL rewards from the miner account."], + "accounts": [ + { + "name": "signer", + "isMut": true, + "isSigner": true + }, + { + "name": "miner", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "claimOre", + "discriminant": { + "type": "u8", + "value": 4 + }, + "docs": ["Claims ORE token rewards from the treasury vault."], + "accounts": [ + { + "name": "signer", + "isMut": true, + "isSigner": true + }, + { + "name": "miner", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false, + "address": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp" + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "treasuryTokens", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false, + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [] + }, + { + "name": "close", + "discriminant": { + "type": "u8", + "value": 5 + }, + "docs": [ + "Closes an expired round account and returns rent to the payer.", + "Round PDA seeds: [\"round\", round_id].", + "Treasury PDA seeds: [\"treasury\"]." + ], + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "board", + "isMut": true, + "isSigner": false + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": false + }, + { + "name": "round", + "isMut": true, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "deploy", + "discriminant": { + "type": "u8", + "value": 6 + }, + "docs": [ + "Deploys SOL to selected squares for the current round.", + "Automation PDA seeds: [\"automation\", authority].", + "Config PDA seeds: [\"config\"].", + "Miner PDA seeds: [\"miner\", authority].", + "Round PDA seeds: [\"round\", board.round_id]." + ], + "accounts": [ + { + "name": "signer", + "isMut": true, + "isSigner": true + }, + { + "name": "authority", + "isMut": true, + "isSigner": false + }, + { + "name": "automation", + "isMut": true, + "isSigner": false + }, + { + "name": "board", + "isMut": true, + "isSigner": false + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "miner", + "isMut": true, + "isSigner": false + }, + { + "name": "round", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + }, + { + "name": "oreProgram", + "isMut": false, + "isSigner": false, + "address": "oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv" + }, + { + "name": "entropyVar", + "isMut": true, + "isSigner": false + }, + { + "name": "entropyProgram", + "isMut": false, + "isSigner": false, + "address": "3jSkUuYBoJzQPMEzTvkDFXCZUBksPamrVhrnHR9igu2X" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "squares", + "type": "u32" + } + ] + }, + { + "name": "log", + "discriminant": { + "type": "u8", + "value": 8 + }, + "docs": [ + "Emits an arbitrary log message from the board PDA.", + "Bytes following the discriminator are logged verbatim." + ], + "accounts": [ + { + "name": "board", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "reset", + "discriminant": { + "type": "u8", + "value": 9 + }, + "docs": [ + "Finalizes the current round, mints rewards, and opens the next round.", + "Board PDA seeds: [\"board\"].", + "Treasury PDA seeds: [\"treasury\"].", + "Round PDA seeds: [\"round\", board.round_id] and [\"round\", board.round_id + 1]." + ], + "accounts": [ + { + "name": "signer", + "isMut": true, + "isSigner": true + }, + { + "name": "board", + "isMut": true, + "isSigner": false + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false, + "address": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp" + }, + { + "name": "round", + "isMut": true, + "isSigner": false + }, + { + "name": "roundNext", + "isMut": true, + "isSigner": false + }, + { + "name": "topMiner", + "isMut": false, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "treasuryTokens", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "oreProgram", + "isMut": false, + "isSigner": false, + "address": "oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv" + }, + { + "name": "slotHashesSysvar", + "isMut": false, + "isSigner": false, + "address": "SysvarS1otHashes111111111111111111111111111" + }, + { + "name": "entropyVar", + "isMut": false, + "isSigner": false + }, + { + "name": "entropyProgram", + "isMut": false, + "isSigner": false, + "address": "3jSkUuYBoJzQPMEzTvkDFXCZUBksPamrVhrnHR9igu2X" + } + ], + "args": [] + }, + { + "name": "deposit", + "discriminant": { + "type": "u8", + "value": 10 + }, + "docs": ["Deposits ORE into a staking account.", "Stake PDA seeds: [\"stake\", signer]."], + "accounts": [ + { + "name": "signer", + "isMut": true, + "isSigner": true + }, + { + "name": "mint", + "isMut": false, + "isSigner": false, + "address": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp" + }, + { + "name": "sender", + "isMut": true, + "isSigner": false + }, + { + "name": "stake", + "isMut": true, + "isSigner": false + }, + { + "name": "stakeTokens", + "isMut": true, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false, + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "withdraw", + "discriminant": { + "type": "u8", + "value": 11 + }, + "docs": ["Withdraws ORE from a staking account.", "Stake PDA seeds: [\"stake\", signer]."], + "accounts": [ + { + "name": "signer", + "isMut": true, + "isSigner": true + }, + { + "name": "mint", + "isMut": false, + "isSigner": false, + "address": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp" + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "stake", + "isMut": true, + "isSigner": false + }, + { + "name": "stakeTokens", + "isMut": true, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false, + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "claimYield", + "discriminant": { + "type": "u8", + "value": 12 + }, + "docs": [ + "Claims accrued staking rewards.", + "Stake PDA seeds: [\"stake\", signer].", + "Treasury PDA seeds: [\"treasury\"]." + ], + "accounts": [ + { + "name": "signer", + "isMut": true, + "isSigner": true + }, + { + "name": "mint", + "isMut": false, + "isSigner": false, + "address": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp" + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "stake", + "isMut": true, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "treasuryTokens", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false, + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "bury", + "discriminant": { + "type": "u8", + "value": 13 + }, + "docs": [ + "Swaps vaulted SOL for ORE via a CPI and burns the proceeds.", + "Treasury PDA seeds: [\"treasury\"].", + "Additional swap accounts are passed through as remaining accounts." + ], + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "board", + "isMut": true, + "isSigner": false + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false, + "address": "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp" + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "treasuryOre", + "isMut": true, + "isSigner": false + }, + { + "name": "treasurySol", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "oreProgram", + "isMut": false, + "isSigner": false, + "address": "oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv" + } + ], + "args": [] + }, + { + "name": "wrap", + "discriminant": { + "type": "u8", + "value": 14 + }, + "docs": ["Wraps SOL held by the treasury into WSOL for swapping.", "Treasury PDA seeds: [\"treasury\"]."], + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "treasurySol", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "setAdmin", + "discriminant": { + "type": "u8", + "value": 15 + }, + "docs": ["Updates the program admin address."], + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "admin", + "type": "publicKey" + } + ] + }, + { + "name": "setFeeCollector", + "discriminant": { + "type": "u8", + "value": 16 + }, + "docs": ["Updates the fee collector authority."], + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "feeCollector", + "type": "publicKey" + } + ] + }, + { + "name": "newVar", + "discriminant": { + "type": "u8", + "value": 17 + }, + "docs": ["Creates a new entropy var account through the entropy program."], + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "board", + "isMut": true, + "isSigner": false + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "provider", + "isMut": false, + "isSigner": false + }, + { + "name": "var", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + }, + { + "name": "entropyProgram", + "isMut": false, + "isSigner": false, + "address": "3jSkUuYBoJzQPMEzTvkDFXCZUBksPamrVhrnHR9igu2X" + } + ], + "args": [ + { + "name": "id", + "type": "u64" + }, + { + "name": "commit", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "samples", + "type": "u64" + } + ] + }, + { + "name": "setBuffer", + "discriminant": { + "type": "u8", + "value": 18 + }, + "docs": ["Updates the global buffer value."], + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "buffer", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "Automation", + "discriminator": [100, 0, 0, 0, 0, 0, 0, 0], + "docs": ["Automation parameters for automated mining deployments."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "balance", + "type": "u64" + }, + { + "name": "executor", + "type": "publicKey" + }, + { + "name": "fee", + "type": "u64" + }, + { + "name": "strategy", + "type": "u64" + }, + { + "name": "mask", + "type": "u64" + } + ] + } + }, + { + "name": "Board", + "discriminator": [105, 0, 0, 0, 0, 0, 0, 0], + "docs": ["Global round tracking for the mining game."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "round_id", + "type": "u64" + }, + { + "name": "start_slot", + "type": "u64" + }, + { + "name": "end_slot", + "type": "u64" + }, + { + "name": "epoch_id", + "type": "u64" + } + ] + } + }, + { + "name": "Config", + "discriminator": [101, 0, 0, 0, 0, 0, 0, 0], + "docs": ["Program configuration state."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "admin", + "type": "publicKey" + }, + { + "name": "bury_authority", + "type": "publicKey" + }, + { + "name": "fee_collector", + "type": "publicKey" + }, + { + "name": "swap_program", + "type": "publicKey" + }, + { + "name": "var_address", + "type": "publicKey" + }, + { + "name": "buffer", + "type": "u64" + } + ] + } + }, + { + "name": "Miner", + "discriminator": [103, 0, 0, 0, 0, 0, 0, 0], + "docs": ["Tracks a miner's deployed SOL and reward balances."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "deployed", + "type": { + "array": ["u64", 25] + } + }, + { + "name": "cumulative", + "type": { + "array": ["u64", 25] + } + }, + { + "name": "checkpoint_fee", + "type": "u64" + }, + { + "name": "checkpoint_id", + "type": "u64" + }, + { + "name": "last_claim_ore_at", + "type": "i64" + }, + { + "name": "last_claim_sol_at", + "type": "i64" + }, + { + "name": "rewards_factor", + "type": { + "defined": "Numeric" + } + }, + { + "name": "rewards_sol", + "type": "u64" + }, + { + "name": "rewards_ore", + "type": "u64" + }, + { + "name": "refined_ore", + "type": "u64" + }, + { + "name": "round_id", + "type": "u64" + }, + { + "name": "lifetime_rewards_sol", + "type": "u64" + }, + { + "name": "lifetime_rewards_ore", + "type": "u64" + }, + { + "name": "lifetime_deployed", + "type": "u64" + } + ] + } + }, + { + "name": "Round", + "discriminator": [109, 0, 0, 0, 0, 0, 0, 0], + "docs": ["State for a single mining round."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "id", + "type": "u64" + }, + { + "name": "deployed", + "type": { + "array": ["u64", 25] + } + }, + { + "name": "slot_hash", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "count", + "type": { + "array": ["u64", 25] + } + }, + { + "name": "expires_at", + "type": "u64" + }, + { + "name": "motherlode", + "type": "u64" + }, + { + "name": "rent_payer", + "type": "publicKey" + }, + { + "name": "top_miner", + "type": "publicKey" + }, + { + "name": "top_miner_reward", + "type": "u64" + }, + { + "name": "total_deployed", + "type": "u64" + }, + { + "name": "total_miners", + "type": "u64" + }, + { + "name": "total_vaulted", + "type": "u64" + }, + { + "name": "total_winnings", + "type": "u64" + } + ] + } + }, + { + "name": "Stake", + "discriminator": [108, 0, 0, 0, 0, 0, 0, 0], + "docs": ["State for a staking participant."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "balance", + "type": "u64" + }, + { + "name": "buffer_a", + "type": "u64" + }, + { + "name": "buffer_b", + "type": "u64" + }, + { + "name": "buffer_c", + "type": "u64" + }, + { + "name": "buffer_d", + "type": "u64" + }, + { + "name": "buffer_e", + "type": "u64" + }, + { + "name": "last_claim_at", + "type": "i64" + }, + { + "name": "last_deposit_at", + "type": "i64" + }, + { + "name": "last_withdraw_at", + "type": "i64" + }, + { + "name": "rewards_factor", + "type": { + "defined": "Numeric" + } + }, + { + "name": "rewards", + "type": "u64" + }, + { + "name": "lifetime_rewards", + "type": "u64" + }, + { + "name": "buffer_f", + "type": "u64" + } + ] + } + }, + { + "name": "Treasury", + "discriminator": [104, 0, 0, 0, 0, 0, 0, 0], + "docs": ["Singleton treasury account tracking protocol balances."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "balance", + "type": "u64" + }, + { + "name": "buffer_a", + "type": "u64" + }, + { + "name": "motherlode", + "type": "u64" + }, + { + "name": "miner_rewards_factor", + "type": { + "defined": "Numeric" + } + }, + { + "name": "stake_rewards_factor", + "type": { + "defined": "Numeric" + } + }, + { + "name": "buffer_b", + "type": "u64" + }, + { + "name": "total_refined", + "type": "u64" + }, + { + "name": "total_staked", + "type": "u64" + }, + { + "name": "total_unclaimed", + "type": "u64" + } + ] + } + } + ], + "types": [ + { + "name": "Numeric", + "docs": ["Fixed-point helper backed by I80F48 from the steel crate."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bits", + "type": { + "array": ["u8", 16] + } + } + ] + } + } + ], + "events": [ + { + "name": "ResetEvent", + "discriminator": [0, 0, 0, 0, 0, 0, 0, 0], + "fields": [ + { + "name": "disc", + "type": "u64", + "index": false + }, + { + "name": "round_id", + "type": "u64", + "index": false + }, + { + "name": "start_slot", + "type": "u64", + "index": false + }, + { + "name": "end_slot", + "type": "u64", + "index": false + }, + { + "name": "winning_square", + "type": "u64", + "index": false + }, + { + "name": "top_miner", + "type": "publicKey", + "index": false + }, + { + "name": "num_winners", + "type": "u64", + "index": false + }, + { + "name": "motherlode", + "type": "u64", + "index": false + }, + { + "name": "total_deployed", + "type": "u64", + "index": false + }, + { + "name": "total_vaulted", + "type": "u64", + "index": false + }, + { + "name": "total_winnings", + "type": "u64", + "index": false + }, + { + "name": "total_minted", + "type": "u64", + "index": false + }, + { + "name": "ts", + "type": "i64", + "index": false + } + ] + }, + { + "name": "BuryEvent", + "discriminator": [1, 0, 0, 0, 0, 0, 0, 0], + "fields": [ + { + "name": "disc", + "type": "u64", + "index": false + }, + { + "name": "ore_buried", + "type": "u64", + "index": false + }, + { + "name": "ore_shared", + "type": "u64", + "index": false + }, + { + "name": "sol_amount", + "type": "u64", + "index": false + }, + { + "name": "new_circulating_supply", + "type": "u64", + "index": false + }, + { + "name": "ts", + "type": "i64", + "index": false + } + ] + } + ], + "errors": [ + { + "code": 0, + "name": "AmountTooSmall", + "msg": "Amount too small" + }, + { + "code": 1, + "name": "NotAuthorized", + "msg": "Not authorized" + } + ], + "metadata": { + "address": "oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv", + "origin": "steel" + } +} diff --git a/src/connectors/ore/ore-routes/accountInfo.ts b/src/connectors/ore/ore-routes/accountInfo.ts new file mode 100644 index 0000000000..07bc5987f6 --- /dev/null +++ b/src/connectors/ore/ore-routes/accountInfo.ts @@ -0,0 +1,51 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { + OreAccountInfoRequest, + OreAccountInfoRequestType, + OreAccountInfoResponse, + OreAccountInfoResponseType, +} from '../schemas'; + +export const accountInfoRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: OreAccountInfoRequestType; + Reply: OreAccountInfoResponseType; + }>( + '/account-info', + { + schema: { + description: 'Get ORE account information (miner + stake) for a wallet', + tags: ['/connector/ore'], + querystring: OreAccountInfoRequest, + response: { + 200: OreAccountInfoResponse, + }, + }, + }, + async (request) => { + try { + const network = request.query.network || 'mainnet-beta'; + const { walletAddress, roundId } = request.query; + + if (!walletAddress) { + throw httpErrors.badRequest('walletAddress is required'); + } + + const ore = await Ore.getInstance(network); + return await ore.getAccountInfo(walletAddress, roundId); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default accountInfoRoute; diff --git a/src/connectors/ore/ore-routes/boardInfo.ts b/src/connectors/ore/ore-routes/boardInfo.ts new file mode 100644 index 0000000000..288853fd4f --- /dev/null +++ b/src/connectors/ore/ore-routes/boardInfo.ts @@ -0,0 +1,46 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { + OreBoardInfoRequest, + OreBoardInfoRequestType, + OreBoardInfoResponse, + OreBoardInfoResponseType, +} from '../schemas'; + +export const boardInfoRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: OreBoardInfoRequestType; + Reply: OreBoardInfoResponseType; + }>( + '/board-info', + { + schema: { + description: 'Get ORE board and current round information', + tags: ['/connector/ore'], + querystring: OreBoardInfoRequest, + response: { + 200: OreBoardInfoResponse, + }, + }, + }, + async (request) => { + try { + const network = request.query.network || 'mainnet-beta'; + const { roundId } = request.query; + const ore = await Ore.getInstance(network); + return await ore.getBoardInfo(roundId); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default boardInfoRoute; diff --git a/src/connectors/ore/ore-routes/checkpoint.ts b/src/connectors/ore/ore-routes/checkpoint.ts new file mode 100644 index 0000000000..2208d4f8ae --- /dev/null +++ b/src/connectors/ore/ore-routes/checkpoint.ts @@ -0,0 +1,185 @@ +import { PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { createCheckpointInstruction } from '../ore.instructions'; +import { + OreCheckpointRequest, + OreCheckpointRequestType, + OreCheckpointResponse, + OreCheckpointResponseType, +} from '../schemas'; + +const ORE_DECIMALS = 11; + +export async function checkpoint( + network: string, + walletAddress: string, + roundIdStr?: string, +): Promise { + // Validate wallet address + try { + new PublicKey(walletAddress); + } catch { + throw httpErrors.badRequest(`Invalid wallet address: ${walletAddress}`); + } + + const ore = await Ore.getInstance(network); + const { wallet, isHardwareWallet } = await ore.prepareWallet(walletAddress); + + // Determine round ID to checkpoint + let roundId: bigint; + if (roundIdStr) { + roundId = BigInt(roundIdStr); + } else { + // Default to the miner's last round + const miner = await ore.getMinerAccount(walletAddress); + if (!miner) { + throw httpErrors.notFound(`Miner account not found for wallet: ${walletAddress}`); + } + roundId = miner.roundId; + } + + // Verify miner has participated in this round + const miner = await ore.getMinerAccount(walletAddress); + if (!miner) { + throw httpErrors.notFound(`Miner account not found for wallet: ${walletAddress}`); + } + + // Check if already checkpointed (miner.roundId has moved past the requested round) + if (miner.roundId > roundId) { + throw httpErrors.badRequest( + `Round ${roundId} has already been checkpointed. Miner is now on round ${miner.roundId}.`, + ); + } + + // Verify miner actually participated in the specified round + if (miner.roundId !== roundId) { + throw httpErrors.badRequest( + `Miner last participated in round ${miner.roundId}, not round ${roundId}. ` + + `Checkpoint is only needed for rounds you participated in.`, + ); + } + + // Get round info to determine winning square and calculate results + const round = await ore.getRoundAccount(roundId); + + // Calculate winning square from slotHash + const isFinalized = !round.slotHash.every((b) => b === 0); + if (!isFinalized) { + throw httpErrors.badRequest(`Round ${roundId} is not yet finalized. Wait for the round to complete.`); + } + + const view = new DataView(round.slotHash.buffer, round.slotHash.byteOffset, 32); + const r1 = view.getBigUint64(0, true); + const r2 = view.getBigUint64(8, true); + const r3 = view.getBigUint64(16, true); + const r4 = view.getBigUint64(24, true); + const rng = r1 ^ r2 ^ r3 ^ r4; + const winningSquareIndex = Number(rng % 25n); // 0-indexed internally + const winningSquare = winningSquareIndex + 1; // 1-indexed for API response + + // Get miner's deployed squares and total + const deployedSquares: number[] = []; + let totalDeployedLamports = 0n; + let deployedToWinningSquare = false; + let deployedToWinningSquareLamports = 0n; + + for (let i = 0; i < 25; i++) { + if (miner.deployed[i] > 0n) { + deployedSquares.push(i + 1); // 1-indexed for API response + totalDeployedLamports += miner.deployed[i]; + if (i === winningSquareIndex) { + deployedToWinningSquare = true; + deployedToWinningSquareLamports = miner.deployed[i]; + } + } + } + + // Capture rewards before checkpoint + const rewardsSolBefore = miner.rewardsSol; + const rewardsOreBefore = miner.rewardsOre; + + // Create checkpoint instruction + const signerPubkey = isHardwareWallet ? (wallet as PublicKey) : (wallet as any).publicKey; + const checkpointIx = createCheckpointInstruction(signerPubkey, roundId); + + // Build transaction + const solana = ore.solana; + const { blockhash } = await solana.connection.getLatestBlockhash('confirmed'); + + const messageV0 = new TransactionMessage({ + payerKey: signerPubkey, + recentBlockhash: blockhash, + instructions: [checkpointIx], + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + logger.info(`Creating checkpoint for round ${roundId}`); + + // Sign and send + const signature = await ore.signAndSendTransaction(transaction, walletAddress, isHardwareWallet); + + // Get updated miner account to see new rewards + const minerAfter = await ore.getMinerAccount(walletAddress); + const rewardsSolAfter = minerAfter ? minerAfter.rewardsSol : rewardsSolBefore; + const rewardsOreAfter = minerAfter ? minerAfter.rewardsOre : rewardsOreBefore; + + // Calculate winnings from this checkpoint + const wonSolLamports = rewardsSolAfter - rewardsSolBefore; + const wonOreRaw = rewardsOreAfter - rewardsOreBefore; + + return { + signature, + roundId: Number(roundId), + winningSquare, + deployedSquares, + deployedSol: Number(totalDeployedLamports) / 1_000_000_000, + won: deployedToWinningSquare, + wonSol: Number(wonSolLamports) / 1_000_000_000, + wonOre: Number(wonOreRaw) / 10 ** ORE_DECIMALS, + }; +} + +export const checkpointRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: OreCheckpointRequestType; + Reply: OreCheckpointResponseType; + }>( + '/check-round', + { + schema: { + description: 'Settle miner rewards for a completed round and return results', + tags: ['/connector/ore'], + body: OreCheckpointRequest, + response: { + 200: OreCheckpointResponse, + }, + }, + }, + async (request) => { + try { + const network = request.body.network || 'mainnet-beta'; + const walletAddress = request.body.walletAddress; + const { roundId } = request.body; + + if (!walletAddress) { + throw httpErrors.badRequest('walletAddress is required'); + } + + return await checkpoint(network, walletAddress, roundId); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default checkpointRoute; diff --git a/src/connectors/ore/ore-routes/claimOre.ts b/src/connectors/ore/ore-routes/claimOre.ts new file mode 100644 index 0000000000..095e8f2850 --- /dev/null +++ b/src/connectors/ore/ore-routes/claimOre.ts @@ -0,0 +1,102 @@ +import { PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { createClaimOreInstruction } from '../ore.instructions'; +import { + OreClaimOreRequest, + OreClaimOreRequestType, + OreTransactionResponse, + OreTransactionResponseType, +} from '../schemas'; + +export async function claimOre(network: string, walletAddress: string): Promise { + // Validate wallet address + try { + new PublicKey(walletAddress); + } catch { + throw httpErrors.badRequest(`Invalid wallet address: ${walletAddress}`); + } + + const ore = await Ore.getInstance(network); + const { wallet, isHardwareWallet } = await ore.prepareWallet(walletAddress); + + // Verify miner account exists + const miner = await ore.getMinerAccount(walletAddress); + if (!miner) { + throw httpErrors.notFound(`Miner account not found for wallet: ${walletAddress}`); + } + + // Check if there are ORE rewards to claim + if (miner.rewardsOre <= 0n) { + throw httpErrors.badRequest('No ORE rewards available to claim'); + } + + // Create claim ORE instruction + const signerPubkey = isHardwareWallet ? (wallet as PublicKey) : (wallet as any).publicKey; + const claimOreIx = createClaimOreInstruction(signerPubkey); + + // Build transaction + const solana = ore.solana; + const { blockhash } = await solana.connection.getLatestBlockhash('confirmed'); + + const messageV0 = new TransactionMessage({ + payerKey: signerPubkey, + recentBlockhash: blockhash, + instructions: [claimOreIx], + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + const rewardsOre = miner.rewardsOre; + logger.info(`Claiming ${rewardsOre} ORE token rewards`); + + // Sign and send + const signature = await ore.signAndSendTransaction(transaction, walletAddress, isHardwareWallet); + + return { + signature, + message: `Claimed ${rewardsOre} ORE token rewards`, + }; +} + +export const claimOreRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: OreClaimOreRequestType; + Reply: OreTransactionResponseType; + }>( + '/claim-ore', + { + schema: { + description: 'Claim ORE token rewards from mining', + tags: ['/connector/ore'], + body: OreClaimOreRequest, + response: { + 200: OreTransactionResponse, + }, + }, + }, + async (request) => { + try { + const network = request.body.network || 'mainnet-beta'; + const walletAddress = request.body.walletAddress; + + if (!walletAddress) { + throw httpErrors.badRequest('walletAddress is required'); + } + + return await claimOre(network, walletAddress); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default claimOreRoute; diff --git a/src/connectors/ore/ore-routes/claimSol.ts b/src/connectors/ore/ore-routes/claimSol.ts new file mode 100644 index 0000000000..5e07ec36f2 --- /dev/null +++ b/src/connectors/ore/ore-routes/claimSol.ts @@ -0,0 +1,102 @@ +import { PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { createClaimSolInstruction } from '../ore.instructions'; +import { + OreClaimSolRequest, + OreClaimSolRequestType, + OreTransactionResponse, + OreTransactionResponseType, +} from '../schemas'; + +export async function claimSol(network: string, walletAddress: string): Promise { + // Validate wallet address + try { + new PublicKey(walletAddress); + } catch { + throw httpErrors.badRequest(`Invalid wallet address: ${walletAddress}`); + } + + const ore = await Ore.getInstance(network); + const { wallet, isHardwareWallet } = await ore.prepareWallet(walletAddress); + + // Verify miner account exists + const miner = await ore.getMinerAccount(walletAddress); + if (!miner) { + throw httpErrors.notFound(`Miner account not found for wallet: ${walletAddress}`); + } + + // Check if there are SOL rewards to claim + if (miner.rewardsSol <= 0n) { + throw httpErrors.badRequest('No SOL rewards available to claim'); + } + + // Create claim SOL instruction + const signerPubkey = isHardwareWallet ? (wallet as PublicKey) : (wallet as any).publicKey; + const claimSolIx = createClaimSolInstruction(signerPubkey); + + // Build transaction + const solana = ore.solana; + const { blockhash } = await solana.connection.getLatestBlockhash('confirmed'); + + const messageV0 = new TransactionMessage({ + payerKey: signerPubkey, + recentBlockhash: blockhash, + instructions: [claimSolIx], + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + const rewardsLamports = miner.rewardsSol; + logger.info(`Claiming ${rewardsLamports} lamports in SOL rewards`); + + // Sign and send + const signature = await ore.signAndSendTransaction(transaction, walletAddress, isHardwareWallet); + + return { + signature, + message: `Claimed ${rewardsLamports} lamports in SOL rewards`, + }; +} + +export const claimSolRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: OreClaimSolRequestType; + Reply: OreTransactionResponseType; + }>( + '/claim-sol', + { + schema: { + description: 'Claim SOL rewards from mining', + tags: ['/connector/ore'], + body: OreClaimSolRequest, + response: { + 200: OreTransactionResponse, + }, + }, + }, + async (request) => { + try { + const network = request.body.network || 'mainnet-beta'; + const walletAddress = request.body.walletAddress; + + if (!walletAddress) { + throw httpErrors.badRequest('walletAddress is required'); + } + + return await claimSol(network, walletAddress); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default claimSolRoute; diff --git a/src/connectors/ore/ore-routes/claimStake.ts b/src/connectors/ore/ore-routes/claimStake.ts new file mode 100644 index 0000000000..e7feb0a421 --- /dev/null +++ b/src/connectors/ore/ore-routes/claimStake.ts @@ -0,0 +1,122 @@ +import { PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { createClaimYieldInstruction } from '../ore.instructions'; +import { + OreClaimStakeRequest, + OreClaimStakeRequestType, + OreTransactionResponse, + OreTransactionResponseType, +} from '../schemas'; + +const ORE_DECIMALS = 11; // ORE token has 11 decimals + +export async function claimStake( + network: string, + walletAddress: string, + amount?: number, +): Promise { + // Validate wallet address + try { + new PublicKey(walletAddress); + } catch { + throw httpErrors.badRequest(`Invalid wallet address: ${walletAddress}`); + } + + const ore = await Ore.getInstance(network); + const { wallet, isHardwareWallet } = await ore.prepareWallet(walletAddress); + + // Verify stake account exists and has rewards + const stakeAccount = await ore.getStakeAccount(walletAddress); + if (!stakeAccount) { + throw httpErrors.notFound(`Stake account not found for wallet: ${walletAddress}`); + } + + if (stakeAccount.rewards <= 0n) { + throw httpErrors.badRequest('No staking rewards available to claim'); + } + + // Determine claim amount + let claimAmount: bigint; + if (amount !== undefined && amount > 0) { + claimAmount = BigInt(Math.floor(amount * 10 ** ORE_DECIMALS)); + if (claimAmount > stakeAccount.rewards) { + throw httpErrors.badRequest( + `Requested amount exceeds available rewards. Available: ${Number(stakeAccount.rewards) / 10 ** ORE_DECIMALS} ORE`, + ); + } + } else { + // Claim all available rewards + claimAmount = stakeAccount.rewards; + } + + // Create claim yield instruction + const signerPubkey = isHardwareWallet ? (wallet as PublicKey) : (wallet as any).publicKey; + const claimYieldIx = createClaimYieldInstruction(signerPubkey, claimAmount); + + // Build transaction + const solana = ore.solana; + const { blockhash } = await solana.connection.getLatestBlockhash('confirmed'); + + const messageV0 = new TransactionMessage({ + payerKey: signerPubkey, + recentBlockhash: blockhash, + instructions: [claimYieldIx], + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + const humanAmount = Number(claimAmount) / 10 ** ORE_DECIMALS; + logger.info(`Claiming ${humanAmount} ORE staking rewards`); + + // Sign and send + const signature = await ore.signAndSendTransaction(transaction, walletAddress, isHardwareWallet); + + return { + signature, + message: `Claimed ${humanAmount} ORE in staking rewards`, + }; +} + +export const claimStakeRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: OreClaimStakeRequestType; + Reply: OreTransactionResponseType; + }>( + '/claim-stake', + { + schema: { + description: 'Claim staking rewards', + tags: ['/connector/ore'], + body: OreClaimStakeRequest, + response: { + 200: OreTransactionResponse, + }, + }, + }, + async (request) => { + try { + const network = request.body.network || 'mainnet-beta'; + const walletAddress = request.body.walletAddress; + const { amount } = request.body; + + if (!walletAddress) { + throw httpErrors.badRequest('walletAddress is required'); + } + + return await claimStake(network, walletAddress, amount); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default claimStakeRoute; diff --git a/src/connectors/ore/ore-routes/deploy.ts b/src/connectors/ore/ore-routes/deploy.ts new file mode 100644 index 0000000000..6345d72571 --- /dev/null +++ b/src/connectors/ore/ore-routes/deploy.ts @@ -0,0 +1,139 @@ +import { PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { createDeployInstruction } from '../ore.instructions'; +import { squaresToBitmask } from '../ore.parser'; +import { OreDeployRequest, OreDeployRequestType, OreTransactionResponse, OreTransactionResponseType } from '../schemas'; + +import { checkpoint } from './checkpoint'; + +const LAMPORTS_PER_SOL = 1_000_000_000; + +export async function deploy( + network: string, + walletAddress: string, + amountSol: number, + squares: number[], +): Promise { + // Validate wallet address + try { + new PublicKey(walletAddress); + } catch { + throw httpErrors.badRequest(`Invalid wallet address: ${walletAddress}`); + } + + // Validate amount + if (amountSol <= 0) { + throw httpErrors.badRequest('Amount must be greater than 0'); + } + + // Validate square indices (1-25) and convert to 0-indexed + const squaresInternal: number[] = []; + for (const sq of squares) { + if (sq < 1 || sq > 25) { + throw httpErrors.badRequest(`Invalid square index: ${sq}. Must be 1-25`); + } + squaresInternal.push(sq - 1); // Convert to 0-indexed for internal use + } + const squaresBitmask = squaresToBitmask(squaresInternal); + + if (squaresBitmask === 0) { + throw httpErrors.badRequest('At least one square must be selected'); + } + + const ore = await Ore.getInstance(network); + const { wallet, isHardwareWallet } = await ore.prepareWallet(walletAddress); + + // Get current board to determine round ID + const board = await ore.getBoardAccount(); + const currentRoundId = board.roundId; + + // Check if miner needs to checkpoint first + const miner = await ore.getMinerAccount(walletAddress); + if (miner && miner.roundId < currentRoundId) { + // Miner has pending rewards from a previous round - checkpoint first + logger.info(`Miner needs checkpoint for round ${miner.roundId}, running checkpoint first...`); + await checkpoint(network, walletAddress, miner.roundId.toString()); + } + + // Get config for entropy var address + const config = await ore.getConfigAccount(); + + // Convert SOL to lamports + const amountLamports = BigInt(Math.floor(amountSol * LAMPORTS_PER_SOL)); + + // Create deploy instruction + const signerPubkey = isHardwareWallet ? (wallet as PublicKey) : (wallet as any).publicKey; + const deployIx = createDeployInstruction( + signerPubkey, + amountLamports, + squaresBitmask, + currentRoundId, + config.varAddress, + ); + + // Build transaction + const solana = ore.solana; + const { blockhash } = await solana.connection.getLatestBlockhash('confirmed'); + + const messageV0 = new TransactionMessage({ + payerKey: signerPubkey, + recentBlockhash: blockhash, + instructions: [deployIx], + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + logger.info(`Deploying ${amountSol} SOL to squares (bitmask: ${squaresBitmask}) for round ${currentRoundId}`); + + // Sign and send + const signature = await ore.signAndSendTransaction(transaction, walletAddress, isHardwareWallet); + + return { + signature, + message: `Deployed ${amountSol} SOL to ${squares.length} square(s)`, + }; +} + +export const deployRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: OreDeployRequestType; + Reply: OreTransactionResponseType; + }>( + '/deploy', + { + schema: { + description: 'Deploy SOL to squares in the current ORE round', + tags: ['/connector/ore'], + body: OreDeployRequest, + response: { + 200: OreTransactionResponse, + }, + }, + }, + async (request) => { + try { + const network = request.body.network || 'mainnet-beta'; + const walletAddress = request.body.walletAddress; + const { amount, squares } = request.body; + + if (!walletAddress) { + throw httpErrors.badRequest('walletAddress is required'); + } + + return await deploy(network, walletAddress, amount, squares); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default deployRoute; diff --git a/src/connectors/ore/ore-routes/index.ts b/src/connectors/ore/ore-routes/index.ts new file mode 100644 index 0000000000..2a77585b2c --- /dev/null +++ b/src/connectors/ore/ore-routes/index.ts @@ -0,0 +1,37 @@ +import { FastifyPluginAsync } from 'fastify'; + +// GET routes - Info endpoints +import { accountInfoRoute } from './accountInfo'; +import { boardInfoRoute } from './boardInfo'; + +// POST routes - Mining operations +import { checkpointRoute } from './checkpoint'; +import { claimOreRoute } from './claimOre'; +import { claimSolRoute } from './claimSol'; + +// POST routes - Staking operations +import { claimStakeRoute } from './claimStake'; +import { deployRoute } from './deploy'; +import { stakeRoute } from './stake'; +import { systemInfoRoute } from './systemInfo'; +import { unstakeRoute } from './unstake'; + +export const oreRoutes: FastifyPluginAsync = async (fastify) => { + // GET routes - Info endpoints + await fastify.register(accountInfoRoute); + await fastify.register(boardInfoRoute); + await fastify.register(systemInfoRoute); + + // POST routes - Mining operations + await fastify.register(deployRoute); + await fastify.register(checkpointRoute); + await fastify.register(claimSolRoute); + await fastify.register(claimOreRoute); + + // POST routes - Staking operations + await fastify.register(stakeRoute); + await fastify.register(unstakeRoute); + await fastify.register(claimStakeRoute); +}; + +export default oreRoutes; diff --git a/src/connectors/ore/ore-routes/stake.ts b/src/connectors/ore/ore-routes/stake.ts new file mode 100644 index 0000000000..3af0f85d8f --- /dev/null +++ b/src/connectors/ore/ore-routes/stake.ts @@ -0,0 +1,100 @@ +import { PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { createDepositInstruction } from '../ore.instructions'; +import { OreStakeRequest, OreStakeRequestType, OreTransactionResponse, OreTransactionResponseType } from '../schemas'; + +const ORE_DECIMALS = 11; // ORE token has 11 decimals + +export async function stake( + network: string, + walletAddress: string, + amount: number, +): Promise { + // Validate wallet address + try { + new PublicKey(walletAddress); + } catch { + throw httpErrors.badRequest(`Invalid wallet address: ${walletAddress}`); + } + + // Validate amount + if (amount <= 0) { + throw httpErrors.badRequest('Amount must be greater than 0'); + } + + const ore = await Ore.getInstance(network); + const { wallet, isHardwareWallet } = await ore.prepareWallet(walletAddress); + + // Convert amount to raw units (ORE has 11 decimals) + const rawAmount = BigInt(Math.floor(amount * 10 ** ORE_DECIMALS)); + + // Create deposit instruction + const signerPubkey = isHardwareWallet ? (wallet as PublicKey) : (wallet as any).publicKey; + const depositIx = createDepositInstruction(signerPubkey, rawAmount); + + // Build transaction + const solana = ore.solana; + const { blockhash } = await solana.connection.getLatestBlockhash('confirmed'); + + const messageV0 = new TransactionMessage({ + payerKey: signerPubkey, + recentBlockhash: blockhash, + instructions: [depositIx], + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + logger.info(`Staking ${amount} ORE`); + + // Sign and send + const signature = await ore.signAndSendTransaction(transaction, walletAddress, isHardwareWallet); + + return { + signature, + message: `Staked ${amount} ORE`, + }; +} + +export const stakeRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: OreStakeRequestType; + Reply: OreTransactionResponseType; + }>( + '/stake', + { + schema: { + description: 'Stake ORE tokens', + tags: ['/connector/ore'], + body: OreStakeRequest, + response: { + 200: OreTransactionResponse, + }, + }, + }, + async (request) => { + try { + const network = request.body.network || 'mainnet-beta'; + const walletAddress = request.body.walletAddress; + const { amount } = request.body; + + if (!walletAddress) { + throw httpErrors.badRequest('walletAddress is required'); + } + + return await stake(network, walletAddress, amount); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default stakeRoute; diff --git a/src/connectors/ore/ore-routes/systemInfo.ts b/src/connectors/ore/ore-routes/systemInfo.ts new file mode 100644 index 0000000000..e856938020 --- /dev/null +++ b/src/connectors/ore/ore-routes/systemInfo.ts @@ -0,0 +1,45 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { + OreSystemInfoRequest, + OreSystemInfoRequestType, + OreSystemInfoResponse, + OreSystemInfoResponseType, +} from '../schemas'; + +export const systemInfoRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: OreSystemInfoRequestType; + Reply: OreSystemInfoResponseType; + }>( + '/system-info', + { + schema: { + description: 'Get ORE system information (treasury and config)', + tags: ['/connector/ore'], + querystring: OreSystemInfoRequest, + response: { + 200: OreSystemInfoResponse, + }, + }, + }, + async (request) => { + try { + const network = request.query.network || 'mainnet-beta'; + const ore = await Ore.getInstance(network); + return await ore.getSystemInfo(); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default systemInfoRoute; diff --git a/src/connectors/ore/ore-routes/unstake.ts b/src/connectors/ore/ore-routes/unstake.ts new file mode 100644 index 0000000000..539df5e993 --- /dev/null +++ b/src/connectors/ore/ore-routes/unstake.ts @@ -0,0 +1,117 @@ +import { PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; +import { FastifyPluginAsync } from 'fastify'; + +import { httpErrors } from '../../../services/error-handler'; +import { logger } from '../../../services/logger'; +import { Ore } from '../ore'; +import { createWithdrawInstruction } from '../ore.instructions'; +import { + OreUnstakeRequest, + OreUnstakeRequestType, + OreTransactionResponse, + OreTransactionResponseType, +} from '../schemas'; + +const ORE_DECIMALS = 11; // ORE token has 11 decimals + +export async function unstake( + network: string, + walletAddress: string, + amount: number, +): Promise { + // Validate wallet address + try { + new PublicKey(walletAddress); + } catch { + throw httpErrors.badRequest(`Invalid wallet address: ${walletAddress}`); + } + + // Validate amount + if (amount <= 0) { + throw httpErrors.badRequest('Amount must be greater than 0'); + } + + const ore = await Ore.getInstance(network); + const { wallet, isHardwareWallet } = await ore.prepareWallet(walletAddress); + + // Verify stake account exists and has sufficient balance + const stakeAccount = await ore.getStakeAccount(walletAddress); + if (!stakeAccount) { + throw httpErrors.notFound(`Stake account not found for wallet: ${walletAddress}`); + } + + // Convert amount to raw units (ORE has 11 decimals) + const rawAmount = BigInt(Math.floor(amount * 10 ** ORE_DECIMALS)); + + if (stakeAccount.balance < rawAmount) { + throw httpErrors.badRequest( + `Insufficient staked balance. Available: ${Number(stakeAccount.balance) / 10 ** ORE_DECIMALS} ORE`, + ); + } + + // Create withdraw instruction + const signerPubkey = isHardwareWallet ? (wallet as PublicKey) : (wallet as any).publicKey; + const withdrawIx = createWithdrawInstruction(signerPubkey, rawAmount); + + // Build transaction + const solana = ore.solana; + const { blockhash } = await solana.connection.getLatestBlockhash('confirmed'); + + const messageV0 = new TransactionMessage({ + payerKey: signerPubkey, + recentBlockhash: blockhash, + instructions: [withdrawIx], + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + logger.info(`Unstaking ${amount} ORE`); + + // Sign and send + const signature = await ore.signAndSendTransaction(transaction, walletAddress, isHardwareWallet); + + return { + signature, + message: `Unstaked ${amount} ORE`, + }; +} + +export const unstakeRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: OreUnstakeRequestType; + Reply: OreTransactionResponseType; + }>( + '/unstake', + { + schema: { + description: 'Unstake ORE tokens', + tags: ['/connector/ore'], + body: OreUnstakeRequest, + response: { + 200: OreTransactionResponse, + }, + }, + }, + async (request) => { + try { + const network = request.body.network || 'mainnet-beta'; + const walletAddress = request.body.walletAddress; + const { amount } = request.body; + + if (!walletAddress) { + throw httpErrors.badRequest('walletAddress is required'); + } + + return await unstake(network, walletAddress, amount); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw httpErrors.internalServerError('Internal server error'); + } + }, + ); +}; + +export default unstakeRoute; diff --git a/src/connectors/ore/ore.config.ts b/src/connectors/ore/ore.config.ts new file mode 100644 index 0000000000..dc99b64c48 --- /dev/null +++ b/src/connectors/ore/ore.config.ts @@ -0,0 +1,110 @@ +import { PublicKey } from '@solana/web3.js'; + +import { AvailableNetworks } from '../../services/base'; + +export namespace OreConfig { + // Program IDs + export const ORE_PROGRAM_ID = new PublicKey('oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv'); + export const ORE_TOKEN_MINT = new PublicKey('oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp'); + export const ENTROPY_PROGRAM_ID = new PublicKey('3jSkUuYBoJzQPMEzTvkDFXCZUBksPamrVhrnHR9igu2X'); + + // Token program IDs + export const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + export const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); + export const SYSTEM_PROGRAM_ID = new PublicKey('11111111111111111111111111111111'); + + // Instruction discriminators (single u8 values for Steel framework) + export const DISCRIMINATORS = { + automate: 0, + checkpoint: 2, + claimSol: 3, + claimOre: 4, + close: 5, + deploy: 6, + log: 8, + reset: 9, + deposit: 10, + withdraw: 11, + claimYield: 12, + bury: 13, + wrap: 14, + setAdmin: 15, + setFeeCollector: 16, + newVar: 17, + setBuffer: 18, + } as const; + + // Account discriminators (first 8 bytes) + export const ACCOUNT_DISCRIMINATORS = { + Automation: [100, 0, 0, 0, 0, 0, 0, 0], + Config: [101, 0, 0, 0, 0, 0, 0, 0], + Miner: [103, 0, 0, 0, 0, 0, 0, 0], + Treasury: [104, 0, 0, 0, 0, 0, 0, 0], + Board: [105, 0, 0, 0, 0, 0, 0, 0], + Stake: [108, 0, 0, 0, 0, 0, 0, 0], + Round: [109, 0, 0, 0, 0, 0, 0, 0], + } as const; + + // PDA seeds + export const PDA_SEEDS = { + automation: 'automation', + board: 'board', + config: 'config', + miner: 'miner', + round: 'round', + stake: 'stake', + treasury: 'treasury', + } as const; + + // Supported networks (ORE v3 is only on mainnet-beta) + export const chain = 'solana'; + export const networks = ['mainnet-beta'] as const; + export type Network = (typeof networks)[number]; + + // Trading types (ore is a new trading type for mining game) + export const tradingTypes = ['ore'] as const; + + export interface RootConfig { + availableNetworks: Array; + } + + export const config: RootConfig = { + availableNetworks: [ + { + chain, + networks: [...networks], + }, + ], + }; + + // Helper to derive PDAs + export function getBoardPDA(): [PublicKey, number] { + return PublicKey.findProgramAddressSync([Buffer.from(PDA_SEEDS.board)], ORE_PROGRAM_ID); + } + + export function getConfigPDA(): [PublicKey, number] { + return PublicKey.findProgramAddressSync([Buffer.from(PDA_SEEDS.config)], ORE_PROGRAM_ID); + } + + export function getTreasuryPDA(): [PublicKey, number] { + return PublicKey.findProgramAddressSync([Buffer.from(PDA_SEEDS.treasury)], ORE_PROGRAM_ID); + } + + export function getMinerPDA(authority: PublicKey): [PublicKey, number] { + return PublicKey.findProgramAddressSync([Buffer.from(PDA_SEEDS.miner), authority.toBuffer()], ORE_PROGRAM_ID); + } + + export function getStakePDA(authority: PublicKey): [PublicKey, number] { + return PublicKey.findProgramAddressSync([Buffer.from(PDA_SEEDS.stake), authority.toBuffer()], ORE_PROGRAM_ID); + } + + export function getAutomationPDA(authority: PublicKey): [PublicKey, number] { + return PublicKey.findProgramAddressSync([Buffer.from(PDA_SEEDS.automation), authority.toBuffer()], ORE_PROGRAM_ID); + } + + export function getRoundPDA(roundId: bigint): [PublicKey, number] { + const roundIdBuffer = Buffer.alloc(8); + roundIdBuffer.writeBigUInt64LE(roundId); + return PublicKey.findProgramAddressSync([Buffer.from(PDA_SEEDS.round), roundIdBuffer], ORE_PROGRAM_ID); + } +} diff --git a/src/connectors/ore/ore.instructions.ts b/src/connectors/ore/ore.instructions.ts new file mode 100644 index 0000000000..7c9134ca63 --- /dev/null +++ b/src/connectors/ore/ore.instructions.ts @@ -0,0 +1,310 @@ +import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { PublicKey, TransactionInstruction, SystemProgram, SYSVAR_SLOT_HASHES_PUBKEY } from '@solana/web3.js'; + +import { OreConfig } from './ore.config'; + +/** + * Instruction builders for ORE program. + * ORE uses Steel framework with single u8 discriminators (NOT Anchor 8-byte discriminators). + * Instruction data format: [discriminator (1 byte)] [args...] + * All integers are little-endian. + */ + +// ============================================================================ +// Instruction Data Builders +// ============================================================================ + +/** + * Build deploy instruction data + * Args: amount (u64), squares (u32) + */ +function buildDeployData(amountLamports: bigint, squaresBitmask: number): Buffer { + const buffer = Buffer.alloc(1 + 8 + 4); + buffer.writeUInt8(OreConfig.DISCRIMINATORS.deploy, 0); + buffer.writeBigUInt64LE(amountLamports, 1); + buffer.writeUInt32LE(squaresBitmask, 9); + return buffer; +} + +/** + * Build checkpoint instruction data + * Args: none + */ +function buildCheckpointData(): Buffer { + const buffer = Buffer.alloc(1); + buffer.writeUInt8(OreConfig.DISCRIMINATORS.checkpoint, 0); + return buffer; +} + +/** + * Build claimSol instruction data + * Args: none + */ +function buildClaimSolData(): Buffer { + const buffer = Buffer.alloc(1); + buffer.writeUInt8(OreConfig.DISCRIMINATORS.claimSol, 0); + return buffer; +} + +/** + * Build claimOre instruction data + * Args: none + */ +function buildClaimOreData(): Buffer { + const buffer = Buffer.alloc(1); + buffer.writeUInt8(OreConfig.DISCRIMINATORS.claimOre, 0); + return buffer; +} + +/** + * Build deposit (stake) instruction data + * Args: amount (u64) + */ +function buildDepositData(amount: bigint): Buffer { + const buffer = Buffer.alloc(1 + 8); + buffer.writeUInt8(OreConfig.DISCRIMINATORS.deposit, 0); + buffer.writeBigUInt64LE(amount, 1); + return buffer; +} + +/** + * Build withdraw (unstake) instruction data + * Args: amount (u64) + */ +function buildWithdrawData(amount: bigint): Buffer { + const buffer = Buffer.alloc(1 + 8); + buffer.writeUInt8(OreConfig.DISCRIMINATORS.withdraw, 0); + buffer.writeBigUInt64LE(amount, 1); + return buffer; +} + +/** + * Build claimYield instruction data + * Args: amount (u64) + */ +function buildClaimYieldData(amount: bigint): Buffer { + const buffer = Buffer.alloc(1 + 8); + buffer.writeUInt8(OreConfig.DISCRIMINATORS.claimYield, 0); + buffer.writeBigUInt64LE(amount, 1); + return buffer; +} + +// ============================================================================ +// Instruction Builders +// ============================================================================ + +/** + * Create deploy instruction + * Deploys SOL to selected squares for the current round. + */ +export function createDeployInstruction( + signer: PublicKey, + amountLamports: bigint, + squaresBitmask: number, + currentRoundId: bigint, + entropyVarAddress: PublicKey, +): TransactionInstruction { + const [automation] = OreConfig.getAutomationPDA(signer); + const [board] = OreConfig.getBoardPDA(); + const [config] = OreConfig.getConfigPDA(); + const [miner] = OreConfig.getMinerPDA(signer); + const [round] = OreConfig.getRoundPDA(currentRoundId); + + const keys = [ + { pubkey: signer, isSigner: true, isWritable: true }, + { pubkey: signer, isSigner: false, isWritable: false }, // authority (read-only) + { pubkey: automation, isSigner: false, isWritable: true }, + { pubkey: board, isSigner: false, isWritable: true }, + { pubkey: config, isSigner: false, isWritable: true }, + { pubkey: miner, isSigner: false, isWritable: true }, + { pubkey: round, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: OreConfig.ORE_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: entropyVarAddress, isSigner: false, isWritable: true }, + { pubkey: OreConfig.ENTROPY_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + + return new TransactionInstruction({ + keys, + programId: OreConfig.ORE_PROGRAM_ID, + data: buildDeployData(amountLamports, squaresBitmask), + }); +} + +/** + * Create checkpoint instruction + * Settles miner rewards for a completed round. + */ +export function createCheckpointInstruction(signer: PublicKey, completedRoundId: bigint): TransactionInstruction { + const [board] = OreConfig.getBoardPDA(); + const [miner] = OreConfig.getMinerPDA(signer); + const [round] = OreConfig.getRoundPDA(completedRoundId); + const [treasury] = OreConfig.getTreasuryPDA(); + + const keys = [ + { pubkey: signer, isSigner: true, isWritable: true }, + { pubkey: board, isSigner: false, isWritable: false }, + { pubkey: miner, isSigner: false, isWritable: true }, + { pubkey: round, isSigner: false, isWritable: true }, + { pubkey: treasury, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ]; + + return new TransactionInstruction({ + keys, + programId: OreConfig.ORE_PROGRAM_ID, + data: buildCheckpointData(), + }); +} + +/** + * Create claimSol instruction + * Claims SOL rewards from the miner account. + */ +export function createClaimSolInstruction(signer: PublicKey): TransactionInstruction { + const [miner] = OreConfig.getMinerPDA(signer); + + const keys = [ + { pubkey: signer, isSigner: true, isWritable: true }, + { pubkey: miner, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ]; + + return new TransactionInstruction({ + keys, + programId: OreConfig.ORE_PROGRAM_ID, + data: buildClaimSolData(), + }); +} + +/** + * Create claimOre instruction + * Claims ORE token rewards from the treasury vault. + */ +export function createClaimOreInstruction(signer: PublicKey): TransactionInstruction { + const [miner] = OreConfig.getMinerPDA(signer); + const [treasury] = OreConfig.getTreasuryPDA(); + + // Get treasury's ORE token account + const treasuryTokens = getAssociatedTokenAddressSync(OreConfig.ORE_TOKEN_MINT, treasury, true); + + // Get signer's ORE token account (recipient) + const recipient = getAssociatedTokenAddressSync(OreConfig.ORE_TOKEN_MINT, signer); + + const keys = [ + { pubkey: signer, isSigner: true, isWritable: true }, + { pubkey: miner, isSigner: false, isWritable: true }, + { pubkey: OreConfig.ORE_TOKEN_MINT, isSigner: false, isWritable: false }, + { pubkey: recipient, isSigner: false, isWritable: true }, + { pubkey: treasury, isSigner: false, isWritable: true }, + { pubkey: treasuryTokens, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + + return new TransactionInstruction({ + keys, + programId: OreConfig.ORE_PROGRAM_ID, + data: buildClaimOreData(), + }); +} + +/** + * Create deposit (stake) instruction + * Deposits ORE into a staking account. + */ +export function createDepositInstruction(signer: PublicKey, amount: bigint): TransactionInstruction { + const [stake] = OreConfig.getStakePDA(signer); + const [treasury] = OreConfig.getTreasuryPDA(); + + // Get signer's ORE token account (sender) + const sender = getAssociatedTokenAddressSync(OreConfig.ORE_TOKEN_MINT, signer); + + // Get stake's ORE token account + const stakeTokens = getAssociatedTokenAddressSync(OreConfig.ORE_TOKEN_MINT, stake, true); + + const keys = [ + { pubkey: signer, isSigner: true, isWritable: true }, + { pubkey: OreConfig.ORE_TOKEN_MINT, isSigner: false, isWritable: false }, + { pubkey: sender, isSigner: false, isWritable: true }, + { pubkey: stake, isSigner: false, isWritable: true }, + { pubkey: stakeTokens, isSigner: false, isWritable: true }, + { pubkey: treasury, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + + return new TransactionInstruction({ + keys, + programId: OreConfig.ORE_PROGRAM_ID, + data: buildDepositData(amount), + }); +} + +/** + * Create withdraw (unstake) instruction + * Withdraws ORE from a staking account. + */ +export function createWithdrawInstruction(signer: PublicKey, amount: bigint): TransactionInstruction { + const [stake] = OreConfig.getStakePDA(signer); + const [treasury] = OreConfig.getTreasuryPDA(); + + // Get signer's ORE token account (recipient) + const recipient = getAssociatedTokenAddressSync(OreConfig.ORE_TOKEN_MINT, signer); + + // Get stake's ORE token account + const stakeTokens = getAssociatedTokenAddressSync(OreConfig.ORE_TOKEN_MINT, stake, true); + + const keys = [ + { pubkey: signer, isSigner: true, isWritable: true }, + { pubkey: OreConfig.ORE_TOKEN_MINT, isSigner: false, isWritable: false }, + { pubkey: recipient, isSigner: false, isWritable: true }, + { pubkey: stake, isSigner: false, isWritable: true }, + { pubkey: stakeTokens, isSigner: false, isWritable: true }, + { pubkey: treasury, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + + return new TransactionInstruction({ + keys, + programId: OreConfig.ORE_PROGRAM_ID, + data: buildWithdrawData(amount), + }); +} + +/** + * Create claimYield instruction + * Claims accrued staking rewards. + */ +export function createClaimYieldInstruction(signer: PublicKey, amount: bigint): TransactionInstruction { + const [stake] = OreConfig.getStakePDA(signer); + const [treasury] = OreConfig.getTreasuryPDA(); + + // Get signer's ORE token account (recipient) + const recipient = getAssociatedTokenAddressSync(OreConfig.ORE_TOKEN_MINT, signer); + + // Get treasury's ORE token account + const treasuryTokens = getAssociatedTokenAddressSync(OreConfig.ORE_TOKEN_MINT, treasury, true); + + const keys = [ + { pubkey: signer, isSigner: true, isWritable: true }, + { pubkey: OreConfig.ORE_TOKEN_MINT, isSigner: false, isWritable: false }, + { pubkey: recipient, isSigner: false, isWritable: true }, + { pubkey: stake, isSigner: false, isWritable: true }, + { pubkey: treasury, isSigner: false, isWritable: true }, + { pubkey: treasuryTokens, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + + return new TransactionInstruction({ + keys, + programId: OreConfig.ORE_PROGRAM_ID, + data: buildClaimYieldData(amount), + }); +} diff --git a/src/connectors/ore/ore.parser.ts b/src/connectors/ore/ore.parser.ts new file mode 100644 index 0000000000..04a24aad9a --- /dev/null +++ b/src/connectors/ore/ore.parser.ts @@ -0,0 +1,554 @@ +import { PublicKey } from '@solana/web3.js'; + +import { OreConfig } from './ore.config'; + +/** + * Account data structures parsed from on-chain data. + * ORE uses Steel framework with 8-byte discriminators followed by struct fields. + * All integers are little-endian. + */ + +// ============================================================================ +// Parsed Account Types +// ============================================================================ + +export interface BoardAccount { + roundId: bigint; + startSlot: bigint; + endSlot: bigint; + epochId: bigint; +} + +export interface ConfigAccount { + admin: PublicKey; + buryAuthority: PublicKey; + feeCollector: PublicKey; + swapProgram: PublicKey; + varAddress: PublicKey; + buffer: bigint; +} + +export interface MinerAccount { + authority: PublicKey; + deployed: bigint[]; // 25 u64 values + cumulative: bigint[]; // 25 u64 values + checkpointFee: bigint; + checkpointId: bigint; + lastClaimOreAt: bigint; + lastClaimSolAt: bigint; + rewardsFactor: Uint8Array; // 16 bytes (Numeric/I80F48) + rewardsSol: bigint; + rewardsOre: bigint; + refinedOre: bigint; + roundId: bigint; + lifetimeRewardsSol: bigint; + lifetimeRewardsOre: bigint; + lifetimeDeployed: bigint; +} + +export interface RoundAccount { + id: bigint; + deployed: bigint[]; // 25 u64 values + slotHash: Uint8Array; // 32 bytes + count: bigint[]; // 25 u64 values + expiresAt: bigint; + motherlode: bigint; + rentPayer: PublicKey; + topMiner: PublicKey; + topMinerReward: bigint; + totalDeployed: bigint; + totalMiners: bigint; + totalVaulted: bigint; + totalWinnings: bigint; +} + +export interface StakeAccount { + authority: PublicKey; + balance: bigint; + bufferA: bigint; + bufferB: bigint; + bufferC: bigint; + bufferD: bigint; + bufferE: bigint; + lastClaimAt: bigint; + lastDepositAt: bigint; + lastWithdrawAt: bigint; + rewardsFactor: Uint8Array; // 16 bytes (Numeric/I80F48) + rewards: bigint; + lifetimeRewards: bigint; + bufferF: bigint; +} + +export interface TreasuryAccount { + balance: bigint; + bufferA: bigint; + motherlode: bigint; + minerRewardsFactor: Uint8Array; // 16 bytes + stakeRewardsFactor: Uint8Array; // 16 bytes + bufferB: bigint; + totalRefined: bigint; + totalStaked: bigint; + totalUnclaimed: bigint; +} + +// ============================================================================ +// Parsing Helpers +// ============================================================================ + +function readU64LE(data: Buffer, offset: number): bigint { + return data.readBigUInt64LE(offset); +} + +function readI64LE(data: Buffer, offset: number): bigint { + return data.readBigInt64LE(offset); +} + +function readPublicKey(data: Buffer, offset: number): PublicKey { + return new PublicKey(data.subarray(offset, offset + 32)); +} + +function readU64Array(data: Buffer, offset: number, count: number): bigint[] { + const result: bigint[] = []; + for (let i = 0; i < count; i++) { + result.push(readU64LE(data, offset + i * 8)); + } + return result; +} + +function verifyDiscriminator(data: Buffer, expected: readonly number[]): boolean { + for (let i = 0; i < 8; i++) { + if (data[i] !== expected[i]) { + return false; + } + } + return true; +} + +// ============================================================================ +// Account Parsers +// ============================================================================ + +/** + * Parse Board account data + * Layout: + * - 8 bytes: discriminator [105, 0, 0, 0, 0, 0, 0, 0] + * - 8 bytes: round_id (u64) + * - 8 bytes: start_slot (u64) + * - 8 bytes: end_slot (u64) + * - 8 bytes: epoch_id (u64) + */ +export function parseBoardAccount(data: Buffer): BoardAccount { + if (!verifyDiscriminator(data, OreConfig.ACCOUNT_DISCRIMINATORS.Board)) { + throw new Error('Invalid Board account discriminator'); + } + + let offset = 8; // Skip discriminator + + const roundId = readU64LE(data, offset); + offset += 8; + + const startSlot = readU64LE(data, offset); + offset += 8; + + const endSlot = readU64LE(data, offset); + offset += 8; + + const epochId = readU64LE(data, offset); + + return { roundId, startSlot, endSlot, epochId }; +} + +/** + * Parse Config account data + * Layout: + * - 8 bytes: discriminator [101, 0, 0, 0, 0, 0, 0, 0] + * - 32 bytes: admin (PublicKey) + * - 32 bytes: bury_authority (PublicKey) + * - 32 bytes: fee_collector (PublicKey) + * - 32 bytes: swap_program (PublicKey) + * - 32 bytes: var_address (PublicKey) + * - 8 bytes: buffer (u64) + */ +export function parseConfigAccount(data: Buffer): ConfigAccount { + if (!verifyDiscriminator(data, OreConfig.ACCOUNT_DISCRIMINATORS.Config)) { + throw new Error('Invalid Config account discriminator'); + } + + let offset = 8; + + const admin = readPublicKey(data, offset); + offset += 32; + + const buryAuthority = readPublicKey(data, offset); + offset += 32; + + const feeCollector = readPublicKey(data, offset); + offset += 32; + + const swapProgram = readPublicKey(data, offset); + offset += 32; + + const varAddress = readPublicKey(data, offset); + offset += 32; + + const buffer = readU64LE(data, offset); + + return { admin, buryAuthority, feeCollector, swapProgram, varAddress, buffer }; +} + +/** + * Parse Miner account data + * Layout: + * - 8 bytes: discriminator [103, 0, 0, 0, 0, 0, 0, 0] + * - 32 bytes: authority (PublicKey) + * - 200 bytes: deployed ([u64; 25]) + * - 200 bytes: cumulative ([u64; 25]) + * - 8 bytes: checkpoint_fee (u64) + * - 8 bytes: checkpoint_id (u64) + * - 8 bytes: last_claim_ore_at (i64) + * - 8 bytes: last_claim_sol_at (i64) + * - 16 bytes: rewards_factor (Numeric) + * - 8 bytes: rewards_sol (u64) + * - 8 bytes: rewards_ore (u64) + * - 8 bytes: refined_ore (u64) + * - 8 bytes: round_id (u64) + * - 8 bytes: lifetime_rewards_sol (u64) + * - 8 bytes: lifetime_rewards_ore (u64) + * - 8 bytes: lifetime_deployed (u64) + */ +export function parseMinerAccount(data: Buffer): MinerAccount { + if (!verifyDiscriminator(data, OreConfig.ACCOUNT_DISCRIMINATORS.Miner)) { + throw new Error('Invalid Miner account discriminator'); + } + + let offset = 8; + + const authority = readPublicKey(data, offset); + offset += 32; + + const deployed = readU64Array(data, offset, 25); + offset += 200; + + const cumulative = readU64Array(data, offset, 25); + offset += 200; + + const checkpointFee = readU64LE(data, offset); + offset += 8; + + const checkpointId = readU64LE(data, offset); + offset += 8; + + const lastClaimOreAt = readI64LE(data, offset); + offset += 8; + + const lastClaimSolAt = readI64LE(data, offset); + offset += 8; + + const rewardsFactor = new Uint8Array(data.subarray(offset, offset + 16)); + offset += 16; + + const rewardsSol = readU64LE(data, offset); + offset += 8; + + const rewardsOre = readU64LE(data, offset); + offset += 8; + + const refinedOre = readU64LE(data, offset); + offset += 8; + + const roundId = readU64LE(data, offset); + offset += 8; + + const lifetimeRewardsSol = readU64LE(data, offset); + offset += 8; + + const lifetimeRewardsOre = readU64LE(data, offset); + offset += 8; + + const lifetimeDeployed = readU64LE(data, offset); + + return { + authority, + deployed, + cumulative, + checkpointFee, + checkpointId, + lastClaimOreAt, + lastClaimSolAt, + rewardsFactor, + rewardsSol, + rewardsOre, + refinedOre, + roundId, + lifetimeRewardsSol, + lifetimeRewardsOre, + lifetimeDeployed, + }; +} + +/** + * Parse Round account data + * Layout: + * - 8 bytes: discriminator [109, 0, 0, 0, 0, 0, 0, 0] + * - 8 bytes: id (u64) + * - 200 bytes: deployed ([u64; 25]) + * - 32 bytes: slot_hash ([u8; 32]) + * - 200 bytes: count ([u64; 25]) + * - 8 bytes: expires_at (u64) + * - 8 bytes: motherlode (u64) + * - 32 bytes: rent_payer (PublicKey) + * - 32 bytes: top_miner (PublicKey) + * - 8 bytes: top_miner_reward (u64) + * - 8 bytes: total_deployed (u64) + * - 8 bytes: total_miners (u64) + * - 8 bytes: total_vaulted (u64) + * - 8 bytes: total_winnings (u64) + */ +export function parseRoundAccount(data: Buffer): RoundAccount { + if (!verifyDiscriminator(data, OreConfig.ACCOUNT_DISCRIMINATORS.Round)) { + throw new Error('Invalid Round account discriminator'); + } + + let offset = 8; + + const id = readU64LE(data, offset); + offset += 8; + + const deployed = readU64Array(data, offset, 25); + offset += 200; + + const slotHash = new Uint8Array(data.subarray(offset, offset + 32)); + offset += 32; + + const count = readU64Array(data, offset, 25); + offset += 200; + + const expiresAt = readU64LE(data, offset); + offset += 8; + + const motherlode = readU64LE(data, offset); + offset += 8; + + const rentPayer = readPublicKey(data, offset); + offset += 32; + + const topMiner = readPublicKey(data, offset); + offset += 32; + + const topMinerReward = readU64LE(data, offset); + offset += 8; + + const totalDeployed = readU64LE(data, offset); + offset += 8; + + const totalMiners = readU64LE(data, offset); + offset += 8; + + const totalVaulted = readU64LE(data, offset); + offset += 8; + + const totalWinnings = readU64LE(data, offset); + + return { + id, + deployed, + slotHash, + count, + expiresAt, + motherlode, + rentPayer, + topMiner, + topMinerReward, + totalDeployed, + totalMiners, + totalVaulted, + totalWinnings, + }; +} + +/** + * Parse Stake account data + * Layout: + * - 8 bytes: discriminator [108, 0, 0, 0, 0, 0, 0, 0] + * - 32 bytes: authority (PublicKey) + * - 8 bytes: balance (u64) + * - 8 bytes: buffer_a (u64) + * - 8 bytes: buffer_b (u64) + * - 8 bytes: buffer_c (u64) + * - 8 bytes: buffer_d (u64) + * - 8 bytes: buffer_e (u64) + * - 8 bytes: last_claim_at (i64) + * - 8 bytes: last_deposit_at (i64) + * - 8 bytes: last_withdraw_at (i64) + * - 16 bytes: rewards_factor (Numeric) + * - 8 bytes: rewards (u64) + * - 8 bytes: lifetime_rewards (u64) + * - 8 bytes: buffer_f (u64) + */ +export function parseStakeAccount(data: Buffer): StakeAccount { + if (!verifyDiscriminator(data, OreConfig.ACCOUNT_DISCRIMINATORS.Stake)) { + throw new Error('Invalid Stake account discriminator'); + } + + let offset = 8; + + const authority = readPublicKey(data, offset); + offset += 32; + + const balance = readU64LE(data, offset); + offset += 8; + + const bufferA = readU64LE(data, offset); + offset += 8; + + const bufferB = readU64LE(data, offset); + offset += 8; + + const bufferC = readU64LE(data, offset); + offset += 8; + + const bufferD = readU64LE(data, offset); + offset += 8; + + const bufferE = readU64LE(data, offset); + offset += 8; + + const lastClaimAt = readI64LE(data, offset); + offset += 8; + + const lastDepositAt = readI64LE(data, offset); + offset += 8; + + const lastWithdrawAt = readI64LE(data, offset); + offset += 8; + + const rewardsFactor = new Uint8Array(data.subarray(offset, offset + 16)); + offset += 16; + + const rewards = readU64LE(data, offset); + offset += 8; + + const lifetimeRewards = readU64LE(data, offset); + offset += 8; + + const bufferF = readU64LE(data, offset); + + return { + authority, + balance, + bufferA, + bufferB, + bufferC, + bufferD, + bufferE, + lastClaimAt, + lastDepositAt, + lastWithdrawAt, + rewardsFactor, + rewards, + lifetimeRewards, + bufferF, + }; +} + +/** + * Parse Treasury account data + * Layout: + * - 8 bytes: discriminator [104, 0, 0, 0, 0, 0, 0, 0] + * - 8 bytes: balance (u64) + * - 8 bytes: buffer_a (u64) + * - 8 bytes: motherlode (u64) + * - 16 bytes: miner_rewards_factor (Numeric) + * - 16 bytes: stake_rewards_factor (Numeric) + * - 8 bytes: buffer_b (u64) + * - 8 bytes: total_refined (u64) + * - 8 bytes: total_staked (u64) + * - 8 bytes: total_unclaimed (u64) + */ +export function parseTreasuryAccount(data: Buffer): TreasuryAccount { + if (!verifyDiscriminator(data, OreConfig.ACCOUNT_DISCRIMINATORS.Treasury)) { + throw new Error('Invalid Treasury account discriminator'); + } + + let offset = 8; + + const balance = readU64LE(data, offset); + offset += 8; + + const bufferA = readU64LE(data, offset); + offset += 8; + + const motherlode = readU64LE(data, offset); + offset += 8; + + const minerRewardsFactor = new Uint8Array(data.subarray(offset, offset + 16)); + offset += 16; + + const stakeRewardsFactor = new Uint8Array(data.subarray(offset, offset + 16)); + offset += 16; + + const bufferB = readU64LE(data, offset); + offset += 8; + + const totalRefined = readU64LE(data, offset); + offset += 8; + + const totalStaked = readU64LE(data, offset); + offset += 8; + + const totalUnclaimed = readU64LE(data, offset); + + return { + balance, + bufferA, + motherlode, + minerRewardsFactor, + stakeRewardsFactor, + bufferB, + totalRefined, + totalStaked, + totalUnclaimed, + }; +} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Convert square indices array to bitmask + * @param squares Array of square indices (0-24) + * @returns Bitmask as a number + */ +export function squaresToBitmask(squares: number[]): number { + let bitmask = 0; + for (const square of squares) { + if (square < 0 || square > 24) { + throw new Error(`Invalid square index: ${square}. Must be 0-24.`); + } + bitmask |= 1 << square; + } + return bitmask; +} + +/** + * Convert bitmask to square indices array + * @param bitmask Bitmask number + * @returns Array of square indices + */ +export function bitmaskToSquares(bitmask: number): number[] { + const squares: number[] = []; + for (let i = 0; i < 25; i++) { + if (bitmask & (1 << i)) { + squares.push(i); + } + } + return squares; +} + +/** + * Convert bytes to hex string + */ +export function bytesToHex(bytes: Uint8Array): string { + return Buffer.from(bytes).toString('hex'); +} diff --git a/src/connectors/ore/ore.routes.ts b/src/connectors/ore/ore.routes.ts new file mode 100644 index 0000000000..c9b26b44f9 --- /dev/null +++ b/src/connectors/ore/ore.routes.ts @@ -0,0 +1,27 @@ +import sensible from '@fastify/sensible'; +import type { FastifyPluginAsync } from 'fastify'; + +// Import routes +import { oreRoutes } from './ore-routes'; + +// ORE mining/staking routes wrapper +const oreRoutesWrapper: FastifyPluginAsync = async (fastify) => { + await fastify.register(sensible); + + await fastify.register(async (instance) => { + instance.addHook('onRoute', (routeOptions) => { + if (routeOptions.schema && routeOptions.schema.tags) { + routeOptions.schema.tags = ['/connector/ore']; + } + }); + + await instance.register(oreRoutes); + }); +}; + +// Export the ORE routes +export const oreConnectorRoutes = { + ore: oreRoutesWrapper, +}; + +export default oreConnectorRoutes; diff --git a/src/connectors/ore/ore.ts b/src/connectors/ore/ore.ts new file mode 100644 index 0000000000..27d93726fc --- /dev/null +++ b/src/connectors/ore/ore.ts @@ -0,0 +1,356 @@ +import { Keypair, PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js'; + +import { Solana } from '../../chains/solana/solana'; +import { SolanaLedger } from '../../chains/solana/solana-ledger'; +import { httpErrors } from '../../services/error-handler'; +import { logger } from '../../services/logger'; + +import { OreConfig } from './ore.config'; +import { + parseBoardAccount, + parseConfigAccount, + parseMinerAccount, + parseRoundAccount, + parseStakeAccount, + parseTreasuryAccount, + BoardAccount, + ConfigAccount, + MinerAccount, + RoundAccount, + StakeAccount, + TreasuryAccount, + bytesToHex, +} from './ore.parser'; +import { OreAccountInfoResponseType, OreBoardInfoResponseType, OreSystemInfoResponseType } from './schemas'; + +export class Ore { + private static _instances: { [name: string]: Ore }; + public solana: Solana; + public config: OreConfig.RootConfig; + + private constructor() { + this.config = OreConfig.config; + this.solana = null as any; + } + + /** Gets singleton instance of Ore */ + public static async getInstance(network: string): Promise { + if (!Ore._instances) { + Ore._instances = {}; + } + + if (!Ore._instances[network]) { + const instance = new Ore(); + await instance.init(network); + Ore._instances[network] = instance; + } + + return Ore._instances[network]; + } + + /** Initializes Ore instance */ + private async init(network: string) { + try { + this.solana = await Solana.getInstance(network); + logger.info('ORE connector initialized'); + } catch (error) { + logger.error('ORE connector initialization failed:', error); + throw error; + } + } + + // ============================================================================ + // Account Fetching Methods + // ============================================================================ + + /** Fetch Board account (singleton) */ + async getBoardAccount(): Promise { + const [boardPDA] = OreConfig.getBoardPDA(); + const accountInfo = await this.solana.connection.getAccountInfo(boardPDA, 'confirmed'); + + if (!accountInfo) { + throw httpErrors.notFound('Board account not found'); + } + + return parseBoardAccount(accountInfo.data as Buffer); + } + + /** Fetch Config account (singleton) */ + async getConfigAccount(): Promise { + const [configPDA] = OreConfig.getConfigPDA(); + const accountInfo = await this.solana.connection.getAccountInfo(configPDA, 'confirmed'); + + if (!accountInfo) { + throw httpErrors.notFound('Config account not found'); + } + + return parseConfigAccount(accountInfo.data as Buffer); + } + + /** Fetch Treasury account (singleton) */ + async getTreasuryAccount(): Promise { + const [treasuryPDA] = OreConfig.getTreasuryPDA(); + const accountInfo = await this.solana.connection.getAccountInfo(treasuryPDA, 'confirmed'); + + if (!accountInfo) { + throw httpErrors.notFound('Treasury account not found'); + } + + return parseTreasuryAccount(accountInfo.data as Buffer); + } + + /** Fetch Round account by ID */ + async getRoundAccount(roundId: bigint): Promise { + const [roundPDA] = OreConfig.getRoundPDA(roundId); + const accountInfo = await this.solana.connection.getAccountInfo(roundPDA, 'confirmed'); + + if (!accountInfo) { + throw httpErrors.notFound(`Round account not found for round ${roundId}`); + } + + return parseRoundAccount(accountInfo.data as Buffer); + } + + /** Fetch Miner account for a wallet */ + async getMinerAccount(walletAddress: string): Promise { + let walletPubkey: PublicKey; + try { + walletPubkey = new PublicKey(walletAddress); + } catch { + throw httpErrors.badRequest(`Invalid wallet address: ${walletAddress}`); + } + + const [minerPDA] = OreConfig.getMinerPDA(walletPubkey); + const accountInfo = await this.solana.connection.getAccountInfo(minerPDA, 'confirmed'); + + if (!accountInfo) { + return null; // Miner account doesn't exist yet + } + + return parseMinerAccount(accountInfo.data as Buffer); + } + + /** Fetch Stake account for a wallet */ + async getStakeAccount(walletAddress: string): Promise { + let walletPubkey: PublicKey; + try { + walletPubkey = new PublicKey(walletAddress); + } catch { + throw httpErrors.badRequest(`Invalid wallet address: ${walletAddress}`); + } + + const [stakePDA] = OreConfig.getStakePDA(walletPubkey); + const accountInfo = await this.solana.connection.getAccountInfo(stakePDA, 'confirmed'); + + if (!accountInfo) { + return null; // Stake account doesn't exist yet + } + + return parseStakeAccount(accountInfo.data as Buffer); + } + + // ============================================================================ + // High-Level Info Methods (for routes) + // ============================================================================ + + /** Get board info including round state */ + async getBoardInfo(roundId?: number): Promise { + const board = await this.getBoardAccount(); + const requestedRoundId = roundId !== undefined ? BigInt(roundId) : board.roundId; + const round = await this.getRoundAccount(requestedRoundId); + + const [roundPDA] = OreConfig.getRoundPDA(requestedRoundId); + + // Calculate seconds left in round (only relevant for current round) + const currentSlot = await this.solana.connection.getSlot('confirmed'); + const slotsRemaining = Number(board.endSlot) - currentSlot; + // Solana averages ~400ms per slot + // For historical rounds, secondsLeft will be 0 + const isCurrentRound = requestedRoundId === board.roundId; + const secondsLeft = isCurrentRound ? Math.max(0, Math.floor(slotsRemaining * 0.4)) : 0; + + // Calculate winning square from slotHash + // slotHash is all zeros for current/unfinalized rounds + // RNG: XOR four 8-byte chunks of the 32-byte hash, then mod 25 + const isFinalized = !round.slotHash.every((b) => b === 0); + let winningSquare: number | null = null; + let winningSquareIndex: number | null = null; // 0-indexed for internal use + if (isFinalized) { + const view = new DataView(round.slotHash.buffer, round.slotHash.byteOffset, 32); + const r1 = view.getBigUint64(0, true); + const r2 = view.getBigUint64(8, true); + const r3 = view.getBigUint64(16, true); + const r4 = view.getBigUint64(24, true); + const rng = r1 ^ r2 ^ r3 ^ r4; + winningSquareIndex = Number(rng % 25n); // 0-indexed internally + winningSquare = winningSquareIndex + 1; // 1-indexed for API response + } + + // Build squares dictionary (1-25) with SOL amounts + const squares: Record = {}; + for (let i = 0; i < 25; i++) { + squares[(i + 1).toString()] = { + deployed: Number(round.deployed[i]) / 1_000_000_000, // Convert lamports to SOL + miners: Number(round.count[i]), + }; + } + + // Get winner miners count (miners who deployed to winning square) + const winnerMiners = winningSquareIndex !== null ? Number(round.count[winningSquareIndex]) : 0; + + // Check for ORE winner + // topMiner is system program if no winner, "SpLiT1111..." if split among winners + const SYSTEM_PROGRAM = '11111111111111111111111111111111'; + const SPLIT_ADDRESS_PREFIX = 'SpLiT'; + const topMinerAddress = round.topMiner.toBase58(); + const isNoWinner = topMinerAddress === SYSTEM_PROGRAM; + const isSplit = topMinerAddress.startsWith(SPLIT_ADDRESS_PREFIX); + + const ORE_DECIMALS = 11; + + return { + roundId: Number(requestedRoundId), + roundAddress: roundPDA.toBase58(), + secondsLeft, + winningSquare, + winnerMiners, + oreWinnerSplit: isSplit, + oreWinner: !isNoWinner && !isSplit ? topMinerAddress : null, + oreReward: Number(round.topMinerReward) / 10 ** ORE_DECIMALS, + squares, + totalDeployedSol: Number(round.totalDeployed) / 1_000_000_000, + totalVaultedSol: Number(round.totalVaulted) / 1_000_000_000, + totalWinningsSol: Number(round.totalWinnings) / 1_000_000_000, + motherlodeOre: Number(round.motherlode) / 10 ** ORE_DECIMALS, + totalMiners: Number(round.totalMiners), + expiresAt: Number(round.expiresAt), + }; + } + + /** Get combined account info (miner + stake) for a wallet */ + async getAccountInfo(walletAddress: string, roundId?: number): Promise { + const walletPubkey = new PublicKey(walletAddress); + + // Fetch both miner and stake accounts (they may or may not exist) + const miner = await this.getMinerAccount(walletAddress); + const stake = await this.getStakeAccount(walletAddress); + + // Get current round from board if no roundId specified + const board = await this.getBoardAccount(); + const currentRoundId = roundId !== undefined ? BigInt(roundId) : board.roundId; + + // Build deployment per square (1-25) + // Only show miner's deployed amounts if they participated in the requested round + const deployedSol: Record = {}; + const minerParticipatedInRound = miner && miner.roundId === currentRoundId; + for (let i = 0; i < 25; i++) { + deployedSol[(i + 1).toString()] = minerParticipatedInRound ? Number(miner.deployed[i]) / 1_000_000_000 : 0; + } + + const [minerPDA] = OreConfig.getMinerPDA(walletPubkey); + const [stakePDA] = OreConfig.getStakePDA(walletPubkey); + + const ORE_DECIMALS = 11; + + return { + // Account addresses + mineAddress: miner ? minerPDA.toBase58() : null, + stakeAddress: stake ? stakePDA.toBase58() : null, + // Mine info + lastRound: miner ? Number(miner.roundId) : null, + checkedRound: miner ? Number(miner.checkpointId) : null, + currentRound: { + roundId: Number(currentRoundId), + deployedSol, + }, + rewardsSol: miner ? Number(miner.rewardsSol) / 1_000_000_000 : 0, + rewardsOre: miner ? Number(miner.rewardsOre) / 10 ** ORE_DECIMALS : 0, + lifetimeRewardsSol: miner ? Number(miner.lifetimeRewardsSol) / 1_000_000_000 : 0, + lifetimeRewardsOre: miner ? Number(miner.lifetimeRewardsOre) / 10 ** ORE_DECIMALS : 0, + lifetimeDeployed: miner ? Number(miner.lifetimeDeployed) / 1_000_000_000 : 0, + // Stake info + stakedOre: stake ? Number(stake.balance) / 10 ** ORE_DECIMALS : 0, + stakeRewardsOre: stake ? Number(stake.rewards) / 10 ** ORE_DECIMALS : 0, + lifetimeStakeRewardsOre: stake ? Number(stake.lifetimeRewards) / 10 ** ORE_DECIMALS : 0, + }; + } + + /** Get system info (treasury + token supply) */ + async getSystemInfo(): Promise { + const treasury = await this.getTreasuryAccount(); + + const [treasuryPDA] = OreConfig.getTreasuryPDA(); + + // Fetch ORE token supply from mint + const tokenSupplyInfo = await this.solana.connection.getTokenSupply(OreConfig.ORE_TOKEN_MINT); + const circulatingSupplyRaw = BigInt(tokenSupplyInfo.value.amount); + + const ORE_DECIMALS = 11; + const MAX_SUPPLY_ORE = 5_000_000; // 5 million ORE max supply + + // Circulating supply from token mint + const circulatingSupplyOre = Number(circulatingSupplyRaw) / 10 ** ORE_DECIMALS; + + // Buried = totalRefined - circulatingSupply (refined but burned) + const totalRefinedOre = Number(treasury.totalRefined) / 10 ** ORE_DECIMALS; + const buriedOre = Math.max(0, totalRefinedOre - circulatingSupplyOre); + + return { + treasuryAddress: treasuryPDA.toBase58(), + treasuryBalanceSol: Number(treasury.balance) / 1_000_000_000, + maxSupplyOre: MAX_SUPPLY_ORE, + circulatingSupplyOre, + buriedOre, + totalRefinedOre, + totalStakedOre: Number(treasury.totalStaked) / 10 ** ORE_DECIMALS, + totalUnclaimedOre: Number(treasury.totalUnclaimed) / 10 ** ORE_DECIMALS, + motherlodeOre: Number(treasury.motherlode) / 10 ** ORE_DECIMALS, + }; + } + + // ============================================================================ + // Wallet Helpers (for hardware wallet support) + // ============================================================================ + + /** Prepare wallet for transaction signing */ + public async prepareWallet(walletAddress: string): Promise<{ + wallet: Keypair | PublicKey; + isHardwareWallet: boolean; + }> { + const isHardwareWallet = await this.solana.isHardwareWallet(walletAddress); + const wallet = isHardwareWallet ? new PublicKey(walletAddress) : await this.solana.getWallet(walletAddress); + + return { wallet, isHardwareWallet }; + } + + /** Sign and send transaction (with hardware wallet support) */ + public async signAndSendTransaction( + transaction: VersionedTransaction | Transaction, + walletAddress: string, + isHardwareWallet: boolean, + ): Promise { + if (isHardwareWallet) { + logger.info(`Hardware wallet detected for ${walletAddress}. Signing transaction with Ledger.`); + const ledger = new SolanaLedger(); + const signedTx = await ledger.signTransaction(walletAddress, transaction); + const signature = await this.solana.connection.sendRawTransaction(signedTx.serialize()); + await this.solana.connection.confirmTransaction(signature, 'confirmed'); + return signature; + } else { + // Regular wallet signing + const wallet = await this.solana.getWallet(walletAddress); + if (transaction instanceof Transaction) { + transaction.sign(wallet); + const signature = await this.solana.connection.sendRawTransaction(transaction.serialize()); + await this.solana.connection.confirmTransaction(signature, 'confirmed'); + return signature; + } else { + // VersionedTransaction + transaction.sign([wallet]); + const signature = await this.solana.connection.sendRawTransaction(transaction.serialize()); + await this.solana.connection.confirmTransaction(signature, 'confirmed'); + return signature; + } + } + } +} diff --git a/src/connectors/ore/schemas.ts b/src/connectors/ore/schemas.ts new file mode 100644 index 0000000000..840a4a59ea --- /dev/null +++ b/src/connectors/ore/schemas.ts @@ -0,0 +1,341 @@ +import { Static, Type } from '@sinclair/typebox'; + +import { getSolanaChainConfig } from '../../chains/solana/solana.config'; + +import { OreConfig } from './ore.config'; + +// Get chain config for defaults +const solanaChainConfig = getSolanaChainConfig(); + +// ============================================================================ +// Response Schemas +// ============================================================================ + +// Square info for each square on the 5x5 board +const SquareInfo = Type.Object({ + deployed: Type.Number({ description: 'SOL deployed to this square' }), + miners: Type.Number({ description: 'Number of miners who deployed to this square' }), +}); + +// Board Info Response (includes current round info) +export const OreBoardInfoResponse = Type.Object({ + roundId: Type.Number({ description: 'Round number' }), + roundAddress: Type.String({ description: 'Round PDA address' }), + secondsLeft: Type.Number({ description: 'Seconds remaining in current round (0 for historical rounds)' }), + winningSquare: Type.Union([Type.Number(), Type.Null()], { + description: 'Winning square (1-25), null if round not finalized', + }), + winnerMiners: Type.Number({ description: 'Number of miners who won (deployed to winning square)' }), + oreWinnerSplit: Type.Boolean({ description: 'True if ORE reward was split among all winners' }), + oreWinner: Type.Union([Type.String(), Type.Null()], { + description: 'ORE winner address (single winner), null if split or no winner', + }), + oreReward: Type.Number({ description: 'ORE reward amount' }), + squares: Type.Record(Type.String(), SquareInfo, { + description: 'Square data indexed 1-25 (5x5 grid)', + }), + totalDeployedSol: Type.Number({ description: 'Total SOL deployed this round' }), + totalVaultedSol: Type.Number({ description: 'Total SOL vaulted this round' }), + totalWinningsSol: Type.Number({ description: 'Total SOL winnings this round' }), + motherlodeOre: Type.Number({ description: 'Prize pool in ORE' }), + totalMiners: Type.Number({ description: 'Total number of unique miners' }), + expiresAt: Type.Number({ description: 'Round expiration timestamp (unix seconds)' }), +}); + +export type OreBoardInfoResponseType = Static; + +// Account Info Response (miner + stake combined) +export const OreAccountInfoResponse = Type.Object({ + // Account addresses + mineAddress: Type.Union([Type.String(), Type.Null()], { description: 'Mine PDA address (null if not created)' }), + stakeAddress: Type.Union([Type.String(), Type.Null()], { description: 'Stake PDA address (null if not created)' }), + // Mine info + lastRound: Type.Union([Type.Number(), Type.Null()], { + description: 'Last round the miner deployed to (null if never mined)', + }), + checkedRound: Type.Union([Type.Number(), Type.Null()], { + description: 'Last round the miner checkpointed (null if never checkpointed)', + }), + currentRound: Type.Object({ + roundId: Type.Union([Type.Number(), Type.Null()], { description: 'Current round ID' }), + deployedSol: Type.Record(Type.String(), Type.Number(), { + description: 'SOL deployed per square this round (1-25)', + }), + }), + rewardsSol: Type.Number({ description: 'Claimable SOL rewards' }), + rewardsOre: Type.Number({ description: 'Claimable ORE rewards' }), + lifetimeRewardsSol: Type.Number({ description: 'Lifetime SOL rewards' }), + lifetimeRewardsOre: Type.Number({ description: 'Lifetime ORE rewards' }), + lifetimeDeployed: Type.Number({ description: 'Lifetime SOL deployed' }), + // Stake info + stakedOre: Type.Number({ description: 'Staked ORE balance' }), + stakeRewardsOre: Type.Number({ description: 'Claimable staking rewards' }), + lifetimeStakeRewardsOre: Type.Number({ description: 'Lifetime staking rewards' }), +}); + +export type OreAccountInfoResponseType = Static; + +// System Info Response (treasury + config combined) +export const OreSystemInfoResponse = Type.Object({ + treasuryAddress: Type.String({ description: 'Treasury PDA address' }), + treasuryBalanceSol: Type.Number({ description: 'Treasury balance in SOL' }), + maxSupplyOre: Type.Number({ description: 'Maximum ORE supply (5 million)' }), + circulatingSupplyOre: Type.Number({ description: 'Circulating ORE supply (from token mint)' }), + buriedOre: Type.Number({ description: 'Buried (burned) ORE' }), + totalRefinedOre: Type.Number({ description: 'Total refined ORE' }), + totalStakedOre: Type.Number({ description: 'Total staked ORE' }), + totalUnclaimedOre: Type.Number({ description: 'Total unclaimed ORE rewards' }), + motherlodeOre: Type.Number({ description: 'Motherlode prize pool in ORE' }), +}); + +export type OreSystemInfoResponseType = Static; + +// Transaction Response +export const OreTransactionResponse = Type.Object({ + signature: Type.String({ description: 'Transaction signature' }), + message: Type.Optional(Type.String({ description: 'Additional message' })), +}); + +export type OreTransactionResponseType = Static; + +// Checkpoint Response (with details about the round) +export const OreCheckpointResponse = Type.Object({ + signature: Type.String({ description: 'Transaction signature' }), + roundId: Type.Number({ description: 'Round that was checkpointed' }), + winningSquare: Type.Number({ description: 'Winning square (1-25)' }), + deployedSquares: Type.Array(Type.Number(), { description: 'Squares you deployed to (1-25)' }), + deployedSol: Type.Number({ description: 'Total SOL you deployed' }), + won: Type.Boolean({ description: 'Whether you deployed to the winning square' }), + wonSol: Type.Number({ description: 'SOL winnings from this round' }), + wonOre: Type.Number({ description: 'ORE winnings from this round' }), +}); + +export type OreCheckpointResponseType = Static; + +// ============================================================================ +// Request Schemas - GET Routes +// ============================================================================ + +// Board Info Request +export const OreBoardInfoRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), + roundId: Type.Optional( + Type.Number({ + description: 'Optional round ID to fetch historical round info (defaults to current round)', + }), + ), +}); + +export type OreBoardInfoRequestType = Static; + +// Account Info Request +export const OreAccountInfoRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), + walletAddress: Type.String({ + description: 'Wallet address to query account info for', + examples: [solanaChainConfig.defaultWallet], + }), + roundId: Type.Optional( + Type.Number({ + description: 'Optional round ID to fetch historical round info (defaults to current round)', + }), + ), +}); + +export type OreAccountInfoRequestType = Static; + +// System Info Request +export const OreSystemInfoRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), +}); + +export type OreSystemInfoRequestType = Static; + +// ============================================================================ +// Request Schemas - POST Mining Routes +// ============================================================================ + +// Deploy Request +export const OreDeployRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), + walletAddress: Type.Optional( + Type.String({ + description: 'Wallet address', + default: solanaChainConfig.defaultWallet, + }), + ), + amount: Type.Number({ + description: 'Amount of SOL to deploy (in SOL, not lamports)', + minimum: 0, + examples: [0.1], + }), + squares: Type.Array(Type.Number(), { + description: 'Square(s) to deploy to (1-25). SOL is split evenly across selected squares.', + examples: [[13], [1, 6, 11, 16, 21]], + }), +}); + +export type OreDeployRequestType = Static; + +// Checkpoint Request +export const OreCheckpointRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), + walletAddress: Type.Optional( + Type.String({ + description: 'Wallet address', + default: solanaChainConfig.defaultWallet, + }), + ), + roundId: Type.Optional( + Type.String({ + description: 'Round ID to checkpoint (defaults to last completed round)', + }), + ), +}); + +export type OreCheckpointRequestType = Static; + +// Claim SOL Request +export const OreClaimSolRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), + walletAddress: Type.Optional( + Type.String({ + description: 'Wallet address', + default: solanaChainConfig.defaultWallet, + }), + ), +}); + +export type OreClaimSolRequestType = Static; + +// Claim ORE Request +export const OreClaimOreRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), + walletAddress: Type.Optional( + Type.String({ + description: 'Wallet address', + default: solanaChainConfig.defaultWallet, + }), + ), +}); + +export type OreClaimOreRequestType = Static; + +// ============================================================================ +// Request Schemas - POST Staking Routes +// ============================================================================ + +// Stake Request +export const OreStakeRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), + walletAddress: Type.Optional( + Type.String({ + description: 'Wallet address', + default: solanaChainConfig.defaultWallet, + }), + ), + amount: Type.Number({ + description: 'Amount of ORE to stake', + minimum: 0, + examples: [100], + }), +}); + +export type OreStakeRequestType = Static; + +// Unstake Request +export const OreUnstakeRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), + walletAddress: Type.Optional( + Type.String({ + description: 'Wallet address', + default: solanaChainConfig.defaultWallet, + }), + ), + amount: Type.Number({ + description: 'Amount of ORE to unstake', + minimum: 0, + examples: [100], + }), +}); + +export type OreUnstakeRequestType = Static; + +// Claim Stake Rewards Request +export const OreClaimStakeRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Solana network to use', + default: solanaChainConfig.defaultNetwork, + enum: [...OreConfig.networks], + }), + ), + walletAddress: Type.Optional( + Type.String({ + description: 'Wallet address', + default: solanaChainConfig.defaultWallet, + }), + ), + amount: Type.Optional( + Type.Number({ + description: 'Amount of yield to claim (defaults to all available)', + minimum: 0, + }), + ), +}); + +export type OreClaimStakeRequestType = Static; diff --git a/src/templates/connectors/ore.yml b/src/templates/connectors/ore.yml new file mode 100644 index 0000000000..675618ad8d --- /dev/null +++ b/src/templates/connectors/ore.yml @@ -0,0 +1,12 @@ +# ORE Mining Game Connector Configuration (Experimental) +# This connector provides access to the ORE v3 mining game on Solana + +# Program IDs are hardcoded in the connector +# ORE Program: oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv +# ORE Token Mint: oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp + +# Default transaction confirmation commitment level +commitment: confirmed + +# Default priority fee in microlamports (optional, 0 = use network default) +priorityFee: 0 diff --git a/src/templates/namespace/ore-schema.json b/src/templates/namespace/ore-schema.json new file mode 100644 index 0000000000..746fb0a641 --- /dev/null +++ b/src/templates/namespace/ore-schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "commitment": { + "type": "string", + "description": "Default transaction confirmation commitment level", + "enum": ["processed", "confirmed", "finalized"] + }, + "priorityFee": { + "type": "number", + "description": "Default priority fee in microlamports (0 = use network default)" + } + }, + "additionalProperties": false, + "required": ["commitment", "priorityFee"] +} diff --git a/src/templates/root.yml b/src/templates/root.yml index d484ef78f4..1222a3acc8 100644 --- a/src/templates/root.yml +++ b/src/templates/root.yml @@ -92,6 +92,10 @@ configurations: configurationPath: connectors/pancakeswap-sol.yml schemaPath: pancakeswap-sol-schema.json + $namespace ore: + configurationPath: connectors/ore.yml + schemaPath: ore-schema.json + # API Keys (centralized) $namespace apiKeys: configurationPath: apiKeys.yml