diff --git a/src/common/Types.mo b/src/common/Types.mo index de6f000..e9780ab 100644 --- a/src/common/Types.mo +++ b/src/common/Types.mo @@ -1374,6 +1374,7 @@ module Types { getMainerCyclesUsedPerResponse : () -> async NatResult; getCyclesBurnRate : (Types.CyclesBurnRateDefault) -> async Types.CyclesBurnRateResult; addCycles: () -> async AddCyclesResult; + topUpCyclesForMainerAgent: (MainerAgentTopUpInput) -> async MainerAgentCanisterResult; }; public type MainerCreator_Actor = actor { @@ -1398,6 +1399,7 @@ module Types { addMainerShareAgentCanister: (OfficialMainerAgentCanister) -> async MainerAgentCanisterResult; startTimerExecutionAdmin: () -> async AuthRecordResult; addCycles: () -> async AddCyclesResult; + updateAgentSettings: (MainerAgentSettingsInput) -> async StatusCodeRecordResult; }; public type LLMCanister = actor { diff --git a/src/mAIningPool/.gitignore b/src/mAIningPool/.gitignore new file mode 100644 index 0000000..495afe5 --- /dev/null +++ b/src/mAIningPool/.gitignore @@ -0,0 +1 @@ +.mops \ No newline at end of file diff --git a/src/mAIningPool/README.md b/src/mAIningPool/README.md new file mode 100644 index 0000000..06966e2 --- /dev/null +++ b/src/mAIningPool/README.md @@ -0,0 +1,153 @@ +# mAIning Pool Quick Reference + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ mAIning Pool Canister │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Current │ │ Next │ │ Archived │ │ +│ │ Participants │ │ Participants │ │ Cycles │ │ +│ │ (Active) │ │ (Committed) │ │ (History) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Pool mAIners (3-20 controlled) │ │ +│ │ mAIner1, mAIner2, mAIner3, ... mAInerN │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ▲ │ ▲ + │ │ │ + ICP │ FUNNAI│ Cycles│ +Contributions Rewards (from ICP) + │ │ │ +┌────────┴────────┐ ┌────────▼────────┐ ┌────────┴────────┐ +│ Participants │ │ Participants │ │ Game State │ +│ (Users) │ │ (Rewards) │ │ Canister │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## Weekly Timeline + +``` +Sunday Monday Tuesday - Sunday Sunday (EOD) + | | | | + | | | | + v v v v +Commit Cycle Start mAIning Commit +Deadline - Distribute Continues Deadline + - Archive - Earn FUNNAI (Next Week) + - Topup mAIners - Burn cycles + - Set burn rates +``` + +## Function Categories + +### 👤 User Functions (Public) +- `contributeToNextPool(icpAmountE8S, burnFunnai)` - Commit ICP for next week +- `getMyCurrentPoolContribution()` - View active contribution +- `getMyNextPoolContribution()` - View next week commitment +- `getMyHistory()` - View all past participation + +### 📊 Query Functions (Public) +- `getCurrentPoolStats()` - Active pool statistics +- `getNextPoolStats()` - Next pool commitments +- `getPoolConfiguration()` - Pool settings and limits +- `getAggregatedHistory()` - Lifetime pool statistics +- `getPoolBalances()` - Current ICP & FUNNAI balances +- `getPoolMainers()` - View all pool mAIners +- `getArchivedPoolCycle(cycleId)` - View specific past cycle +- `getAllArchivedPoolCycles()` - View all past cycles +- `getArchivedCycleParticipants(cycleId)` - View cycle participants + +### 🔧 Admin Functions (Controller Only) +- `startNextPoolCycle(weekStart, weekEnd)` - **Critical weekly function** +- `addPoolMainer(address, type)` - Add mAIner to pool +- `removePoolMainer(address)` - Remove mAIner from pool +- `updatePoolBalances(icp, funnai)` - Manual balance update +- `accrueFunnaiRewards(amount)` - Record mAIner rewards + +### ⚙️ System Functions +- `setGameStateCanisterId(id)` - Configure Game State +- `getGameStateCanisterId()` - View Game State ID +- `whoami()` - Identity check +- `health()` - Canister health status +- `amiController()` - Controller verification + +## Configuration Constants + +| Constant | Value | Description | +|----------|-------|-------------| +| MIN_ICP_CONTRIBUTION_E8S | 100_000_000 | 1 ICP minimum | +| MAX_ICP_CONTRIBUTION_E8S | 1_000_000_000_000 | 10,000 ICP maximum | +| FUNNAI_BURN_AMOUNT | 10_000_000 | 0.1 FUNNAI for new entrants | +| TREASURY_FEE_PERCENTAGE | 10 | 10% of ICP goes to treasury | + +## Common Workflows + +### New Participant Flow +1. User approves FUNNAI burn (0.1 FUNNAI) +2. User approves ICP transfer (≥1 ICP) +3. User calls `contributeToNextPool(amount, true)` +4. Pool records commitment for next cycle +5. Monday: admin starts cycle, next cycle becomes active +6. Week: pool mAIners earn rewards +7. Next Monday: pool distributes FUNNAI to user + +### Returning Participant Flow +1. User approves ICP transfer +2. User calls `contributeToNextPool(amount, false)` (no burn!) +3. Rest same as new participant + +### Weekly Admin Flow +1. Sunday ends (commitment phase closes) +2. Monday 00:00 UTC: admin calls `startNextPoolCycle()` +3. Function distributes last week's FUNNAI +4. Function archives completed cycle +5. Function promotes next → current participants +6. Function tops up mAIners +7. Function sets mAIner burn rates +8. New week begins + +## Monitoring Checklist + +Daily: +- [ ] Check pool balances +- [ ] Verify mAIner cycles balances +- [ ] Check reward accrual + +Weekly (Monday): +- [ ] Execute `startNextPoolCycle()` +- [ ] Verify distributions completed +- [ ] Check all mAIners topped up +- [ ] Verify burn rates set correctly +- [ ] Review participant count + +Monthly: +- [ ] Review aggregated statistics +- [ ] Check for failed distributions +- [ ] Verify data archival +- [ ] Monitor canister cycles + +## Testing Quick Commands + +```bash +# Check pool status +dfx canister call mAIningPool getCurrentPoolStats + +# View my participation +dfx canister call mAIningPool getMyCurrentPoolContribution + +# Contribute (testnet) +dfx canister call mAIningPool contributeToNextPool '(200_000_000, false)' + +# Start cycle (admin only) +dfx canister call mAIningPool startNextPoolCycle '(1737331200, 1737935999)' + +# View mAIners +dfx canister call mAIningPool getPoolMainers + +# Check balances +dfx canister call mAIningPool getPoolBalances +``` diff --git a/src/mAIningPool/USAGE_EXAMPLES.md b/src/mAIningPool/USAGE_EXAMPLES.md new file mode 100644 index 0000000..6d6a0a8 --- /dev/null +++ b/src/mAIningPool/USAGE_EXAMPLES.md @@ -0,0 +1,424 @@ +# mAIning Pool Usage Examples + +## For Pool Participants + +### 1. First-Time Participation + +```javascript +// Step 1: Approve FUNNAI burn (frontend call to FUNNAI ledger) +await funnaiLedger.approve({ + spender: poolCanisterPrincipal, + amount: 10_000_000n, // 0.1 FUNNAI +}); + +// Step 2: Approve ICP transfer (frontend call to ICP ledger) +await icpLedger.approve({ + spender: poolCanisterPrincipal, + amount: 200_000_000n, // 2 ICP in e8s +}); + +// Step 3: Contribute to pool +const result = await poolCanister.contributeToNextPool( + 200_000_000n, // 2 ICP + true // burnFunnai = true for first time +); + +// Result: { Ok: 200_000_000 } (your total contribution) +``` + +### 2. Continuing Participation (No FUNNAI Burn) + +```javascript +// Step 1: Approve ICP transfer only +await icpLedger.approve({ + spender: poolCanisterPrincipal, + amount: 300_000_000n, // 3 ICP +}); + +// Step 2: Contribute to pool +const result = await poolCanister.contributeToNextPool( + 300_000_000n, // 3 ICP + false // burnFunnai = false (participated last week) +); +``` + +### 3. Adding More to Existing Contribution + +```javascript +// Can contribute multiple times before cycle starts +// Contributions accumulate + +// First contribution this week +await poolCanister.contributeToNextPool(200_000_000n, false); +// Result: { Ok: 200_000_000 } + +// Add more before Sunday deadline +await poolCanister.contributeToNextPool(100_000_000n, false); +// Result: { Ok: 300_000_000 } (cumulative) +``` + +### 4. Check Your Contributions + +```javascript +// Check current active pool contribution +const current = await poolCanister.getMyCurrentPoolContribution(); +// Result: { Ok: 200_000_000 } (2 ICP) + +// Check next pool commitment +const next = await poolCanister.getMyNextPoolContribution(); +// Result: { Ok: 300_000_000 } (3 ICP committed for next week) +``` + +### 5. View Your History + +```javascript +const history = await poolCanister.getMyHistory(); +// Result: { Ok: [ +// { +// weekStartTimestamp: 1737331200n, +// weekEndTimestamp: 1737936000n, +// icpContributionE8S: 200_000_000n, +// funnaiDistribution: 15_000_000n +// }, +// { +// weekStartTimestamp: 1737936000n, +// weekEndTimestamp: 1738540800n, +// icpContributionE8S: 300_000_000n, +// funnaiDistribution: 22_500_000n +// } +// ]} +``` + +## For Pool Viewers + +### 1. Check Current Pool Stats + +```javascript +const stats = await poolCanister.getCurrentPoolStats(); +// Result: { Ok: { +// cycleId: 5, +// startTimestamp: 1737331200n, +// endTimestamp: 1737936000n, +// participantCount: 47, +// totalIcpContributedE8S: 15_000_000_000n, // 150 ICP +// totalFunnaiRewardsAccumulated: 500_000_000n +// }} +``` + +### 2. Check Next Pool Stats + +```javascript +const nextStats = await poolCanister.getNextPoolStats(); +// Result: { Ok: { +// cycleId: 6, +// participantCount: 52, +// totalIcpCommittedE8S: 18_000_000_000n, // 180 ICP committed +// commitmentDeadline: 1737936000n // Sunday end of day +// }} +``` + +### 3. View Pool Configuration + +```javascript +const config = await poolCanister.getPoolConfiguration(); +// Result: { Ok: { +// minIcpContributionE8S: 100_000_000n, // 1 ICP +// maxIcpContributionE8S: 1_000_000_000_000n, // 10,000 ICP +// funnaiEntryBurnAmount: 10_000_000n, // 0.1 FUNNAI +// treasuryFeePercentage: 10, +// currentCycleId: 5, +// nextCycleId: 6, +// totalPoolCycles: 5, +// totalParticipantsAllTime: 324 +// }} +``` + +### 4. View Aggregated History + +```javascript +const aggregated = await poolCanister.getAggregatedHistory(); +// Result: { Ok: { +// totalCycles: 5, +// totalParticipants: 324, +// totalIcpContributedE8S: 75_000_000_000n, // 750 ICP total +// totalFunnaiDistributed: 2_500_000_000n // 25 FUNNAI distributed +// }} +``` + +### 5. View Specific Past Cycle + +```javascript +const cycle = await poolCanister.getArchivedPoolCycle(3); +// Result: { Ok: { +// cycleId: 3, +// startTimestamp: 1736121600n, +// endTimestamp: 1736726400n, +// totalIcpContributedE8S: 12_000_000_000n, +// totalFunnaiDistributed: 400_000_000n, +// participantCount: 38 +// }} + +// Get participants for that cycle +const participants = await poolCanister.getArchivedCycleParticipants(3); +// Result: { Ok: [ +// [Principal.fromText("aaaaa-aa..."), { +// principal: Principal.fromText("aaaaa-aa..."), +// icpContributionE8S: 500_000_000n, +// funnaiDistribution: 16_666_666n, +// joinTimestamp: 1736121600n, +// participatedInLastWeek: true +// }], +// // ... more participants +// ]} +``` + +### 6. View Pool mAIners + +```javascript +const mainers = await poolCanister.getPoolMainers(); +// Result: { Ok: [ +// { +// address: "ryjl3-tyaaa-aaaaa-aaaba-cai", +// mainerType: { Own: null }, +// creationTimestamp: 1735000000n, +// currentCyclesBalance: 5_000_000_000_000n, +// cyclesBurnRate: 8_267_195n // cycles per second +// }, +// { +// address: "rrkah-fqaaa-aaaaa-aaaaq-cai", +// mainerType: { ShareAgent: null }, +// creationTimestamp: 1735100000n, +// currentCyclesBalance: 5_000_000_000_000n, +// cyclesBurnRate: 8_267_195n +// } +// ]} +``` + +### 7. Check Pool Balances + +```javascript +const balances = await poolCanister.getPoolBalances(); +// Result: { Ok: { +// icpBalanceE8S: 18_000_000_000n, // 180 ICP +// funnaiBalance: 500_000_000n // 5 FUNNAI accumulated +// }} +``` + +## For Administrators + +### 1. Initialize Pool (First Time) + +```bash +# Deploy the canister first +dfx deploy mAIningPool + +# Set the Game State canister ID +dfx canister call mAIningPool setGameStateCanisterId '("r5m5y-diaaa-aaaaa-qanaa-cai")' + +# Add mAIners to the pool +dfx canister call mAIningPool addPoolMainer '("ryjl3-tyaaa-aaaaa-aaaba-cai", variant { Own })' +dfx canister call mAIningPool addPoolMainer '("rrkah-fqaaa-aaaaa-aaaaq-cai", variant { Own })' +dfx canister call mAIningPool addPoolMainer '("r7inp-6aaaa-aaaaa-aaabq-cai", variant { ShareAgent })' + +# Start the first cycle (Monday 00:00:00 UTC to Sunday 23:59:59 UTC) +# Timestamps are in seconds since epoch +dfx canister call mAIningPool startNextPoolCycle '( + 1737331200, # Monday Jan 20, 2026 00:00:00 UTC + 1737935999 # Sunday Jan 26, 2026 23:59:59 UTC +)' +``` + +### 2. Weekly Cycle Transition (Every Monday) + +```bash +# This should be called after Sunday (start of Monday) to: +# 1. Distribute rewards from last week +# 2. Archive completed cycle +# 3. Start new cycle with committed participants +# 4. Top up mAIners and set burn rates + +dfx canister call mAIningPool startNextPoolCycle '( + 1737936000, # Monday Jan 27, 2026 00:00:00 UTC + 1738540799 # Sunday Feb 2, 2026 23:59:59 UTC +)' +``` + +### 3. Add/Remove mAIners + +```bash +# Add a new mAIner to the pool +dfx canister call mAIningPool addPoolMainer '( + "rkp4c-7iaaa-aaaaa-aaaca-cai", + variant { Own } +)' + +# Remove a mAIner from the pool +dfx canister call mAIningPool removePoolMainer '("rkp4c-7iaaa-aaaaa-aaaca-cai")' +``` + +### 4. Manual Balance Updates (if needed) + +```bash +# Update pool balances manually (for accounting corrections) +dfx canister call mAIningPool updatePoolBalances '( + 18_000_000_000, # ICP balance in e8s + 500_000_000 # FUNNAI balance +)' +``` + +## Automated Weekly Script Example + +```bash +#!/bin/bash +# weekly_pool_cycle.sh +# Run this script every Monday at 00:01 UTC + +# Calculate timestamps for the new week +WEEK_START=$(date -d "today 00:00:00" +%s) +WEEK_END=$(date -d "next Sunday 23:59:59" +%s) + +# Start the next pool cycle +dfx canister call mAIningPool startNextPoolCycle \ + "($WEEK_START, $WEEK_END)" \ + --identity admin + +# Log the result +echo "Pool cycle started: $WEEK_START to $WEEK_END" + +# Check stats +dfx canister call mAIningPool getCurrentPoolStats +``` + +## Frontend Integration Example (React) + +```typescript +import { Actor, HttpAgent } from "@dfinity/agent"; +import { idlFactory } from "./declarations/mAIningPool"; + +// Initialize actor +const agent = new HttpAgent({ host: "https://ic0.app" }); +const poolActor = Actor.createActor(idlFactory, { + agent, + canisterId: "YOUR_POOL_CANISTER_ID", +}); + +// Component for pool participation +function PoolParticipation() { + const [contribution, setContribution] = useState(""); + const [isNewParticipant, setIsNewParticipant] = useState(false); + + const handleContribute = async () => { + try { + // 1. Check if user needs to burn FUNNAI + const currentContribution = await poolActor.getMyCurrentPoolContribution(); + const needsBurn = currentContribution.Ok === 0n; + + // 2. Convert ICP to e8s + const amountE8S = BigInt(parseFloat(contribution) * 100_000_000); + + // 3. Approve ICP + await icpLedger.approve({ + spender: poolCanisterPrincipal, + amount: amountE8S, + }); + + // 4. If needed, approve and burn FUNNAI + if (needsBurn) { + await funnaiLedger.approve({ + spender: poolCanisterPrincipal, + amount: 10_000_000n, + }); + } + + // 5. Contribute to pool + const result = await poolActor.contributeToNextPool(amountE8S, needsBurn); + + if ("Ok" in result) { + alert(`Successfully contributed ${contribution} ICP!`); + } else { + alert(`Error: ${result.Err}`); + } + } catch (error) { + console.error("Contribution failed:", error); + } + }; + + return ( +
+ setContribution(e.target.value)} + placeholder="Amount in ICP" + min="1" + max="10000" + /> + +
+ ); +} +``` + +## Error Handling Examples + +```javascript +// Handle contribution errors +const result = await poolCanister.contributeToNextPool(50_000_000n, false); + +if ("Err" in result) { + switch (result.Err) { + case "Unauthorized": + console.error("Must be logged in"); + break; + case "Other": + console.error("Error:", result.Err.Other); + // Could be: "Contribution below minimum of 1 ICP" + // Or: "New participants must burn 10000000 FUNNAI to join" + break; + default: + console.error("Unknown error:", result.Err); + } +} +``` + +## Testing Scenarios + +### 1. Test First-Time Participation + +```bash +# As a new user +dfx identity use user1 +dfx canister call mAIningPool contributeToNextPool '(200_000_000, true)' +# Should succeed with FUNNAI burn + +dfx canister call mAIningPool contributeToNextPool '(200_000_000, false)' +# Should fail: "New participants must burn FUNNAI" +``` + +### 2. Test Contribution Limits + +```bash +# Try below minimum +dfx canister call mAIningPool contributeToNextPool '(50_000_000, false)' +# Should fail: "Contribution below minimum of 1 ICP" + +# Try above maximum +dfx canister call mAIningPool contributeToNextPool '(2_000_000_000_000, false)' +# Should fail: "Contribution exceeds maximum of 10000 ICP" +``` + +### 3. Test Continuous Participation + +```bash +# Week 1: contribute +dfx canister call mAIningPool contributeToNextPool '(200_000_000, true)' + +# Admin starts next cycle +dfx identity use admin +dfx canister call mAIningPool startNextPoolCycle '(...)' + +# Week 2: contribute again (no FUNNAI burn needed) +dfx identity use user1 +dfx canister call mAIningPool contributeToNextPool '(200_000_000, false)' +# Should succeed without burning +``` diff --git a/src/mAIningPool/canister_ids.json b/src/mAIningPool/canister_ids.json new file mode 100644 index 0000000..7897682 --- /dev/null +++ b/src/mAIningPool/canister_ids.json @@ -0,0 +1,10 @@ +{ + "funnai_maining_pool": { + "development": "", + "ic": "", + "local": "", + "testing": "", + "demo": "", + "prd": "" + } +} \ No newline at end of file diff --git a/src/mAIningPool/dfx.json b/src/mAIningPool/dfx.json new file mode 100644 index 0000000..33f37cc --- /dev/null +++ b/src/mAIningPool/dfx.json @@ -0,0 +1,48 @@ +{ + "version": 1, + "canisters": { + "funnai_maining_pool": { + "main": "src/Main.mo", + "type": "motoko", + "args": "--enhanced-orthogonal-persistence" + } + }, + "output_env_file": ".env", + "defaults": { + "build": { + "packtool": "mops sources" + } + }, + "networks": { + "development": { + "providers": [ + "https://icp0.io" + ], + "type": "persistent" + }, + "testing": { + "providers": [ + "https://icp0.io" + ], + "type": "persistent" + }, + "backup": { + "providers": [ + "https://icp0.io" + ], + "type": "persistent" + }, + "demo": { + "providers": [ + "https://icp0.io" + ], + "type": "persistent" + }, + "prd": { + "providers": [ + "https://icp0.io" + ], + "type": "persistent" + } + } +} \ No newline at end of file diff --git a/src/mAIningPool/mops.toml b/src/mAIningPool/mops.toml new file mode 100644 index 0000000..373589f --- /dev/null +++ b/src/mAIningPool/mops.toml @@ -0,0 +1,3 @@ +[dependencies] +base = "0.13.5" +uuid = "https://github.com/aviate-labs/uuid.mo#v0.2.0" diff --git a/src/mAIningPool/src/Main.mo b/src/mAIningPool/src/Main.mo new file mode 100644 index 0000000..b38190a --- /dev/null +++ b/src/mAIningPool/src/Main.mo @@ -0,0 +1,933 @@ +import D "mo:base/Debug"; +import Buffer "mo:base/Buffer"; +import Principal "mo:base/Principal"; +import Text "mo:base/Text"; +import HashMap "mo:base/HashMap"; +import List "mo:base/List"; +import Error "mo:base/Error"; +import Hash "mo:base/Hash"; +import Array "mo:base/Array"; +import Iter "mo:base/Iter"; +import Nat "mo:base/Nat"; +import Nat64 "mo:base/Nat64"; +import Int "mo:base/Int"; +import Time "mo:base/Time"; +import Option "mo:base/Option"; + +import Types "../../common/Types"; +import Constants "../../common/Constants"; + +import CMC "../../common/cycles-minting-canister-interface"; +import TokenLedger "../../common/icp-ledger-interface"; + +persistent actor class MainingPoolCanister() = this { + + var GAME_STATE_CANISTER_ID : Text = "r5m5y-diaaa-aaaaa-qanaa-cai"; // Corresponds to prd Game State canister + + public shared (msg) func setGameStateCanisterId(_game_state_canister_id : Text) : async Types.AuthRecordResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + GAME_STATE_CANISTER_ID := _game_state_canister_id; + let authRecord = { auth = "You set the game state canister for this canister." }; + return #Ok(authRecord); + }; + + public query (msg) func getGameStateCanisterId() : async Types.AuthRecordResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + let authRecord = { auth = "Game state canister id for this canister: " # GAME_STATE_CANISTER_ID }; + return #Ok(authRecord); + }; + + transient let CMC_ACTOR : CMC.CYCLES_MINTING_CANISTER = Types.CyclesMintingCanister_Actor; + + transient let ICP_LEDGER_ACTOR : TokenLedger.TOKEN_LEDGER = Types.IcpLedger_Actor; + + var TOKEN_LEDGER_CANISTER_ID : Text = "vpyot-zqaaa-aaaaa-qavaq-cai"; + + transient let TokenLedger_Actor : TokenLedger.TOKEN_LEDGER = actor (TOKEN_LEDGER_CANISTER_ID); + + // ------------------------------------------------------------------------------- + // Canister Endpoints + + public shared query (msg) func whoami() : async Principal { + return msg.caller; + }; + + // Function to verify that canister is up & running + public shared query func health() : async Types.StatusCodeRecordResult { + return #Ok({ status_code = 200 }); + }; + + public shared query (msg) func amiController() : async Types.AuthRecordResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + let authRecord = { auth = "You are a controller of this canister." }; + return #Ok(authRecord); + }; + + // ------------------------------------------------------------------------------- + // Pool Configuration Constants + + private let MIN_ICP_CONTRIBUTION_E8S : Nat = 100_000_000; // 1 ICP minimum + private let MAX_ICP_CONTRIBUTION_E8S : Nat = 10_000_000_000; // 100 ICP maximum + private let TREASURY_FEE_PERCENTAGE : Nat = 10; // 10% fee for treasury + + // Week boundaries in nanoseconds (Sunday end of day is commitment deadline) + // Using Monday 00:00:00 UTC as start of mAIning week + + // ------------------------------------------------------------------------------- + // Data Structures specific to mAIning Pool + + // Pool participant contribution record + type PoolParticipantEntry = { + principal : Principal; + icpContributionE8S : Nat; + funnaiDistributionE8S : Nat; + joinTimestamp : Nat64; + distributionTimestamp : Nat64; + }; + + // User history record + type UserHistoryEntry = { + poolCycleId : Nat; + poolCycleStartTimestamp : Nat64; + poolCycleEndTimestamp : Nat64; + poolParticipantEntry : PoolParticipantEntry; + }; + + // Pool cycle record + type PoolCycleRecord = { + poolCycleId : Nat; + poolCycleStartTimestamp : Nat64; + poolCycleEndTimestamp : Nat64; + totalIcpContributedE8S : Nat; + totalFunnaiDistributedE8S : Nat; + participantCount : Nat; + }; + + // mAIner owned by pool + type PoolMainerEntry = Types.OfficialMainerAgentCanister; + + // ------------------------------------------------------------------------------- + // Storage Variables + + // Current pool cycle ID + var currentCycleId : Nat = 0; + + // Current pool cycle timestamps + var currentCycleStartTimestamp : Nat64 = 0; + var currentCycleEndTimestamp : Nat64 = 0; + + // mAIners owned by the pool + var poolMainersStorageStable : [(Text, PoolMainerEntry)] = []; + transient var poolMainersStorage : HashMap.HashMap = HashMap.HashMap(0, Text.equal, Text.hash); + + // Current pool participants (active mAIning cycle) + var currentPoolParticipantsStable : [(Principal, PoolParticipantEntry)] = []; + transient var currentPoolParticipants : HashMap.HashMap = HashMap.HashMap(0, Principal.equal, Principal.hash); + + // Next pool participants (commitments for next mAIning cycle) + var nextPoolParticipantsStable : [(Principal, PoolParticipantEntry)] = []; + transient var nextPoolParticipants : HashMap.HashMap = HashMap.HashMap(0, Principal.equal, Principal.hash); + + // Archive of past pool mAIning cycles + var archivedPoolCyclesStable : [(Nat, PoolCycleRecord)] = []; + transient var archivedPoolCycles : HashMap.HashMap = HashMap.HashMap(0, Nat.equal, Hash.hash); + + // Archive of past participants per mAIning cycle + var archivedParticipantsStable : [(Nat, [(Principal, PoolParticipantEntry)])] = []; + transient var archivedParticipants : HashMap.HashMap = HashMap.HashMap(0, Nat.equal, Hash.hash); + + // User history mapping (Principal -> list of history entries) + var userHistoryStable : [(Principal, [UserHistoryEntry])] = []; + transient var userHistory : HashMap.HashMap> = HashMap.HashMap(0, Principal.equal, Principal.hash); + + // Balance tracking + var poolIcpBalanceE8S : Nat = 0; + var poolFunnaiBalanceE8S : Nat = 0; + + // Counters + var totalParticipantsAllTime : Nat = 0; + + // ------------------------------------------------------------------------------- + // Helper Functions + + // Calculate cycles from ICP using CMC conversion rate (accounting for treasury fee) + private func calculateCyclesFromIcp(icpE8S : Nat) : async Nat { + // Query the CMC for the current conversion rate + let queryResult = await CMC_ACTOR.get_icp_xdr_conversion_rate(); + + // Extract the conversion rate + let xdrPermyriadPerIcp = queryResult.data.xdr_permyriad_per_icp; + + // Constants + let CYCLES_PER_XDR : Nat = 1_000_000_000_000; // 1 trillion cycles per XDR + let E8S_PER_ICP : Nat = 100_000_000; // 10^8 e8s per ICP + + // Calculate total cycles from ICP + let totalCycles : Nat = (icpE8S * Nat64.toNat(xdrPermyriadPerIcp) * CYCLES_PER_XDR) / (10_000 * E8S_PER_ICP); + + // Apply treasury fee + let treasuryFee = (totalCycles * TREASURY_FEE_PERCENTAGE) / 100; + totalCycles - treasuryFee + }; + + // Get current timestamp in seconds + private func getCurrentTimestamp() : Nat64 { + let now = Time.now(); + Nat64.fromNat(Int.abs(now) / 1_000_000_000) + }; + + // Check if we're in commitment phase (before Monday start) + private func isInCommitmentPhase() : Bool { + let now = getCurrentTimestamp(); + // Commitment phase is until Sunday end of day (before current cycle ends) + now < currentCycleEndTimestamp + }; + + // Calculate cycles per mAIner based on total cycles and number of mAIners + private func calculateCyclesPerMainer(totalCycles : Nat, mainerCount : Nat) : Nat { + if (mainerCount == 0) { return 0 }; + totalCycles / mainerCount + }; + + // Calculate burn rate for a mAIner for one week + var cyclesBurnRateDefaultLow : Types.CyclesBurnRate = { + cycles : Nat = 1 * Constants.CYCLES_TRILLION; + timeInterval : Types.TimeInterval = #Daily; + }; + var cyclesBurnRateDefaultMid : Types.CyclesBurnRate = { + cycles : Nat = 2 * Constants.CYCLES_TRILLION; + timeInterval : Types.TimeInterval = #Daily; + }; + var cyclesBurnRateDefaultHigh : Types.CyclesBurnRate = { + cycles : Nat = 4 * Constants.CYCLES_TRILLION; + timeInterval : Types.TimeInterval = #Daily; + }; + var cyclesBurnRateDefaultVeryHigh : Types.CyclesBurnRate = { + cycles : Nat = 6 * Constants.CYCLES_TRILLION; + timeInterval : Types.TimeInterval = #Daily; + }; + + // Determine the CyclesBurnRateDefault tier based on cycles allocated for the week + private func determineBurnRateTier(cyclesForWeek : Nat) : Types.CyclesBurnRateDefault { + if (cyclesForWeek <= 7 * cyclesBurnRateDefaultLow.cycles) { + return #Low; + } else if (cyclesForWeek <= 7 * cyclesBurnRateDefaultMid.cycles) { + return #Mid; + } else if (cyclesForWeek <= 7 * cyclesBurnRateDefaultHigh.cycles) { + return #High; + } else { + return #VeryHigh; + }; + }; + + // Distribute FUNNAI rewards to participants based on their ICP contribution + private func calculateFunnaiDistribution(userContribution : Nat, totalContributions : Nat, totalFunnaiRewards : Nat) : Nat { + if (totalContributions == 0) { return 0 }; + (userContribution * totalFunnaiRewards) / totalContributions + }; + + // Helper function to get actual FUNNAI balance from ledger + private func getFunnaiBalance() : async Nat { + let funnaiAccount : TokenLedger.Account = { + owner = Principal.fromActor(this); + subaccount = null; + }; + let actualFunnaiBalance = await TokenLedger_Actor.icrc1_balance_of(funnaiAccount); + D.print("MiningPool: getFunnaiBalance - FUNNAI balance: " # debug_show(actualFunnaiBalance)); + return actualFunnaiBalance; + }; + + // Helper function to get actual ICP balance from ledger + private func getIcpBalance() : async Nat { + let icpAccount : TokenLedger.Account = { + owner = Principal.fromActor(this); + subaccount = null; + }; + let actualIcpBalance = await ICP_LEDGER_ACTOR.icrc1_balance_of(icpAccount); + D.print("MiningPool: getIcpBalance - ICP balance: " # debug_show(actualIcpBalance)); + return actualIcpBalance; + }; + + // Helper function to top up a mAIner with cycles via Game State + private func topUpMainerWithCycles(mainerEntry : PoolMainerEntry, icpAmountE8S : Nat) : async Types.TextResult { + D.print("MiningPool: topUpMainerWithCycles - mainerEntry: " # debug_show(mainerEntry)); + D.print("MiningPool: topUpMainerWithCycles - icpAmountE8S: " # debug_show(icpAmountE8S)); + + // Step 1: Transfer ICP to Game State canister + let gameStatePrincipal = Principal.fromText(GAME_STATE_CANISTER_ID); + let icpFee : Nat = 10_000; // 0.0001 ICP fee + + let transferArgs : TokenLedger.TransferArg = { + from_subaccount = null; + to = { + owner = gameStatePrincipal; + subaccount = null; + }; + amount = icpAmountE8S; + fee = ?icpFee; + memo = null; + created_at_time = null; + }; + + D.print("MiningPool: topUpMainerWithCycles - transferArgs: " # debug_show(transferArgs)); + + try { + let transferResult = await ICP_LEDGER_ACTOR.icrc1_transfer(transferArgs); + D.print("MiningPool: topUpMainerWithCycles - transferResult: " # debug_show(transferResult)); + + switch (transferResult) { + case (#Err(err)) { + D.print("MiningPool: topUpMainerWithCycles - ICP transfer failed: " # debug_show(err)); + return #Err(#Other("ICP transfer to Game State failed: " # debug_show(err))); + }; + case (#Ok(blockIndex)) { + D.print("MiningPool: topUpMainerWithCycles - ICP transfer successful, block: " # debug_show(blockIndex)); + + // Step 2: Call topUpCyclesForMainerAgent on Game State + let gameStateCanisterActor = actor (GAME_STATE_CANISTER_ID) : Types.GameStateCanister_Actor; + + let topUpInput : Types.MainerAgentTopUpInput = { + paymentTransactionBlockId = Nat64.fromNat(blockIndex); + mainerAgent = mainerEntry; + }; + + D.print("MiningPool: topUpMainerWithCycles - topUpInput: " # debug_show(topUpInput)); + + let topUpResult = await gameStateCanisterActor.topUpCyclesForMainerAgent(topUpInput); + D.print("MiningPool: topUpMainerWithCycles - topUpResult: " # debug_show(topUpResult)); + + switch (topUpResult) { + case (#Err(err)) { + D.print("MiningPool: topUpMainerWithCycles - Top up failed: " # debug_show(err)); + return #Err(#Other("Top up failed: " # debug_show(err))); + }; + case (#Ok(results)) { + D.print("MiningPool: topUpMainerWithCycles - Top up successful"); + return #Ok("Topped up mAIner " # mainerEntry.address # " successfully"); + }; + }; + }; + }; + } catch (e) { + D.print("MiningPool: topUpMainerWithCycles - Exception: " # Error.message(e)); + return #Err(#Other("Exception during top up: " # Error.message(e))); + }; + }; + + // ------------------------------------------------------------------------------- + // Pool Participant Functions + + // Contribute to next pool + public shared (msg) func contributeToNextPool(icpAmountE8S : Nat) : async Types.NatResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + + // Check minimum and maximum contribution + if (icpAmountE8S < MIN_ICP_CONTRIBUTION_E8S) { + return #Err(#Other("Contribution below minimum of " # Nat.toText(MIN_ICP_CONTRIBUTION_E8S / 100_000_000) # " ICP")); + }; + + if (icpAmountE8S > MAX_ICP_CONTRIBUTION_E8S) { + return #Err(#Other("Contribution exceeds maximum of " # Nat.toText(MAX_ICP_CONTRIBUTION_E8S / 100_000_000) # " ICP")); + }; + + // Retrieve approved ICP from contributor using icrc2_transfer_from + let icpFee : Nat = 10_000; // 0.0001 ICP fee + let transferFromArgs : TokenLedger.TransferFromArgs = { + from = { owner = msg.caller; subaccount = null }; + to = { owner = Principal.fromActor(this); subaccount = null }; + amount = icpAmountE8S; + fee = ?icpFee; + memo = null; + created_at_time = null; + spender_subaccount = null; + }; + + try { + let icpTransferResult : TokenLedger.Result_3 = await ICP_LEDGER_ACTOR.icrc2_transfer_from(transferFromArgs); + D.print("MiningPool: contributeToNextPool - icpTransferResult: " # debug_show(icpTransferResult)); + switch (icpTransferResult) { + case (#Err(transferError)) { + D.print("MiningPool: contributeToNextPool - ICP transfer failed: " # debug_show(transferError)); + return #Err(#Other("ICP payment transfer failed: " # debug_show(transferError))); + }; + case (#Ok(icpBlockIndex)) { + D.print("MiningPool: contributeToNextPool - ICP transferred successfully, block: " # debug_show(icpBlockIndex)); + // Continue with contribution processing + }; + }; + } catch (e) { + D.print("MiningPool: contributeToNextPool - Failed icrc2_transfer_from: " # Error.message(e)); + return #Err(#Other("Failed icrc2_transfer_from: " # Error.message(e))); + }; + + // Check if user already has an entry for next pool + switch (nextPoolParticipants.get(msg.caller)) { + case (?existingEntry) { + // User already has a commitment, add to it + let updatedEntry : PoolParticipantEntry = { + principal = existingEntry.principal; + icpContributionE8S = existingEntry.icpContributionE8S + icpAmountE8S; + funnaiDistributionE8S = existingEntry.funnaiDistributionE8S; + joinTimestamp = existingEntry.joinTimestamp; + distributionTimestamp = existingEntry.distributionTimestamp; + }; + nextPoolParticipants.put(msg.caller, updatedEntry); + + // Update pool ICP balance + poolIcpBalanceE8S := poolIcpBalanceE8S + icpAmountE8S; + + return #Ok(updatedEntry.icpContributionE8S); + }; + case null { + // New entry for next pool + let newEntry : PoolParticipantEntry = { + principal = msg.caller; + icpContributionE8S = icpAmountE8S; + funnaiDistributionE8S = 0; + joinTimestamp = getCurrentTimestamp(); + distributionTimestamp = 0; + }; + nextPoolParticipants.put(msg.caller, newEntry); + + // Update pool ICP balance + poolIcpBalanceE8S := poolIcpBalanceE8S + icpAmountE8S; + + // Track total participants + totalParticipantsAllTime := totalParticipantsAllTime + 1; + + return #Ok(newEntry.icpContributionE8S); + }; + }; + }; + + // Query: Get my contribution for current pool + public shared query (msg) func getMyCurrentPoolContribution() : async Types.NatResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + + switch (currentPoolParticipants.get(msg.caller)) { + case (?entry) { + return #Ok(entry.icpContributionE8S); + }; + case null { + return #Ok(0); + }; + }; + }; + + // Query: Get my contribution for next pool + public shared query (msg) func getMyNextPoolContribution() : async Types.NatResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + + switch (nextPoolParticipants.get(msg.caller)) { + case (?entry) { + return #Ok(entry.icpContributionE8S); + }; + case null { + return #Ok(0); + }; + }; + }; + + // Query: Get my past contributions and distributions + public shared query (msg) func getMyHistory() : async Types.Result<[UserHistoryEntry], Types.ApiError> { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + + switch (userHistory.get(msg.caller)) { + case (?historyBuffer) { + return #Ok(Buffer.toArray(historyBuffer)); + }; + case null { + return #Ok([]); + }; + }; + }; + + // Query: Get current pool statistics + public shared query func getCurrentPoolStats() : async Types.Result<{ + cycleId : Nat; + startTimestamp : Nat64; + endTimestamp : Nat64; + participantCount : Nat; + totalIcpContributedE8S : Nat; + totalFunnaiRewardsAccumulated : Nat; + }, Types.ApiError> { + let participantCount = currentPoolParticipants.size(); + var totalIcp : Nat = 0; + + for ((_, entry) in currentPoolParticipants.entries()) { + totalIcp := totalIcp + entry.icpContributionE8S; + }; + + return #Ok({ + cycleId = currentCycleId; + startTimestamp = currentCycleStartTimestamp; + endTimestamp = currentCycleEndTimestamp; + participantCount = participantCount; + totalIcpContributedE8S = totalIcp; + totalFunnaiRewardsAccumulated = poolFunnaiBalanceE8S; + }); + }; + + // Query: Get next pool statistics + public shared query func getNextPoolStats() : async Types.Result<{ + cycleId : Nat; + participantCount : Nat; + totalIcpCommittedE8S : Nat; + commitmentDeadline : Nat64; + }, Types.ApiError> { + let participantCount = nextPoolParticipants.size(); + var totalIcp : Nat = 0; + + for ((_, entry) in nextPoolParticipants.entries()) { + totalIcp := totalIcp + entry.icpContributionE8S; + }; + + return #Ok({ + cycleId = currentCycleId + 1; + participantCount = participantCount; + totalIcpCommittedE8S = totalIcp; + commitmentDeadline = currentCycleEndTimestamp; + }); + }; + + // ------------------------------------------------------------------------------- + // Admin Functions + + // Start next pool cycle - this distributes rewards and sets up the new cycle + public shared (msg) func startNextPoolCycle(weekStartTimestamp : Nat64, weekEndTimestamp : Nat64) : async Types.TextResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + + // Step 1: Distribute FUNNAI rewards to current pool participants + var totalCurrentIcp : Nat = 0; + for ((_, entry) in currentPoolParticipants.entries()) { + totalCurrentIcp := totalCurrentIcp + entry.icpContributionE8S; + }; + + // Get actual FUNNAI balance from ledger + var totalFunnaiToDistribute = poolFunnaiBalanceE8S; + try { + totalFunnaiToDistribute := await getFunnaiBalance(); + D.print("MiningPool: startNextPoolCycle - updated FUNNAI balance: " # debug_show(totalFunnaiToDistribute)); + } catch (e) { + D.print("MiningPool: startNextPoolCycle - Failed to update FUNNAI balance: " # Error.message(e)); + return #Err(#Other("Failed to get FUNNAI balance: " # Error.message(e))); + }; + + // Calculate and update distributions for each participant + let distributionTime = getCurrentTimestamp(); + for ((principal, entry) in currentPoolParticipants.entries()) { + let distribution = calculateFunnaiDistribution( + entry.icpContributionE8S, + totalCurrentIcp, + totalFunnaiToDistribute + ); + + let updatedEntry : PoolParticipantEntry = { + principal = entry.principal; + icpContributionE8S = entry.icpContributionE8S; + funnaiDistributionE8S = distribution; + joinTimestamp = entry.joinTimestamp; + distributionTimestamp = distributionTime; + }; + + currentPoolParticipants.put(principal, updatedEntry); + + // Execute actual FUNNAI transfer to participant + if (distribution > 0) { + let transferArgs : TokenLedger.TransferArg = { + from_subaccount = null; + to = { + owner = principal; + subaccount = null; + }; + amount = distribution; + fee = null; + memo = null; + created_at_time = null; + }; + D.print("MiningPool: startNextPoolCycle - transferArgs: " # debug_show(transferArgs)); + + try { + let transferResult = await TokenLedger_Actor.icrc1_transfer(transferArgs); + D.print("MiningPool: startNextPoolCycle - transferResult: " # debug_show(transferResult)); + + switch (transferResult) { + case (#Ok(blockIndex)) { + D.print("MiningPool: startNextPoolCycle - FUNNAI transfer successful to " # debug_show(principal) # ", block: " # debug_show(blockIndex)); + }; + case (#Err(err)) { + D.print("MiningPool: startNextPoolCycle - FUNNAI transfer failed to " # debug_show(principal) # ": " # debug_show(err)); + }; + }; + } catch (e) { + D.print("MiningPool: startNextPoolCycle - Failed to call ledger for " # debug_show(principal)); + }; + }; + }; + + // Reset FUNNAI balance after distribution + poolFunnaiBalanceE8S := 0; + + // Step 2: Archive current pool participants and cycle data + let cycleRecord : PoolCycleRecord = { + poolCycleId = currentCycleId; + poolCycleStartTimestamp = currentCycleStartTimestamp; + poolCycleEndTimestamp = currentCycleEndTimestamp; + totalIcpContributedE8S = totalCurrentIcp; + totalFunnaiDistributedE8S = totalFunnaiToDistribute; + participantCount = currentPoolParticipants.size(); + }; + + archivedPoolCycles.put(currentCycleId, cycleRecord); + + // Archive participants for this cycle + let participantsArray = Iter.toArray(currentPoolParticipants.entries()); + archivedParticipants.put(currentCycleId, participantsArray); + + // Step 3: Add to user history + for ((principal, entry) in currentPoolParticipants.entries()) { + let historyEntry : UserHistoryEntry = { + poolCycleId = currentCycleId; + poolCycleStartTimestamp = currentCycleStartTimestamp; + poolCycleEndTimestamp = currentCycleEndTimestamp; + poolParticipantEntry = entry; + }; + + switch (userHistory.get(principal)) { + case (?buffer) { + buffer.add(historyEntry); + }; + case null { + let newBuffer = Buffer.Buffer(1); + newBuffer.add(historyEntry); + userHistory.put(principal, newBuffer); + }; + }; + }; + + // Step 4: Move next pool participants to current pool participants + currentPoolParticipants := nextPoolParticipants; + + // Step 5: Reset next pool participants + nextPoolParticipants := HashMap.HashMap(0, Principal.equal, Principal.hash); + + // Step 6: Update cycle IDs and timestamps + currentCycleId := currentCycleId + 1; + currentCycleStartTimestamp := weekStartTimestamp; + currentCycleEndTimestamp := weekEndTimestamp; + + // Step 7: Calculate cycles for mAIners and set burn rates + var totalNextIcp : Nat = 0; + for ((_, entry) in currentPoolParticipants.entries()) { + totalNextIcp := totalNextIcp + entry.icpContributionE8S; + }; + + var totalCyclesForWeek = 0; + try { + totalCyclesForWeek := await calculateCyclesFromIcp(totalNextIcp); + D.print("MiningPool: startNextPoolCycle - calculated cycles from ICP: " # debug_show(totalCyclesForWeek)); + } catch (e) { + D.print("MiningPool: startNextPoolCycle - Failed to calculate cycles from ICP: " # Error.message(e)); + return #Err(#Other("Failed to calculate cycles from ICP: " # Error.message(e))); + }; + + let mainerCount = poolMainersStorage.size(); + + if (mainerCount > 0) { + let cyclesPerMainer = calculateCyclesPerMainer(totalCyclesForWeek, mainerCount); + let burnRatePerMainer = determineBurnRateTier(cyclesPerMainer); + let icpPerMainer = totalNextIcp / mainerCount; + + for ((address, mainerEntry) in poolMainersStorage.entries()) { + // Top up mAIner with cyclesPerMainer via Game State + let topUpResult = await topUpMainerWithCycles(mainerEntry, icpPerMainer); + D.print("MiningPool: startNextPoolCycle - topUpResult for " # address # ": " # debug_show(topUpResult)); + + // Set burn rate based on cyclesPerMainer on mAIner canister + let mainerCanisterActor = actor (mainerEntry.address) : Types.MainerAgentCtrlbCanister; + let settingInput : Types.MainerAgentSettingsInput = { + cyclesBurnRate : Types.CyclesBurnRateDefault = burnRatePerMainer; + }; + try { + let updateResult = await mainerCanisterActor.updateAgentSettings(settingInput); + D.print("MiningPool: startNextPoolCycle - updateSettings result for " # address # ": " # debug_show(updateResult)); + } catch (e) { + D.print("MiningPool: startNextPoolCycle - Error calling updateAgentSettings for " # address # ": " # Error.message(e)); + }; + }; + }; + + return #Ok("Pool cycle " # Nat.toText(currentCycleId) # " started successfully. Participants: " # Nat.toText(currentPoolParticipants.size())); + }; + + // Admin function: Add a mAIner to the pool + public shared (msg) func addPoolMainer( + mainerEntry : Types.OfficialMainerAgentCanister + ) : async Types.TextResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + + // Check if mAIner already exists + switch (poolMainersStorage.get(mainerEntry.address)) { + case (?_) { + return #Err(#Other("mAIner already exists in pool")); + }; + case null { + poolMainersStorage.put(mainerEntry.address, mainerEntry); + return #Ok("mAIner " # mainerEntry.address # " added to pool"); + }; + }; + }; + + // Admin function: Remove a mAIner from the pool + public shared (msg) func removePoolMainer(mainerAddress : Text) : async Types.TextResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + + switch (poolMainersStorage.remove(mainerAddress)) { + case (?_) { + return #Ok("mAIner " # mainerAddress # " removed from pool"); + }; + case null { + return #Err(#Other("mAIner not found in pool")); + }; + }; + }; + + // Admin function: Update pool's ICP and FUNNAI balances + public shared (msg) func updatePoolBalances(icpBalanceE8S : Nat, funnaiBalance : Nat) : async Types.TextResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + + // Get balances from ledgers + poolIcpBalanceE8S := await getIcpBalance(); + poolFunnaiBalanceE8S := await getFunnaiBalance(); + + return #Ok("Pool balances updated: ICP=" # Nat.toText(icpBalanceE8S) # " e8s, FUNNAI=" # Nat.toText(funnaiBalance)); + }; + + // ------------------------------------------------------------------------------- + // Query Functions for Historical Data + + // Query: Get archived pool cycle details + public query (msg) func getArchivedPoolCycle(cycleId : Nat) : async Types.Result { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + switch (archivedPoolCycles.get(cycleId)) { + case (?record) { + return #Ok(record); + }; + case null { + return #Err(#Other("Pool cycle not found")); + }; + }; + }; + + // Query: Get all archived pool cycles + public query (msg)func getAllArchivedPoolCycles() : async Types.Result<[PoolCycleRecord], Types.ApiError> { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + let cycles = Buffer.Buffer(archivedPoolCycles.size()); + for ((_, record) in archivedPoolCycles.entries()) { + cycles.add(record); + }; + return #Ok(Buffer.toArray(cycles)); + }; + + // Query: Get participants for a specific archived cycle + public query (msg) func getArchivedCycleParticipants(cycleId : Nat) : async Types.Result<[(Principal, PoolParticipantEntry)], Types.ApiError> { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + switch (archivedParticipants.get(cycleId)) { + case (?participants) { + return #Ok(participants); + }; + case null { + return #Err(#Other("Participants not found for cycle")); + }; + }; + }; + + // Query: Get aggregated past contributions and distributions + public shared query func getAggregatedHistory() : async Types.Result<{ + totalCycles : Nat; + totalParticipants : Nat; + totalIcpContributedE8S : Nat; + totalFunnaiDistributed : Nat; + }, Types.ApiError> { + var totalIcp : Nat = 0; + var totalFunnai : Nat = 0; + + for ((_, record) in archivedPoolCycles.entries()) { + totalIcp := totalIcp + record.totalIcpContributedE8S; + totalFunnai := totalFunnai + record.totalFunnaiDistributedE8S; + }; + + return #Ok({ + totalCycles = currentCycleId; + totalParticipants = totalParticipantsAllTime; + totalIcpContributedE8S = totalIcp; + totalFunnaiDistributed = totalFunnai; + }); + }; + + // Query: Get pool mAIners + public query (msg) func getPoolMainers() : async Types.Result<[PoolMainerEntry], Types.ApiError> { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + let mainers = Buffer.Buffer(poolMainersStorage.size()); + for ((_, mainer) in poolMainersStorage.entries()) { + mainers.add(mainer); + }; + return #Ok(Buffer.toArray(mainers)); + }; + + // Query: Get specific pool mAIner + public query (msg) func getPoolMainer(mainerAddress : Text) : async Types.Result { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + switch (poolMainersStorage.get(mainerAddress)) { + case (?mainer) { + return #Ok(mainer); + }; + case null { + return #Err(#Other("mAIner not found")); + }; + }; + }; + + // Query: Get pool balances + public shared query func getPoolBalances() : async Types.Result<{ + icpBalanceE8S : Nat; + funnaiBalance : Nat; + }, Types.ApiError> { + return #Ok({ + icpBalanceE8S = poolIcpBalanceE8S; + funnaiBalance = poolFunnaiBalanceE8S; + }); + }; + + // Query: Get pool configuration + public shared query func getPoolConfiguration() : async Types.Result<{ + minIcpContributionE8S : Nat; + maxIcpContributionE8S : Nat; + treasuryFeePercentage : Nat; + currentCycleId : Nat; + nextCycleId : Nat; + totalPoolCycles : Nat; + totalParticipantsAllTime : Nat; + }, Types.ApiError> { + return #Ok({ + minIcpContributionE8S = MIN_ICP_CONTRIBUTION_E8S; + maxIcpContributionE8S = MAX_ICP_CONTRIBUTION_E8S; + treasuryFeePercentage = TREASURY_FEE_PERCENTAGE; + currentCycleId = currentCycleId; + nextCycleId = currentCycleId + 1; + totalPoolCycles = currentCycleId; + totalParticipantsAllTime = totalParticipantsAllTime; + }); + }; + + // ------------------------------------------------------------------------------- + // System Functions for Upgrades + + system func preupgrade() { + poolMainersStorageStable := Iter.toArray(poolMainersStorage.entries()); + currentPoolParticipantsStable := Iter.toArray(currentPoolParticipants.entries()); + nextPoolParticipantsStable := Iter.toArray(nextPoolParticipants.entries()); + archivedPoolCyclesStable := Iter.toArray(archivedPoolCycles.entries()); + archivedParticipantsStable := Iter.toArray(archivedParticipants.entries()); + + // Convert user history buffers to arrays + let userHistoryArray = Buffer.Buffer<(Principal, [UserHistoryEntry])>(userHistory.size()); + for ((principal, historyBuffer) in userHistory.entries()) { + userHistoryArray.add((principal, Buffer.toArray(historyBuffer))); + }; + userHistoryStable := Buffer.toArray(userHistoryArray); + }; + + system func postupgrade() { + poolMainersStorage := HashMap.fromIter(poolMainersStorageStable.vals(), poolMainersStorageStable.size(), Text.equal, Text.hash); + currentPoolParticipants := HashMap.fromIter(currentPoolParticipantsStable.vals(), currentPoolParticipantsStable.size(), Principal.equal, Principal.hash); + nextPoolParticipants := HashMap.fromIter(nextPoolParticipantsStable.vals(), nextPoolParticipantsStable.size(), Principal.equal, Principal.hash); + archivedPoolCycles := HashMap.fromIter(archivedPoolCyclesStable.vals(), archivedPoolCyclesStable.size(), Nat.equal, Hash.hash); + archivedParticipants := HashMap.fromIter(archivedParticipantsStable.vals(), archivedParticipantsStable.size(), Nat.equal, Hash.hash); + + // Convert user history arrays back to buffers + for ((principal, historyArray) in userHistoryStable.vals()) { + let historyBuffer = Buffer.Buffer(historyArray.size()); + for (entry in historyArray.vals()) { + historyBuffer.add(entry); + }; + userHistory.put(principal, historyBuffer); + }; + + // Clear stable storage + poolMainersStorageStable := []; + currentPoolParticipantsStable := []; + nextPoolParticipantsStable := []; + archivedPoolCyclesStable := []; + archivedParticipantsStable := []; + userHistoryStable := []; + }; +}; \ No newline at end of file