Skip to content
Draft
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
13 changes: 13 additions & 0 deletions scripts/console.withdraw.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env node

import { consoleActorLocal } from './actor.mjs';

try {
const { withdraw_payments } = await consoleActorLocal();

await withdraw_payments();

console.log('✅ Payments successfully withdrawn.');
} catch (error) {
console.error('❌ Payments cannot be withdrawn', error);
}
1 change: 1 addition & 0 deletions src/console/console.did
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,5 @@ service : () -> {
update_rate_config : (SegmentType, RateConfig) -> ();
upload_asset_chunk : (UploadChunk) -> (UploadChunkResult);
version : () -> (text) query;
withdraw_payments : () -> (nat64);
}
9 changes: 8 additions & 1 deletion src/console/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod impls;
mod memory;
mod metadata;
mod msg;
mod payments;
mod proposals;
mod storage;
mod store;
Expand All @@ -17,6 +18,7 @@ use crate::factory::orbiter::create_orbiter as create_orbiter_console;
use crate::factory::satellite::create_satellite as create_satellite_console;
use crate::guards::{caller_is_admin_controller, caller_is_observatory};
use crate::memory::{init_storage_heap_state, STATE};
use crate::payments::payments::withdraw_balance;
use crate::proposals::{
commit_proposal as make_commit_proposal,
delete_proposal_assets as delete_proposal_assets_proposal, init_proposal as make_init_proposal,
Expand Down Expand Up @@ -51,7 +53,7 @@ use ic_cdk::api::call::ManualReply;
use ic_cdk::api::caller;
use ic_cdk::{id, trap};
use ic_cdk_macros::{export_candid, init, post_upgrade, pre_upgrade, query, update};
use ic_ledger_types::Tokens;
use ic_ledger_types::{BlockIndex, Tokens};
use junobuild_collections::types::core::CollectionKey;
use junobuild_shared::controllers::init_controllers;
use junobuild_shared::rate::types::RateConfig;
Expand Down Expand Up @@ -172,6 +174,11 @@ fn list_payments() -> Payments {
list_payments_state()
}

#[update(guard = "caller_is_admin_controller")]
async fn withdraw_payments() -> BlockIndex {
withdraw_balance().await.unwrap_or_else(|e| trap(&e))
}

/// Satellites

#[update]
Expand Down
1 change: 1 addition & 0 deletions src/console/src/payments/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod payments;
71 changes: 71 additions & 0 deletions src/console/src/payments/payments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use candid::Principal;
use ic_cdk::id;
use ic_ledger_types::{
account_balance, AccountBalanceArgs, AccountIdentifier, BlockIndex, Memo, Tokens, TransferArgs,
};
use junobuild_shared::constants::IC_TRANSACTION_FEE_ICP;
use junobuild_shared::env::LEDGER;
use junobuild_shared::ledger::icp::{principal_to_account_identifier, transfer_token, SUB_ACCOUNT};

/// Withdraws the entire balance of the Console — i.e., withdraws the payments for the additional
/// Satellites and Orbiters that have been made.
///
/// The destination account for the withdrawal is one of mine (David here).
///
/// # Returns
/// - `Ok(BlockIndex)`: If the transfer was successful, it returns the block index of the transaction.
/// - `Err(String)`: If an error occurs during the process, it returns a descriptive error message.
///
/// # Errors
/// This function can return errors in the following cases:
/// - If the account balance retrieval fails.
/// - If the transfer to the ledger fails due to insufficient balance or other issues.
///
/// # Example
/// ```rust
/// let result = withdraw_balance().await;
/// match result {
/// Ok(block_index) => println!("Withdrawal successful! Block index: {}", block_index),
/// Err(e) => println!("Error during withdrawal: {}", e),
/// }
/// ```
pub async fn withdraw_balance() -> Result<BlockIndex, String> {
let account_identifier: AccountIdentifier = AccountIdentifier::from_hex(
"e4aaed31b1cbf2dfaaca8ef9862a51b04fc4a314e2c054bae8f28d501c57068b",
)?;

let balance = console_balance().await?;

let args = TransferArgs {
memo: Memo(0),
amount: balance - IC_TRANSACTION_FEE_ICP,
fee: IC_TRANSACTION_FEE_ICP,
from_subaccount: Some(SUB_ACCOUNT),
to: account_identifier,
created_at_time: None,
};

let block_index = transfer_token(args)
.await
.map_err(|e| format!("failed to call ledger: {:?}", e))?
.map_err(|e| format!("ledger transfer error {:?}", e))?;

Ok(block_index)
}

async fn console_balance() -> Result<Tokens, String> {
let ledger = Principal::from_text(LEDGER).unwrap();

let console_account_identifier: AccountIdentifier =
principal_to_account_identifier(&id(), &SUB_ACCOUNT);

let args: AccountBalanceArgs = AccountBalanceArgs {
account: console_account_identifier,
};

let tokens = account_balance(ledger, args)
.await
.map_err(|e| format!("failed to call ledger balance: {:?}", e))?;

Ok(tokens)
}
1 change: 1 addition & 0 deletions src/declarations/console/console.did.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ export interface _SERVICE {
update_rate_config: ActorMethod<[SegmentType, RateConfig], undefined>;
upload_asset_chunk: ActorMethod<[UploadChunk], UploadChunkResult>;
version: ActorMethod<[], string>;
withdraw_payments: ActorMethod<[], bigint>;
}
export declare const idlFactory: IDL.InterfaceFactory;
export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];
3 changes: 2 additions & 1 deletion src/declarations/console/console.factory.did.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ export const idlFactory = ({ IDL }) => {
submit_proposal: IDL.Func([IDL.Nat], [IDL.Nat, Proposal], []),
update_rate_config: IDL.Func([SegmentType, RateConfig], [], []),
upload_asset_chunk: IDL.Func([UploadChunk], [UploadChunkResult], []),
version: IDL.Func([], [IDL.Text], ['query'])
version: IDL.Func([], [IDL.Text], ['query']),
withdraw_payments: IDL.Func([], [IDL.Nat64], [])
});
};
// @ts-ignore
Expand Down
3 changes: 2 additions & 1 deletion src/declarations/console/console.factory.did.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ export const idlFactory = ({ IDL }) => {
submit_proposal: IDL.Func([IDL.Nat], [IDL.Nat, Proposal], []),
update_rate_config: IDL.Func([SegmentType, RateConfig], [], []),
upload_asset_chunk: IDL.Func([UploadChunk], [UploadChunkResult], []),
version: IDL.Func([], [IDL.Text], ['query'])
version: IDL.Func([], [IDL.Text], ['query']),
withdraw_payments: IDL.Func([], [IDL.Nat64], [])
});
};
// @ts-ignore
Expand Down
50 changes: 46 additions & 4 deletions src/tests/console.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { _SERVICE as ConsoleActor } from '$declarations/console/console.did';
import { idlFactory as idlFactorConsole } from '$declarations/console/console.factory.did';
import { AnonymousIdentity } from '@dfinity/agent';
import { Ed25519KeyIdentity } from '@dfinity/identity';
import { PocketIc, type Actor } from '@hadronous/pic';
import { afterEach, beforeEach, describe, expect, inject } from 'vitest';
import { CONTROLLER_ERROR_MSG } from './constants/console-tests.constants';
import { deploySegments, initMissionControls } from './utils/console-tests.utils';
import { CONSOLE_WASM_PATH } from './utils/setup-tests.utils';

Expand Down Expand Up @@ -31,9 +33,49 @@ describe('Console', () => {
await pic?.tearDown();
});

it('should throw errors if too many users are created quickly', async () => {
await expect(
async () => await initMissionControls({ actor, pic, length: 2 })
).rejects.toThrowError(new RegExp('Rate limit reached, try again later', 'i'));
describe('owner', () => {
it('should throw errors if too many users are created quickly', async () => {
await expect(
async () => await initMissionControls({ actor, pic, length: 2 })
).rejects.toThrowError(new RegExp('Rate limit reached, try again later', 'i'));
});
});

describe('anonymous', () => {
beforeEach(() => {
actor.setIdentity(new AnonymousIdentity());
});

it('should throw errors on list payments', async () => {
const { list_payments } = actor;

await expect(list_payments()).rejects.toThrow(CONTROLLER_ERROR_MSG);
});

it('should throw errors on withdraw payments', async () => {
const { withdraw_payments } = actor;

await expect(withdraw_payments()).rejects.toThrow(CONTROLLER_ERROR_MSG);
});
});

describe('random', () => {
const randomCaller = Ed25519KeyIdentity.generate();

beforeEach(() => {
actor.setIdentity(randomCaller);
});

it('should throw errors on list payments', async () => {
const { list_payments } = actor;

await expect(list_payments()).rejects.toThrow(CONTROLLER_ERROR_MSG);
});

it('should throw errors on withdraw payments', async () => {
const { withdraw_payments } = actor;

await expect(withdraw_payments()).rejects.toThrow(CONTROLLER_ERROR_MSG);
});
});
});