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