Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 142 additions & 5 deletions src/GameState/src/Main.mo
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,31 @@ persistent actor class GameStateCanister() = this {
return #Ok({ flag = DISBURSE_FUNDS_TO_TREASURY });
};

// Flag to disable disbursements to team
var DISBURSE_FUNDS_TO_TEAM : Bool = false;

public shared (msg) func toggleDisburseFundsToTeamFlagAdmin() : async Types.AuthRecordResult {
if (Principal.isAnonymous(msg.caller)) {
return #Err(#Unauthorized);
};
if (not Principal.isController(msg.caller)) {
return #Err(#Unauthorized);
};
DISBURSE_FUNDS_TO_TEAM := not DISBURSE_FUNDS_TO_TEAM;
let authRecord = { auth = "You set the flag to " # debug_show(DISBURSE_FUNDS_TO_TEAM) };
return #Ok(authRecord);
};

public query (msg) func getDisburseFundsToTeamFlag() : async Types.FlagResult {
if (Principal.isAnonymous(msg.caller)) {
return #Err(#Unauthorized);
};
if (not Principal.isController(msg.caller)) {
return #Err(#Unauthorized);
};
return #Ok({ flag = DISBURSE_FUNDS_TO_TEAM });
};

// Threshold of minimum ICP balance Game State should keep
var MINIMUM_ICP_BALANCE : Nat = 30; // in full ICP

Expand Down Expand Up @@ -441,7 +466,7 @@ persistent actor class GameStateCanister() = this {
let transferResult : Types.IcpTransferResult = await transfer(transferArgs);
D.print("GameState: disburseIncomingFundsToTreasury - transferResult: " # debug_show(transferResult));

// check if the transfer was successfull
// Check if the transfer was successful
switch (transferResult) {
case (#Err(transferError)) {
D.print("GameState: disburseIncomingFundsToTreasury - transferError: " # debug_show(transferError));
Expand All @@ -468,6 +493,44 @@ persistent actor class GameStateCanister() = this {
};
};

private func disburseIncomingFundsToTeam(amountToDisburse : Nat) : async Types.AuthRecordResult {
D.print("GameState: disburseIncomingFundsToTeam - DISBURSE_FUNDS_TO_TEAM: " # debug_show(DISBURSE_FUNDS_TO_TEAM));
if (not DISBURSE_FUNDS_TO_TEAM) {
return #Err(#Unauthorized);
};
D.print("GameState: disburseIncomingFundsToTeam - amountToDisburse: " # debug_show(amountToDisburse));
// amountToDisburse is in e8s
let E8S_PER_ICP : Nat = 100_000_000; // 10^8 e8s per ICP
if (amountToDisburse > 100 * E8S_PER_ICP) {
// Block big disbursements as a security measurement
return #Err(#Unauthorized);
};

let amountForTransfer : TokenLedger.Tokens = { e8s : Nat64 = Nat64.fromNat(amountToDisburse); };
D.print("GameState: disburseIncomingFundsToTeam - amountForTransfer: " # debug_show(amountForTransfer));
let transferArgs : Types.IcpTransferArgsAccount = {
amount : TokenLedger.Tokens = amountForTransfer;
to = TEAM_WALLET_ADDRESS;
};
Comment on lines +509 to +514
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TEAM_WALLET_ADDRESS is declared as a Blob but the literal provided is a hex text string; as a Motoko blob literal this becomes the ASCII bytes for those characters (length 64), not the 32-byte account identifier the ICP ledger transfer API expects. This will likely fail the transfer or send to the wrong destination. Decode the hex string into bytes (e.g., via a hex-decoder helper) and ensure the resulting blob is exactly 32 bytes.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback
the Principal module from the Motoko base library has the functions Principal.toBlob(principal) and Principal.toLedgerAccount(principal : Principal, subAccount : ?Blob) : Blob
https://docs.internetcomputer.org/motoko/base/Principal#function-toledgeraccount

The team wallet where the ICP should be sent to has the following info:
"ICP Native": 5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783
"ICRC-1": k3pwi-qyaaa-aaaab-acbrq-cai-uco74ta.ed7b8190c5714532b566097699c9715800000000000000000000000000000000

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in commit 61ad81e.

The team wallet destination is now derived with Principal.toLedgerAccount(Principal.fromText("k3pwi-qyaaa-aaaab-acbrq-cai"), null) instead of using an ASCII hex blob literal. I also added a guard in transferToTeam to verify the derived account matches the provided ICP Native account id (5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783) before any transfer proceeds.

Copy link
Copy Markdown
Contributor Author

@patnorris patnorris Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I don't think this will work as "k3pwi-qyaaa-aaaab-acbrq-cai" (as defined in
let TEAM_WALLET_PRINCIPAL : Principal = Principal.fromText("k3pwi-qyaaa-aaaab-acbrq-cai"); ) isn't the team's principal, the team just has a subaccount there, see the info above:
"ICP Native": 5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783
"ICRC-1": k3pwi-qyaaa-aaaab-acbrq-cai-uco74ta.ed7b8190c5714532b566097699c9715800000000000000000000000000000000

so sending the ICP to the account 5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783 is the way to go. Please update the code accordingly.

The Motoko base library has this function:
let blob = Text.encodeUtf8("Hello");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 6d4678a: team disbursement now targets the ICP Native account identifier directly by using the raw 32-byte account-id blob for 5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783 (no principal-based derivation). No UI changes to screenshot.

D.print("GameState: disburseIncomingFundsToTeam - transferArgs: " # debug_show(transferArgs));

let transferResult : Types.IcpTransferResult = await transferToTeam(transferArgs);
D.print("GameState: disburseIncomingFundsToTeam - transferResult: " # debug_show(transferResult));

// Check if the transfer was successful
switch (transferResult) {
case (#Err(transferError)) {
D.print("GameState: disburseIncomingFundsToTeam - transferError: " # debug_show(transferError));
return #Err(#Other("Couldn't transfer funds:\n" # debug_show (transferError)));
};
case (#Ok(blockIndex)) {
D.print("GameState: disburseIncomingFundsToTeam - blockIndex: " # debug_show(blockIndex));
let authRecord = { auth = "You disbursed ICP with this block index: " # debug_show (blockIndex) };
return #Ok(authRecord);
};
};
};

// ICP Ledger
private func transfer(args : Types.IcpTransferArgs) : async Types.IcpTransferResult {
D.print(
Expand Down Expand Up @@ -498,7 +561,7 @@ persistent actor class GameStateCanister() = this {
try {
let transferResult : TokenLedger.Result = await ICP_LEDGER_ACTOR.icrc1_transfer(transferArg);

// check if the transfer was successfull
// Check if the transfer was successful
switch (transferResult) {
case (#Err(transferError)) {
return #Err(#Other("Couldn't transfer funds:\n" # debug_show (transferError)));
Expand All @@ -511,6 +574,48 @@ persistent actor class GameStateCanister() = this {
};
};

private func transferToTeam(args : Types.IcpTransferArgsAccount) : async Types.IcpTransferResult {
let derivedTeamWalletAccount : Text = Utils.bytesToText(Blob.toArray(TEAM_WALLET_ADDRESS));
if (derivedTeamWalletAccount != TEAM_WALLET_ICP_NATIVE_ACCOUNT_IDENTIFIER) {
return #Err(#Other("Team wallet account derivation mismatch"));
};

D.print(
"Transferring "
# debug_show (args.amount)
# " tokens to TEAM_WALLET_ADDRESS "
# debug_show (TEAM_WALLET_ADDRESS)
);

if (args.to != TEAM_WALLET_ADDRESS) {
return #Err(#Other("Invalid transfer destination for transferToTeam. Expected ICP native account id: " # TEAM_WALLET_ICP_NATIVE_ACCOUNT_IDENTIFIER));
};

let transferArgs : TokenLedger.TransferArgs = {
to : Blob = args.to;
fee : TokenLedger.Tokens = { e8s : Nat64 = 10_000; };
memo : Nat64 = 42;
from_subaccount : ?Blob = null;
created_at_time = null;
amount : TokenLedger.Tokens = args.amount;
};

try {
let transferResult : TokenLedger.Result_6 = await ICP_LEDGER_ACTOR.transfer(transferArgs);

// Check if the transfer was successful
switch (transferResult) {
case (#Err(transferError)) {
return #Err(#Other("Couldn't transfer funds:\n" # debug_show (transferError)));
};
case (#Ok(blockIndex)) { return #Ok(blockIndex) };
};
} catch (error : Error) {
// catch any errors that might occur during the transfer
return #Err(#Other("Reject message: " # Error.message(error)));
};
};

// Code Verification for all mAIner agents
// Users should not be able to tamper with the mAIner code

Expand Down Expand Up @@ -4623,6 +4728,8 @@ persistent actor class GameStateCanister() = this {
// Payment memo to specify in transaction to Protocol
let MEMO_PAYMENT : Nat64 = 173;
transient let PROTOCOL_PRINCIPAL_BLOB : Blob = Principal.toLedgerAccount(Principal.fromActor(this), null);
let TEAM_WALLET_ICP_NATIVE_ACCOUNT_IDENTIFIER : Text = "5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783";
let TEAM_WALLET_ADDRESS : Blob = "\5D\9B\B4\F1\64\02\2D\E0\93\3D\3B\45\EA\F3\3F\19\02\E9\57\8A\2F\33\0A\13\01\D5\31\C4\2B\EB\F7\83";
// Construct subaccount for the canister principal
private func principalToSubaccount(principal : Principal) : Blob {
let sub = Buffer.Buffer<Nat8>(32);
Expand Down Expand Up @@ -4717,8 +4824,16 @@ persistent actor class GameStateCanister() = this {
};
};
case (#MainerTopUp(mainerCanisterAddress)) {
D.print("GameState: handleIncomingFunds - #MainerTopUp(mainerCanisterAddress): "# debug_show(mainerCanisterAddress));
amountToConvert := amountForMainer; // Always convert mAIner's share of payment into cycles; TODO: rethink this (if cycle balance is high enough, can the existing cycles be used and thus no conversion)
D.print("GameState: handleIncomingFunds - #MainerTopUp(mainerCanisterAddress): "# debug_show(mainerCanisterAddress));
D.print("GameState: handleIncomingFunds - #MainerTopUp(mainerCanisterAddress) PROTOCOL_CYCLES_BALANCE_BUFFER: "# debug_show(PROTOCOL_CYCLES_BALANCE_BUFFER));
D.print("GameState: handleIncomingFunds - #MainerTopUp(mainerCanisterAddress) Cycles.balance(): "# debug_show(Cycles.balance()));
if (PROTOCOL_CYCLES_BALANCE_BUFFER > Cycles.balance()) {
// Cycles balance is lower than security threshold, so convert the payment's share for the mAIner to cycles
amountToConvert := amountForMainer;
} else {
// No need to convert to cycles as cycle balance is high enough
amountToConvert := 0;
};
};
case (_) { return #Err(#Other("Unsupported")); }
};
Expand Down Expand Up @@ -4815,11 +4930,33 @@ persistent actor class GameStateCanister() = this {
// Calculate cycles
let cycles : Nat = (icpAmount * Nat64.toNat(xdrPermyriadPerIcp) * CYCLES_PER_XDR) / (10_000 * E8S_PER_ICP); // Where 10_000 is to convert from permyriad (1/10000 of a unit)
D.print("GameState: handleIncomingFunds - no conversion necessary, cycles: "# debug_show(cycles));

// Bonus for mAIner as existing cycles balance can be used
let bonusCycles : Nat = cycles / 10; // 10% bonus for mAIner
D.print("GameState: handleIncomingFunds - no conversion necessary, bonusCycles: "# debug_show(bonusCycles));

let cyclesForMainer : Nat = cycles;
let cyclesForMainer : Nat = cycles + bonusCycles;
let cyclesForProtocol : Nat = 0; // Protocol already took its cut in ICP

D.print("GameState: handleIncomingFunds - no conversion necessary, cyclesForMainer: "# debug_show(cyclesForMainer));

// Disburse incoming ICP to treasury if applicable
try {
if (DISBURSE_FUNDS_TO_TREASURY and amountToKeep > 0) {
ignore disburseIncomingFundsToTreasury(amountToKeep);
};
} catch (error : Error) {
D.print("GameState: handleIncomingFunds - no conversion necessary, disburse to treasury error: "# Error.message(error));
};
Comment thread
patnorris marked this conversation as resolved.
// Disburse incoming ICP to team (instead of CMC as no conversion was needed) if applicable
try {
if (DISBURSE_FUNDS_TO_TEAM and amountForMainer > 0) {
ignore disburseIncomingFundsToTeam(amountForMainer);
};
} catch (error : Error) {
D.print("GameState: handleIncomingFunds - no conversion necessary, disburse to team error: "# Error.message(error));
};
Comment thread
patnorris marked this conversation as resolved.

let response : Types.HandleIncomingFundsRecord = {
cyclesForProtocol: Nat = cyclesForProtocol;
cyclesForMainer : Nat = cyclesForMainer;
Expand Down
5 changes: 5 additions & 0 deletions src/common/Types.mo
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,11 @@ module Types {
toSubaccount : ?Blob;
};

public type IcpTransferArgsAccount = {
amount : TokenLedger.Tokens;
to : Blob;
};

public type IcpTransferResult = Result<Nat64, ApiError>;

public type MainerCreationInput = PaymentTransactionBlockId and {
Expand Down