Skip to content

Commit edd0eec

Browse files
feat: withdraw payments (#725)
* feat: withdraw payments Signed-off-by: David Dal Busco <[email protected]> * feat: generate did Signed-off-by: David Dal Busco <[email protected]> * docs: withdraw balance Signed-off-by: David Dal Busco <[email protected]> * feat: script to widthdraw payments Signed-off-by: David Dal Busco <[email protected]> * test: should throw Signed-off-by: David Dal Busco <[email protected]> --------- Signed-off-by: David Dal Busco <[email protected]>
1 parent 10835e5 commit edd0eec

File tree

10 files changed

+163
-8
lines changed

10 files changed

+163
-8
lines changed

scripts/console.withdraw.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env node
2+
3+
import { consoleActorLocal } from './actor.mjs';
4+
5+
try {
6+
const { withdraw_payments } = await consoleActorLocal();
7+
8+
await withdraw_payments();
9+
10+
console.log('✅ Payments successfully withdrawn.');
11+
} catch (error) {
12+
console.error('❌ Payments cannot be withdrawn', error);
13+
}

src/console/console.did

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,5 @@ service : () -> {
224224
update_rate_config : (SegmentType, RateConfig) -> ();
225225
upload_asset_chunk : (UploadChunk) -> (UploadChunkResult);
226226
version : () -> (text) query;
227+
withdraw_payments : () -> (nat64);
227228
}

src/console/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod impls;
66
mod memory;
77
mod metadata;
88
mod msg;
9+
mod payments;
910
mod proposals;
1011
mod storage;
1112
mod store;
@@ -17,6 +18,7 @@ use crate::factory::orbiter::create_orbiter as create_orbiter_console;
1718
use crate::factory::satellite::create_satellite as create_satellite_console;
1819
use crate::guards::{caller_is_admin_controller, caller_is_observatory};
1920
use crate::memory::{init_storage_heap_state, STATE};
21+
use crate::payments::payments::withdraw_balance;
2022
use crate::proposals::{
2123
commit_proposal as make_commit_proposal,
2224
delete_proposal_assets as delete_proposal_assets_proposal, init_proposal as make_init_proposal,
@@ -51,7 +53,7 @@ use ic_cdk::api::call::ManualReply;
5153
use ic_cdk::api::caller;
5254
use ic_cdk::{id, trap};
5355
use ic_cdk_macros::{export_candid, init, post_upgrade, pre_upgrade, query, update};
54-
use ic_ledger_types::Tokens;
56+
use ic_ledger_types::{BlockIndex, Tokens};
5557
use junobuild_collections::types::core::CollectionKey;
5658
use junobuild_shared::controllers::init_controllers;
5759
use junobuild_shared::types::core::DomainName;
@@ -171,6 +173,11 @@ fn list_payments() -> Payments {
171173
list_payments_state()
172174
}
173175

176+
#[update(guard = "caller_is_admin_controller")]
177+
async fn withdraw_payments() -> BlockIndex {
178+
withdraw_balance().await.unwrap_or_else(|e| trap(&e))
179+
}
180+
174181
/// Satellites
175182
176183
#[update]

src/console/src/payments/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod payments;

src/console/src/payments/payments.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use candid::Principal;
2+
use ic_cdk::id;
3+
use ic_ledger_types::{
4+
account_balance, AccountBalanceArgs, AccountIdentifier, BlockIndex, Memo, Tokens,
5+
};
6+
use junobuild_shared::constants::IC_TRANSACTION_FEE_ICP;
7+
use junobuild_shared::env::LEDGER;
8+
use junobuild_shared::ledger::{principal_to_account_identifier, transfer_token, SUB_ACCOUNT};
9+
10+
/// Withdraws the entire balance of the Console — i.e., withdraws the payments for the additional
11+
/// Satellites and Orbiters that have been made.
12+
///
13+
/// The destination account for the withdrawal is one of mine (David here).
14+
///
15+
/// # Returns
16+
/// - `Ok(BlockIndex)`: If the transfer was successful, it returns the block index of the transaction.
17+
/// - `Err(String)`: If an error occurs during the process, it returns a descriptive error message.
18+
///
19+
/// # Errors
20+
/// This function can return errors in the following cases:
21+
/// - If the account balance retrieval fails.
22+
/// - If the transfer to the ledger fails due to insufficient balance or other issues.
23+
///
24+
/// # Example
25+
/// ```rust
26+
/// let result = withdraw_balance().await;
27+
/// match result {
28+
/// Ok(block_index) => println!("Withdrawal successful! Block index: {}", block_index),
29+
/// Err(e) => println!("Error during withdrawal: {}", e),
30+
/// }
31+
/// ```
32+
pub async fn withdraw_balance() -> Result<BlockIndex, String> {
33+
let account_identifier: AccountIdentifier = AccountIdentifier::from_hex(
34+
"e4aaed31b1cbf2dfaaca8ef9862a51b04fc4a314e2c054bae8f28d501c57068b",
35+
)?;
36+
37+
let balance = console_balance().await?;
38+
39+
let block_index = transfer_token(
40+
account_identifier,
41+
Memo(0),
42+
balance - IC_TRANSACTION_FEE_ICP,
43+
IC_TRANSACTION_FEE_ICP,
44+
)
45+
.await
46+
.map_err(|e| format!("failed to call ledger: {:?}", e))?
47+
.map_err(|e| format!("ledger transfer error {:?}", e))?;
48+
49+
Ok(block_index)
50+
}
51+
52+
async fn console_balance() -> Result<Tokens, String> {
53+
let ledger = Principal::from_text(LEDGER).unwrap();
54+
55+
let console_account_identifier: AccountIdentifier =
56+
principal_to_account_identifier(&id(), &SUB_ACCOUNT);
57+
58+
let args: AccountBalanceArgs = AccountBalanceArgs {
59+
account: console_account_identifier,
60+
};
61+
62+
let tokens = account_balance(ledger, args)
63+
.await
64+
.map_err(|e| format!("failed to call ledger balance: {:?}", e))?;
65+
66+
Ok(tokens)
67+
}

src/declarations/console/console.did.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ export interface _SERVICE {
261261
update_rate_config: ActorMethod<[SegmentType, RateConfig], undefined>;
262262
upload_asset_chunk: ActorMethod<[UploadChunk], UploadChunkResult>;
263263
version: ActorMethod<[], string>;
264+
withdraw_payments: ActorMethod<[], bigint>;
264265
}
265266
export declare const idlFactory: IDL.InterfaceFactory;
266267
export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];

src/declarations/console/console.factory.did.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ export const idlFactory = ({ IDL }) => {
275275
submit_proposal: IDL.Func([IDL.Nat], [IDL.Nat, Proposal], []),
276276
update_rate_config: IDL.Func([SegmentType, RateConfig], [], []),
277277
upload_asset_chunk: IDL.Func([UploadChunk], [UploadChunkResult], []),
278-
version: IDL.Func([], [IDL.Text], ['query'])
278+
version: IDL.Func([], [IDL.Text], ['query']),
279+
withdraw_payments: IDL.Func([], [IDL.Nat64], [])
279280
});
280281
};
281282
// @ts-ignore

src/declarations/console/console.factory.did.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ export const idlFactory = ({ IDL }) => {
275275
submit_proposal: IDL.Func([IDL.Nat], [IDL.Nat, Proposal], []),
276276
update_rate_config: IDL.Func([SegmentType, RateConfig], [], []),
277277
upload_asset_chunk: IDL.Func([UploadChunk], [UploadChunkResult], []),
278-
version: IDL.Func([], [IDL.Text], ['query'])
278+
version: IDL.Func([], [IDL.Text], ['query']),
279+
withdraw_payments: IDL.Func([], [IDL.Nat64], [])
279280
});
280281
};
281282
// @ts-ignore

src/libs/shared/src/ledger.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,34 @@ pub async fn transfer_payment(
4646
memo: Memo,
4747
amount: Tokens,
4848
fee: Tokens,
49+
) -> CallResult<TransferResult> {
50+
let account_identifier: AccountIdentifier = principal_to_account_identifier(to, to_sub_account);
51+
52+
transfer_token(account_identifier, memo, amount, fee).await
53+
}
54+
55+
/// Transfers tokens to a specified account identified.
56+
///
57+
/// # Arguments
58+
/// * `account_identifier` - The account identifier of the destination.
59+
/// * `memo` - A memo for the transaction.
60+
/// * `amount` - The amount of tokens to transfer.
61+
/// * `fee` - The transaction fee.
62+
///
63+
/// # Returns
64+
/// A result containing the transfer result or an error message.
65+
pub async fn transfer_token(
66+
account_identifier: AccountIdentifier,
67+
memo: Memo,
68+
amount: Tokens,
69+
fee: Tokens,
4970
) -> CallResult<TransferResult> {
5071
let args = TransferArgs {
5172
memo,
5273
amount,
5374
fee,
5475
from_subaccount: Some(SUB_ACCOUNT),
55-
to: principal_to_account_identifier(to, to_sub_account),
76+
to: account_identifier,
5677
created_at_time: None,
5778
};
5879

src/tests/console.spec.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { _SERVICE as ConsoleActor } from '$declarations/console/console.did';
22
import { idlFactory as idlFactorConsole } from '$declarations/console/console.factory.did';
3+
import { AnonymousIdentity } from '@dfinity/agent';
34
import { Ed25519KeyIdentity } from '@dfinity/identity';
45
import { PocketIc, type Actor } from '@hadronous/pic';
56
import { afterEach, beforeEach, describe, expect, inject } from 'vitest';
7+
import { CONTROLLER_ERROR_MSG } from './constants/console-tests.constants';
68
import { deploySegments, initMissionControls } from './utils/console-tests.utils';
79
import { CONSOLE_WASM_PATH } from './utils/setup-tests.utils';
810

@@ -31,9 +33,49 @@ describe('Console', () => {
3133
await pic?.tearDown();
3234
});
3335

34-
it('should throw errors if too many users are created quickly', async () => {
35-
await expect(
36-
async () => await initMissionControls({ actor, pic, length: 2 })
37-
).rejects.toThrowError(new RegExp('Rate limit reached, try again later', 'i'));
36+
describe('owner', () => {
37+
it('should throw errors if too many users are created quickly', async () => {
38+
await expect(
39+
async () => await initMissionControls({ actor, pic, length: 2 })
40+
).rejects.toThrowError(new RegExp('Rate limit reached, try again later', 'i'));
41+
});
42+
});
43+
44+
describe('anonymous', () => {
45+
beforeEach(() => {
46+
actor.setIdentity(new AnonymousIdentity());
47+
});
48+
49+
it('should throw errors on list payments', async () => {
50+
const { list_payments } = actor;
51+
52+
await expect(list_payments()).rejects.toThrow(CONTROLLER_ERROR_MSG);
53+
});
54+
55+
it('should throw errors on withdraw payments', async () => {
56+
const { withdraw_payments } = actor;
57+
58+
await expect(withdraw_payments()).rejects.toThrow(CONTROLLER_ERROR_MSG);
59+
});
60+
});
61+
62+
describe('random', () => {
63+
const randomCaller = Ed25519KeyIdentity.generate();
64+
65+
beforeEach(() => {
66+
actor.setIdentity(randomCaller);
67+
});
68+
69+
it('should throw errors on list payments', async () => {
70+
const { list_payments } = actor;
71+
72+
await expect(list_payments()).rejects.toThrow(CONTROLLER_ERROR_MSG);
73+
});
74+
75+
it('should throw errors on withdraw payments', async () => {
76+
const { withdraw_payments } = actor;
77+
78+
await expect(withdraw_payments()).rejects.toThrow(CONTROLLER_ERROR_MSG);
79+
});
3880
});
3981
});

0 commit comments

Comments
 (0)