From 111b62c3e03df3ac73a5f3334ff3904e30fe5af7 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 7 Oct 2024 21:50:34 +0200 Subject: [PATCH 01/31] feat: send icp tokens Signed-off-by: David Dal Busco --- src/frontend/src/app.d.ts | 1 + .../src/lib/components/core/Wallet.svelte | 6 ++ .../MissionControlWallet.svelte | 19 +++++++ .../src/lib/components/modals/Modals.svelte | 5 ++ .../components/modals/SendTokenModal.svelte | 38 +++++++++++++ .../lib/components/wallet/WalletForm.svelte | 57 +++++++++++++++++++ src/frontend/src/lib/i18n/en.json | 6 +- src/frontend/src/lib/types/i18n.d.ts | 4 ++ src/frontend/src/lib/types/modal.ts | 10 +++- 9 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 src/frontend/src/lib/components/modals/SendTokenModal.svelte create mode 100644 src/frontend/src/lib/components/wallet/WalletForm.svelte diff --git a/src/frontend/src/app.d.ts b/src/frontend/src/app.d.ts index 87a8d08f2..b0c1a374b 100644 --- a/src/frontend/src/app.d.ts +++ b/src/frontend/src/app.d.ts @@ -22,6 +22,7 @@ declare namespace svelteHTML { 'on:junoReloadVersions'?: (event: CustomEvent) => void; 'on:junoCloseActions'?: (event: CustomEvent) => void; 'on:junoRegistrationState'?: (event: CustomEvent) => void; + 'on:junoSyncBalance'?: (event: CustomEvent) => void; } } diff --git a/src/frontend/src/lib/components/core/Wallet.svelte b/src/frontend/src/lib/components/core/Wallet.svelte index c42dd1a0a..f3843dbb4 100644 --- a/src/frontend/src/lib/components/core/Wallet.svelte +++ b/src/frontend/src/lib/components/core/Wallet.svelte @@ -6,6 +6,7 @@ import type { WalletWorker } from '$lib/services/worker.wallet.services'; import type { Principal } from '@dfinity/principal'; import { onDestroy, onMount } from 'svelte'; + import {emit} from "$lib/utils/events.utils"; export let missionControlId: Principal; export let balance: bigint | undefined = undefined; @@ -20,6 +21,11 @@ balance = data.wallet.balance; transactions = [...JSON.parse(data.wallet.newTransactions, jsonReviver), ...transactions]; + + emit({ + message: "junoSyncBalance", + detail: balance + }) }; const initWorker = async () => { diff --git a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte index 5f3fbff45..a79192ad5 100644 --- a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte +++ b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte @@ -18,6 +18,7 @@ import { fade } from 'svelte/transition'; import type { TransactionWithId } from '@dfinity/ledger-icp'; import Wallet from '$lib/components/core/Wallet.svelte'; + import {emit} from "$lib/utils/events.utils"; export let missionControlId: Principal; @@ -95,6 +96,22 @@ */ onMount(async () => await loadCredits()); + + /** + * Actions + */ + + const openModal = () => { + emit({ + message: 'junoModal', + detail: { + type: 'send_token', + detail: { + balance + } + } + }); + } {#if $authSignedInStore} @@ -139,6 +156,8 @@ + + {/if} + +{#if modal?.type === 'send_token' && nonNullish(modal.detail)} + +{/if} \ No newline at end of file diff --git a/src/frontend/src/lib/components/modals/SendTokenModal.svelte b/src/frontend/src/lib/components/modals/SendTokenModal.svelte new file mode 100644 index 000000000..b52bde0d2 --- /dev/null +++ b/src/frontend/src/lib/components/modals/SendTokenModal.svelte @@ -0,0 +1,38 @@ + + + balance = syncBalance} /> + +{#if nonNullish($missionControlStore)} + + {#if steps === 'ready'} +
+

Done

+ +
+ {:else if steps === 'in_progress'} + +

{$i18n.canisters.upgrade_in_progress}

+
+ {:else if steps === 'review'}{:else} + + {/if} +
+{/if} diff --git a/src/frontend/src/lib/components/wallet/WalletForm.svelte b/src/frontend/src/lib/components/wallet/WalletForm.svelte new file mode 100644 index 000000000..584201e80 --- /dev/null +++ b/src/frontend/src/lib/components/wallet/WalletForm.svelte @@ -0,0 +1,57 @@ + + +

{$i18n.wallet.send}

+ +

+ {@html i18nFormat($i18n.wallet.send_information, [ + { + placeholder: '{0}', + value: formatE8sICP(balance ?? 0n) + } + ])} +

+ +
+
+ + {$i18n.wallet.destination} + + +
+ +
+ + {$i18n.wallet.tx_amount} + + +
+
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index c848af19f..128e0494f 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -201,7 +201,11 @@ "memo_received": "Received", "memo_sent": "Sent", "export_title": "Export to CSV", - "export_info": "This process exports the displayed transactions to a CSV file. Shall we proceed?" + "export_info": "This process exports the displayed transactions to a CSV file. Shall we proceed?", + "send": "Send", + "send_information": "Send tokens securely to any address. Your wallet currently holds {0} ICP.", + "destination": "Destination", + "destination_placeholder": "Enter destination address" }, "authentication": { "title": "Authentication", diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index 7624f4f5b..6201e6f55 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -210,6 +210,10 @@ interface I18nWallet { memo_sent: string; export_title: string; export_info: string; + send: string; + send_information: string; + destination: string; + destination_placeholder: string; } interface I18nAuthentication { diff --git a/src/frontend/src/lib/types/modal.ts b/src/frontend/src/lib/types/modal.ts index da819fca8..cf95c100f 100644 --- a/src/frontend/src/lib/types/modal.ts +++ b/src/frontend/src/lib/types/modal.ts @@ -70,6 +70,10 @@ export interface JunoModalEditCanisterSettingsDetail { settings: CanisterSettings; } +export interface JunoModalSendTokenDetail { + balance: bigint | undefined; +} + export type JunoModalDetail = | JunoModalTopUpSatelliteDetail | JunoModalTopUpMissionControlDetail @@ -77,7 +81,8 @@ export type JunoModalDetail = | JunoModalCustomDomainDetail | JunoModalCreateControllerDetail | JunoModalCyclesSatelliteDetail - | JunoModalDeleteSatelliteDetail; + | JunoModalDeleteSatelliteDetail + | JunoModalSendTokenDetail; export interface JunoModal { type: @@ -96,6 +101,7 @@ export interface JunoModal { | 'edit_canister_settings' | 'upgrade_satellite' | 'upgrade_mission_control' - | 'upgrade_orbiter'; + | 'upgrade_orbiter' + | 'send_token'; detail?: JunoModalDetail; } From edd0eec011f523ecf9d433d2b7ec8e633facdfa5 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 9 Oct 2024 21:14:42 +0200 Subject: [PATCH 02/31] feat: withdraw payments (#725) * feat: withdraw payments Signed-off-by: David Dal Busco * feat: generate did Signed-off-by: David Dal Busco * docs: withdraw balance Signed-off-by: David Dal Busco * feat: script to widthdraw payments Signed-off-by: David Dal Busco * test: should throw Signed-off-by: David Dal Busco --------- Signed-off-by: David Dal Busco --- scripts/console.withdraw.mjs | 13 ++++ src/console/console.did | 1 + src/console/src/lib.rs | 9 ++- src/console/src/payments/mod.rs | 1 + src/console/src/payments/payments.rs | 67 +++++++++++++++++++ src/declarations/console/console.did.d.ts | 1 + .../console/console.factory.did.js | 3 +- .../console/console.factory.did.mjs | 3 +- src/libs/shared/src/ledger.rs | 23 ++++++- src/tests/console.spec.ts | 50 ++++++++++++-- 10 files changed, 163 insertions(+), 8 deletions(-) create mode 100755 scripts/console.withdraw.mjs create mode 100644 src/console/src/payments/mod.rs create mode 100644 src/console/src/payments/payments.rs diff --git a/scripts/console.withdraw.mjs b/scripts/console.withdraw.mjs new file mode 100755 index 000000000..47515f933 --- /dev/null +++ b/scripts/console.withdraw.mjs @@ -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); +} diff --git a/src/console/console.did b/src/console/console.did index 53d49c3a4..a7a68b638 100644 --- a/src/console/console.did +++ b/src/console/console.did @@ -224,4 +224,5 @@ service : () -> { update_rate_config : (SegmentType, RateConfig) -> (); upload_asset_chunk : (UploadChunk) -> (UploadChunkResult); version : () -> (text) query; + withdraw_payments : () -> (nat64); } diff --git a/src/console/src/lib.rs b/src/console/src/lib.rs index fcfc2e584..f7e92a2bc 100644 --- a/src/console/src/lib.rs +++ b/src/console/src/lib.rs @@ -6,6 +6,7 @@ mod impls; mod memory; mod metadata; mod msg; +mod payments; mod proposals; mod storage; mod store; @@ -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, @@ -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::types::core::DomainName; @@ -171,6 +173,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] diff --git a/src/console/src/payments/mod.rs b/src/console/src/payments/mod.rs new file mode 100644 index 000000000..ca98e4ba2 --- /dev/null +++ b/src/console/src/payments/mod.rs @@ -0,0 +1 @@ +pub mod payments; diff --git a/src/console/src/payments/payments.rs b/src/console/src/payments/payments.rs new file mode 100644 index 000000000..97f7498c6 --- /dev/null +++ b/src/console/src/payments/payments.rs @@ -0,0 +1,67 @@ +use candid::Principal; +use ic_cdk::id; +use ic_ledger_types::{ + account_balance, AccountBalanceArgs, AccountIdentifier, BlockIndex, Memo, Tokens, +}; +use junobuild_shared::constants::IC_TRANSACTION_FEE_ICP; +use junobuild_shared::env::LEDGER; +use junobuild_shared::ledger::{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 { + let account_identifier: AccountIdentifier = AccountIdentifier::from_hex( + "e4aaed31b1cbf2dfaaca8ef9862a51b04fc4a314e2c054bae8f28d501c57068b", + )?; + + let balance = console_balance().await?; + + let block_index = transfer_token( + account_identifier, + Memo(0), + balance - IC_TRANSACTION_FEE_ICP, + IC_TRANSACTION_FEE_ICP, + ) + .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 { + 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) +} diff --git a/src/declarations/console/console.did.d.ts b/src/declarations/console/console.did.d.ts index 47f50838f..ba64aeaa2 100644 --- a/src/declarations/console/console.did.d.ts +++ b/src/declarations/console/console.did.d.ts @@ -261,6 +261,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[]; diff --git a/src/declarations/console/console.factory.did.js b/src/declarations/console/console.factory.did.js index ae0242f13..932f0f47d 100644 --- a/src/declarations/console/console.factory.did.js +++ b/src/declarations/console/console.factory.did.js @@ -275,7 +275,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 diff --git a/src/declarations/console/console.factory.did.mjs b/src/declarations/console/console.factory.did.mjs index ae0242f13..932f0f47d 100644 --- a/src/declarations/console/console.factory.did.mjs +++ b/src/declarations/console/console.factory.did.mjs @@ -275,7 +275,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 diff --git a/src/libs/shared/src/ledger.rs b/src/libs/shared/src/ledger.rs index 4166f63c9..d4b8e883f 100644 --- a/src/libs/shared/src/ledger.rs +++ b/src/libs/shared/src/ledger.rs @@ -46,13 +46,34 @@ pub async fn transfer_payment( memo: Memo, amount: Tokens, fee: Tokens, +) -> CallResult { + let account_identifier: AccountIdentifier = principal_to_account_identifier(to, to_sub_account); + + transfer_token(account_identifier, memo, amount, fee).await +} + +/// Transfers tokens to a specified account identified. +/// +/// # Arguments +/// * `account_identifier` - The account identifier of the destination. +/// * `memo` - A memo for the transaction. +/// * `amount` - The amount of tokens to transfer. +/// * `fee` - The transaction fee. +/// +/// # Returns +/// A result containing the transfer result or an error message. +pub async fn transfer_token( + account_identifier: AccountIdentifier, + memo: Memo, + amount: Tokens, + fee: Tokens, ) -> CallResult { let args = TransferArgs { memo, amount, fee, from_subaccount: Some(SUB_ACCOUNT), - to: principal_to_account_identifier(to, to_sub_account), + to: account_identifier, created_at_time: None, }; diff --git a/src/tests/console.spec.ts b/src/tests/console.spec.ts index dc35df4e7..1642e28c0 100644 --- a/src/tests/console.spec.ts +++ b/src/tests/console.spec.ts @@ -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'; @@ -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); + }); }); }); From 0dfcce434b21b6f42fdfbd625056eeb2cd5469a1 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 10 Oct 2024 16:28:19 +0200 Subject: [PATCH 03/31] feat: form Signed-off-by: David Dal Busco --- .../src/lib/components/core/Wallet.svelte | 10 +- .../MissionControlWallet.svelte | 8 +- .../src/lib/components/modals/Modals.svelte | 4 +- .../components/modals/SendTokenModal.svelte | 9 +- .../src/lib/components/ui/Input.svelte | 121 +++++++++++------ .../lib/components/wallet/WalletForm.svelte | 127 +++++++++++++----- src/frontend/src/lib/i18n/en.json | 13 +- src/frontend/src/lib/types/i18n.d.ts | 7 + .../src/lib/utils/icp-account.utils.ts | 20 +++ .../src/lib/utils/icrc-account.utils.ts | 22 +++ 10 files changed, 241 insertions(+), 100 deletions(-) create mode 100644 src/frontend/src/lib/utils/icp-account.utils.ts create mode 100644 src/frontend/src/lib/utils/icrc-account.utils.ts diff --git a/src/frontend/src/lib/components/core/Wallet.svelte b/src/frontend/src/lib/components/core/Wallet.svelte index f3843dbb4..0fe765509 100644 --- a/src/frontend/src/lib/components/core/Wallet.svelte +++ b/src/frontend/src/lib/components/core/Wallet.svelte @@ -6,7 +6,7 @@ import type { WalletWorker } from '$lib/services/worker.wallet.services'; import type { Principal } from '@dfinity/principal'; import { onDestroy, onMount } from 'svelte'; - import {emit} from "$lib/utils/events.utils"; + import { emit } from '$lib/utils/events.utils'; export let missionControlId: Principal; export let balance: bigint | undefined = undefined; @@ -22,10 +22,10 @@ balance = data.wallet.balance; transactions = [...JSON.parse(data.wallet.newTransactions, jsonReviver), ...transactions]; - emit({ - message: "junoSyncBalance", - detail: balance - }) + emit({ + message: 'junoSyncBalance', + detail: balance + }); }; const initWorker = async () => { diff --git a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte index a79192ad5..18f9add4f 100644 --- a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte +++ b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte @@ -18,7 +18,7 @@ import { fade } from 'svelte/transition'; import type { TransactionWithId } from '@dfinity/ledger-icp'; import Wallet from '$lib/components/core/Wallet.svelte'; - import {emit} from "$lib/utils/events.utils"; + import { emit } from '$lib/utils/events.utils'; export let missionControlId: Principal; @@ -111,7 +111,7 @@ } } }); - } + }; {#if $authSignedInStore} @@ -156,7 +156,9 @@ - + {#if balance > 0n} + + {/if} -{/if} \ No newline at end of file +{/if} diff --git a/src/frontend/src/lib/components/modals/SendTokenModal.svelte b/src/frontend/src/lib/components/modals/SendTokenModal.svelte index b52bde0d2..c570a2f9a 100644 --- a/src/frontend/src/lib/components/modals/SendTokenModal.svelte +++ b/src/frontend/src/lib/components/modals/SendTokenModal.svelte @@ -4,11 +4,8 @@ import Modal from '$lib/components/ui/Modal.svelte'; import SpinnerModal from '$lib/components/ui/SpinnerModal.svelte'; import { i18n } from '$lib/stores/i18n.store'; - import type { - JunoModalDetail, - JunoModalSendTokenDetail - } from '$lib/types/modal'; - import WalletForm from "$lib/components/wallet/WalletForm.svelte"; + import type { JunoModalDetail, JunoModalSendTokenDetail } from '$lib/types/modal'; + import WalletForm from '$lib/components/wallet/WalletForm.svelte'; export let detail: JunoModalDetail; @@ -18,7 +15,7 @@ let steps: 'form' | 'review' | 'in_progress' | 'ready' | 'error'; - balance = syncBalance} /> + (balance = syncBalance)} /> {#if nonNullish($missionControlStore)} diff --git a/src/frontend/src/lib/components/ui/Input.svelte b/src/frontend/src/lib/components/ui/Input.svelte index d5a07e958..2f64a3ecf 100644 --- a/src/frontend/src/lib/components/ui/Input.svelte +++ b/src/frontend/src/lib/components/ui/Input.svelte @@ -1,15 +1,22 @@ diff --git a/src/frontend/src/lib/components/wallet/WalletForm.svelte b/src/frontend/src/lib/components/wallet/WalletForm.svelte index 584201e80..fbb60f7fe 100644 --- a/src/frontend/src/lib/components/wallet/WalletForm.svelte +++ b/src/frontend/src/lib/components/wallet/WalletForm.svelte @@ -2,56 +2,109 @@ import { i18n } from '$lib/stores/i18n.store'; import Value from '$lib/components/ui/Value.svelte'; import Input from '$lib/components/ui/Input.svelte'; - import {i18nFormat} from "$lib/utils/i18n.utils"; - import {formatE8sICP} from "$lib/utils/icp.utils"; + import { i18nFormat } from '$lib/utils/i18n.utils'; + import { formatE8sICP } from '$lib/utils/icp.utils'; + import { invalidIcrcAddress } from '$lib/utils/icrc-account.utils'; + import { invalidIcpAddress } from '$lib/utils/icp-account.utils'; + import { toasts } from '$lib/stores/toasts.store'; + import { ICPToken, isNullish, TokenAmountV2 } from '@dfinity/utils'; + import { FromStringToTokenError } from '@dfinity/utils'; + import { IC_TRANSACTION_FEE_ICP } from '$lib/constants/constants'; export let balance: bigint | undefined; let destination = ''; - let amount: number | undefined; + let amount: string | undefined; - const onSubmit = async () => {}; + const onSubmit = async () => { + if (isNullish(balance) || balance === 0n) { + toasts.error({ + text: $i18n.errors.empty_balance + }); + return; + } + + if (invalidIcrcAddress(destination) && invalidIcpAddress(destination)) { + toasts.error({ + text: $i18n.errors.invalid_destination + }); + return; + } + + if (isNullish(amount)) { + toasts.error({ + text: $i18n.errors.empty_amount + }); + return; + } + + const tokenAmount = TokenAmountV2.fromString({ token: ICPToken, amount }); + + if (Object.values(FromStringToTokenError).includes(tokenAmount)) { + toasts.error({ + text: $i18n.errors.invalid_amount + }); + return; + } + + if (balance + IC_TRANSACTION_FEE_ICP < (tokenAmount as TokenAmountV2).toE8s()) { + toasts.error({ + text: $i18n.errors.invalid_amount + }); + return; + } + };

{$i18n.wallet.send}

- {@html i18nFormat($i18n.wallet.send_information, [ - { - placeholder: '{0}', - value: formatE8sICP(balance ?? 0n) - } - ])} + {@html i18nFormat($i18n.wallet.send_information, [ + { + placeholder: '{0}', + value: formatE8sICP(balance ?? 0n) + } + ])}

-
- - {$i18n.wallet.destination} - - -
- -
- - {$i18n.wallet.tx_amount} - - -
+
+ + {$i18n.wallet.destination} + + +
+ +
+ + {$i18n.wallet.icp_amount} + + +
+ +
\ No newline at end of file + form { + margin: var(--padding-4x) 0; + } + + div { + margin-bottom: var(--padding); + } + diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index 128e0494f..573bd23c4 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -43,7 +43,8 @@ "refresh": "Refresh", "reload": "Reload", "open_website": "Open Juno website and documentation", - "file": "File" + "file": "File", + "review": "Review" }, "canisters": { "insight": "Insight", @@ -205,7 +206,9 @@ "send": "Send", "send_information": "Send tokens securely to any address. Your wallet currently holds {0} ICP.", "destination": "Destination", - "destination_placeholder": "Enter destination address" + "destination_placeholder": "Enter destination address", + "icp_amount": "Amount (ICP)", + "amount_placeholder": "Enter an amount" }, "authentication": { "title": "Authentication", @@ -415,7 +418,11 @@ "no_file_selected_for_upload": "No file was selected for the upload.", "upload_error": "Unexpected error(s) while uploading the file.", "no_collection_for_upload": "Unexpected error. No collection is selected for the upload.", - "invalid_email": "Please enter a valid email address." + "invalid_email": "Please enter a valid email address.", + "invalid_destination": "Please enter a valid ICP or ICRC destination address.", + "empty_amount": "Please enter an amount for the transfer.", + "invalid_amount": "The amount should be greater than zero and less than your balance minus the fee.", + "empty_balance": "Your wallet does not have any available funds." }, "document": { "owner": "Owner", diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index 6201e6f55..33bdea470 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -47,6 +47,7 @@ interface I18nCore { reload: string; open_website: string; file: string; + review: string; } interface I18nCanisters { @@ -214,6 +215,8 @@ interface I18nWallet { send_information: string; destination: string; destination_placeholder: string; + icp_amount: string; + amount_placeholder: string; } interface I18nAuthentication { @@ -432,6 +435,10 @@ interface I18nErrors { upload_error: string; no_collection_for_upload: string; invalid_email: string; + invalid_destination: string; + empty_amount: string; + invalid_amount: string; + empty_balance: string; } interface I18nDocument { diff --git a/src/frontend/src/lib/utils/icp-account.utils.ts b/src/frontend/src/lib/utils/icp-account.utils.ts new file mode 100644 index 000000000..30fc8d1bc --- /dev/null +++ b/src/frontend/src/lib/utils/icp-account.utils.ts @@ -0,0 +1,20 @@ +import { checkAccountId } from '@dfinity/ledger-icp'; +import { isNullish } from '@dfinity/utils'; + +export const isIcpAccountIdentifier = (address: string | undefined): boolean => { + if (isNullish(address)) { + return false; + } + + try { + checkAccountId(address); + return true; + } catch (_: unknown) { + // We do not parse the error + } + + return false; +}; + +export const invalidIcpAddress = (address: string | undefined): boolean => + !isIcpAccountIdentifier(address); diff --git a/src/frontend/src/lib/utils/icrc-account.utils.ts b/src/frontend/src/lib/utils/icrc-account.utils.ts new file mode 100644 index 000000000..f9392c8d5 --- /dev/null +++ b/src/frontend/src/lib/utils/icrc-account.utils.ts @@ -0,0 +1,22 @@ +import { decodeIcrcAccount, type IcrcAccount } from '@dfinity/ledger-icrc'; +import type { Principal } from '@dfinity/principal'; +import { isNullish } from '@dfinity/utils'; + +export const getIcrcAccount = (principal: Principal): IcrcAccount => ({ owner: principal }); + +export const isIcrcAddress = (address: string | undefined): boolean => { + if (isNullish(address)) { + return false; + } + + try { + decodeIcrcAccount(address); + return true; + } catch (_: unknown) { + // We do not parse the error + } + + return false; +}; + +export const invalidIcrcAddress = (address: string | undefined): boolean => !isIcrcAddress(address); From 3155af955a36a8b3457290d2cfa39b39c64f0c28 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 10 Oct 2024 17:28:59 +0200 Subject: [PATCH 04/31] feat: review Signed-off-by: David Dal Busco --- .../MissionControlWallet.svelte | 7 + .../components/modals/SendTokenModal.svelte | 16 ++- .../lib/components/wallet/WalletForm.svelte | 16 ++- .../lib/components/wallet/WalletReview.svelte | 128 ++++++++++++++++++ src/frontend/src/lib/i18n/en.json | 9 +- src/frontend/src/lib/types/i18n.d.ts | 5 + src/frontend/src/lib/utils/token.utils.ts | 16 +++ src/frontend/src/routes/+layout.ts | 8 ++ 8 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 src/frontend/src/lib/components/wallet/WalletReview.svelte create mode 100644 src/frontend/src/lib/utils/token.utils.ts diff --git a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte index 18f9add4f..3f621877f 100644 --- a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte +++ b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte @@ -121,6 +121,13 @@
+ + {$i18n.wallet.wallet_id} +

+ +

+
+ {$i18n.wallet.account_identifier}

diff --git a/src/frontend/src/lib/components/modals/SendTokenModal.svelte b/src/frontend/src/lib/components/modals/SendTokenModal.svelte index c570a2f9a..b35d7d954 100644 --- a/src/frontend/src/lib/components/modals/SendTokenModal.svelte +++ b/src/frontend/src/lib/components/modals/SendTokenModal.svelte @@ -6,6 +6,7 @@ import { i18n } from '$lib/stores/i18n.store'; import type { JunoModalDetail, JunoModalSendTokenDetail } from '$lib/types/modal'; import WalletForm from '$lib/components/wallet/WalletForm.svelte'; + import WalletReview from '$lib/components/wallet/WalletReview.svelte'; export let detail: JunoModalDetail; @@ -13,6 +14,9 @@ $: ({ balance } = detail as JunoModalSendTokenDetail); let steps: 'form' | 'review' | 'in_progress' | 'ready' | 'error'; + + let destination = ''; + let amount: string | undefined; (balance = syncBalance)} /> @@ -28,8 +32,16 @@

{$i18n.canisters.upgrade_in_progress}

- {:else if steps === 'review'}{:else} - + {:else if steps === 'review'} + (steps = 'form')} + /> + {:else} + (steps = 'review')} /> {/if} {/if} diff --git a/src/frontend/src/lib/components/wallet/WalletForm.svelte b/src/frontend/src/lib/components/wallet/WalletForm.svelte index fbb60f7fe..305ad9aaa 100644 --- a/src/frontend/src/lib/components/wallet/WalletForm.svelte +++ b/src/frontend/src/lib/components/wallet/WalletForm.svelte @@ -7,14 +7,16 @@ import { invalidIcrcAddress } from '$lib/utils/icrc-account.utils'; import { invalidIcpAddress } from '$lib/utils/icp-account.utils'; import { toasts } from '$lib/stores/toasts.store'; - import { ICPToken, isNullish, TokenAmountV2 } from '@dfinity/utils'; - import { FromStringToTokenError } from '@dfinity/utils'; + import { isNullish, TokenAmountV2 } from '@dfinity/utils'; import { IC_TRANSACTION_FEE_ICP } from '$lib/constants/constants'; + import { createEventDispatcher } from 'svelte'; + import { amountToICPToken } from '$lib/utils/token.utils'; export let balance: bigint | undefined; + export let destination = ''; + export let amount: string | undefined; - let destination = ''; - let amount: string | undefined; + const dispatch = createEventDispatcher(); const onSubmit = async () => { if (isNullish(balance) || balance === 0n) { @@ -38,9 +40,9 @@ return; } - const tokenAmount = TokenAmountV2.fromString({ token: ICPToken, amount }); + const tokenAmount = amountToICPToken(amount); - if (Object.values(FromStringToTokenError).includes(tokenAmount)) { + if (isNullish(tokenAmount)) { toasts.error({ text: $i18n.errors.invalid_amount }); @@ -53,6 +55,8 @@ }); return; } + + dispatch('junoReview'); }; diff --git a/src/frontend/src/lib/components/wallet/WalletReview.svelte b/src/frontend/src/lib/components/wallet/WalletReview.svelte new file mode 100644 index 000000000..81c6286eb --- /dev/null +++ b/src/frontend/src/lib/components/wallet/WalletReview.svelte @@ -0,0 +1,128 @@ + + +

{$i18n.wallet.send}

+ +

{$i18n.wallet.review_and_confirm}

+ +
+
+
+ {$i18n.wallet.tx_from} + +
+ + {$i18n.wallet.wallet_id} +

+ +

+
+ + + {$i18n.wallet.account_identifier} +

+ +

+
+ + + {$i18n.wallet.balance} +

+ {#if nonNullish(balance)}{formatE8sICP(balance)} ICP{/if} +

+
+
+
+ +
+ {$i18n.wallet.tx_to} + +
+ + {$i18n.wallet.destination} +

+ +

+
+
+
+ +
+ {$i18n.wallet.sending} + +
+ + {$i18n.wallet.tx_amount} +

+ {#if nonNullish(token)}{formatE8sICP(token.toE8s())} ICP{/if} +

+
+ + + {$i18n.wallet.fee} +

+ {formatE8sICP(IC_TRANSACTION_FEE_ICP)} ICP +

+
+
+
+
+ +
+ + +
+
+ + diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index 573bd23c4..c76013e3a 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -44,7 +44,8 @@ "reload": "Reload", "open_website": "Open Juno website and documentation", "file": "File", - "review": "Review" + "review": "Review", + "confirm": "Confirm" }, "canisters": { "insight": "Insight", @@ -208,7 +209,11 @@ "destination": "Destination", "destination_placeholder": "Enter destination address", "icp_amount": "Amount (ICP)", - "amount_placeholder": "Enter an amount" + "amount_placeholder": "Enter an amount", + "wallet_id": "Wallet ID", + "sending": "Sending", + "fee": "Fee", + "review_and_confirm": "Review the details below and confirm to proceed with your transaction." }, "authentication": { "title": "Authentication", diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index 33bdea470..abf1c7557 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -48,6 +48,7 @@ interface I18nCore { open_website: string; file: string; review: string; + confirm: string; } interface I18nCanisters { @@ -217,6 +218,10 @@ interface I18nWallet { destination_placeholder: string; icp_amount: string; amount_placeholder: string; + wallet_id: string; + sending: string; + fee: string; + review_and_confirm: string; } interface I18nAuthentication { diff --git a/src/frontend/src/lib/utils/token.utils.ts b/src/frontend/src/lib/utils/token.utils.ts new file mode 100644 index 000000000..9136454e5 --- /dev/null +++ b/src/frontend/src/lib/utils/token.utils.ts @@ -0,0 +1,16 @@ +import { FromStringToTokenError, ICPToken, isNullish, TokenAmountV2 } from '@dfinity/utils'; + +export const amountToICPToken = (amount: string | undefined): TokenAmountV2 | undefined => { + // For convenience reason the function accept undefined + if (isNullish(amount)) { + return undefined; + } + + const token = TokenAmountV2.fromString({ token: ICPToken, amount }); + + if (Object.values(FromStringToTokenError).includes(token as string | FromStringToTokenError)) { + return undefined; + } + + return token; +}; diff --git a/src/frontend/src/routes/+layout.ts b/src/frontend/src/routes/+layout.ts index 1e62f0b8a..8c718d6e4 100644 --- a/src/frontend/src/routes/+layout.ts +++ b/src/frontend/src/routes/+layout.ts @@ -1,3 +1,11 @@ export const prerender = true; export const ssr = false; export const trailingSlash = 'always'; + +// ⚠️ For production build the polyfill needs to be injected with Rollup (see vite.config.ts) because the page might be loaded before the _layout.js which will contains this polyfill. +// The / in buffer/ is mandatory here. +// More workaround: https://github.com/vitejs/vite/discussions/2785 +import { Buffer } from 'buffer/'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore Polyfill Buffer for development purpose +globalThis.Buffer = Buffer; From 2d3e13d98382324deff1268644a42ef8ec88de43 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 10 Oct 2024 19:06:29 +0200 Subject: [PATCH 05/31] Revert "feat: withdraw payments (#725)" This reverts commit edd0eec011f523ecf9d433d2b7ec8e633facdfa5. --- scripts/console.withdraw.mjs | 13 ---- src/console/console.did | 1 - src/console/src/lib.rs | 9 +-- src/console/src/payments/mod.rs | 1 - src/console/src/payments/payments.rs | 67 ------------------- src/declarations/console/console.did.d.ts | 1 - .../console/console.factory.did.js | 3 +- .../console/console.factory.did.mjs | 3 +- src/libs/shared/src/ledger.rs | 23 +------ src/tests/console.spec.ts | 50 ++------------ 10 files changed, 8 insertions(+), 163 deletions(-) delete mode 100755 scripts/console.withdraw.mjs delete mode 100644 src/console/src/payments/mod.rs delete mode 100644 src/console/src/payments/payments.rs diff --git a/scripts/console.withdraw.mjs b/scripts/console.withdraw.mjs deleted file mode 100755 index 47515f933..000000000 --- a/scripts/console.withdraw.mjs +++ /dev/null @@ -1,13 +0,0 @@ -#!/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); -} diff --git a/src/console/console.did b/src/console/console.did index a7a68b638..53d49c3a4 100644 --- a/src/console/console.did +++ b/src/console/console.did @@ -224,5 +224,4 @@ service : () -> { update_rate_config : (SegmentType, RateConfig) -> (); upload_asset_chunk : (UploadChunk) -> (UploadChunkResult); version : () -> (text) query; - withdraw_payments : () -> (nat64); } diff --git a/src/console/src/lib.rs b/src/console/src/lib.rs index f7e92a2bc..fcfc2e584 100644 --- a/src/console/src/lib.rs +++ b/src/console/src/lib.rs @@ -6,7 +6,6 @@ mod impls; mod memory; mod metadata; mod msg; -mod payments; mod proposals; mod storage; mod store; @@ -18,7 +17,6 @@ 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, @@ -53,7 +51,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::{BlockIndex, Tokens}; +use ic_ledger_types::Tokens; use junobuild_collections::types::core::CollectionKey; use junobuild_shared::controllers::init_controllers; use junobuild_shared::types::core::DomainName; @@ -173,11 +171,6 @@ 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] diff --git a/src/console/src/payments/mod.rs b/src/console/src/payments/mod.rs deleted file mode 100644 index ca98e4ba2..000000000 --- a/src/console/src/payments/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod payments; diff --git a/src/console/src/payments/payments.rs b/src/console/src/payments/payments.rs deleted file mode 100644 index 97f7498c6..000000000 --- a/src/console/src/payments/payments.rs +++ /dev/null @@ -1,67 +0,0 @@ -use candid::Principal; -use ic_cdk::id; -use ic_ledger_types::{ - account_balance, AccountBalanceArgs, AccountIdentifier, BlockIndex, Memo, Tokens, -}; -use junobuild_shared::constants::IC_TRANSACTION_FEE_ICP; -use junobuild_shared::env::LEDGER; -use junobuild_shared::ledger::{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 { - let account_identifier: AccountIdentifier = AccountIdentifier::from_hex( - "e4aaed31b1cbf2dfaaca8ef9862a51b04fc4a314e2c054bae8f28d501c57068b", - )?; - - let balance = console_balance().await?; - - let block_index = transfer_token( - account_identifier, - Memo(0), - balance - IC_TRANSACTION_FEE_ICP, - IC_TRANSACTION_FEE_ICP, - ) - .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 { - 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) -} diff --git a/src/declarations/console/console.did.d.ts b/src/declarations/console/console.did.d.ts index ba64aeaa2..47f50838f 100644 --- a/src/declarations/console/console.did.d.ts +++ b/src/declarations/console/console.did.d.ts @@ -261,7 +261,6 @@ 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[]; diff --git a/src/declarations/console/console.factory.did.js b/src/declarations/console/console.factory.did.js index 932f0f47d..ae0242f13 100644 --- a/src/declarations/console/console.factory.did.js +++ b/src/declarations/console/console.factory.did.js @@ -275,8 +275,7 @@ 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']), - withdraw_payments: IDL.Func([], [IDL.Nat64], []) + version: IDL.Func([], [IDL.Text], ['query']) }); }; // @ts-ignore diff --git a/src/declarations/console/console.factory.did.mjs b/src/declarations/console/console.factory.did.mjs index 932f0f47d..ae0242f13 100644 --- a/src/declarations/console/console.factory.did.mjs +++ b/src/declarations/console/console.factory.did.mjs @@ -275,8 +275,7 @@ 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']), - withdraw_payments: IDL.Func([], [IDL.Nat64], []) + version: IDL.Func([], [IDL.Text], ['query']) }); }; // @ts-ignore diff --git a/src/libs/shared/src/ledger.rs b/src/libs/shared/src/ledger.rs index d4b8e883f..4166f63c9 100644 --- a/src/libs/shared/src/ledger.rs +++ b/src/libs/shared/src/ledger.rs @@ -46,34 +46,13 @@ pub async fn transfer_payment( memo: Memo, amount: Tokens, fee: Tokens, -) -> CallResult { - let account_identifier: AccountIdentifier = principal_to_account_identifier(to, to_sub_account); - - transfer_token(account_identifier, memo, amount, fee).await -} - -/// Transfers tokens to a specified account identified. -/// -/// # Arguments -/// * `account_identifier` - The account identifier of the destination. -/// * `memo` - A memo for the transaction. -/// * `amount` - The amount of tokens to transfer. -/// * `fee` - The transaction fee. -/// -/// # Returns -/// A result containing the transfer result or an error message. -pub async fn transfer_token( - account_identifier: AccountIdentifier, - memo: Memo, - amount: Tokens, - fee: Tokens, ) -> CallResult { let args = TransferArgs { memo, amount, fee, from_subaccount: Some(SUB_ACCOUNT), - to: account_identifier, + to: principal_to_account_identifier(to, to_sub_account), created_at_time: None, }; diff --git a/src/tests/console.spec.ts b/src/tests/console.spec.ts index 1642e28c0..dc35df4e7 100644 --- a/src/tests/console.spec.ts +++ b/src/tests/console.spec.ts @@ -1,10 +1,8 @@ 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'; @@ -33,49 +31,9 @@ describe('Console', () => { await pic?.tearDown(); }); - 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); - }); + 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')); }); }); From 70db118fd6b1136d89c8a30ce20f24ddd59b441b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 10 Oct 2024 19:15:05 +0200 Subject: [PATCH 06/31] refactor: plural Signed-off-by: David Dal Busco --- .../mission-control/MissionControlWallet.svelte | 2 +- src/frontend/src/lib/components/modals/Modals.svelte | 6 +++--- ...{SendTokenModal.svelte => SendTokensModal.svelte} | 12 ++++++------ .../SendTokensForm.svelte} | 0 .../SendTokensReview.svelte} | 0 src/frontend/src/lib/types/modal.ts | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) rename src/frontend/src/lib/components/modals/{SendTokenModal.svelte => SendTokensModal.svelte} (72%) rename src/frontend/src/lib/components/{wallet/WalletForm.svelte => tokens/SendTokensForm.svelte} (100%) rename src/frontend/src/lib/components/{wallet/WalletReview.svelte => tokens/SendTokensReview.svelte} (100%) diff --git a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte index 3f621877f..18a1d1ecb 100644 --- a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte +++ b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte @@ -105,7 +105,7 @@ emit({ message: 'junoModal', detail: { - type: 'send_token', + type: 'send_tokens', detail: { balance } diff --git a/src/frontend/src/lib/components/modals/Modals.svelte b/src/frontend/src/lib/components/modals/Modals.svelte index 04fdd0ef4..c20df0996 100644 --- a/src/frontend/src/lib/components/modals/Modals.svelte +++ b/src/frontend/src/lib/components/modals/Modals.svelte @@ -17,7 +17,7 @@ import OrbiterTransferCyclesModal from '$lib/components/modals/OrbiterTransferCyclesModal.svelte'; import MissionControlTransferCyclesModal from '$lib/components/modals/MissionControlTransferCyclesModal.svelte'; import CanisterEditSettingsModal from '$lib/components/modals/CanisterEditSettingsModal.svelte'; - import SendTokenModal from '$lib/components/modals/SendTokenModal.svelte'; + import SendTokensModal from '$lib/components/modals/SendTokensModal.svelte'; let modal: JunoModal | undefined = undefined; @@ -90,6 +90,6 @@ {/if} -{#if modal?.type === 'send_token' && nonNullish(modal.detail)} - +{#if modal?.type === 'send_tokens' && nonNullish(modal.detail)} + {/if} diff --git a/src/frontend/src/lib/components/modals/SendTokenModal.svelte b/src/frontend/src/lib/components/modals/SendTokensModal.svelte similarity index 72% rename from src/frontend/src/lib/components/modals/SendTokenModal.svelte rename to src/frontend/src/lib/components/modals/SendTokensModal.svelte index b35d7d954..7891385f3 100644 --- a/src/frontend/src/lib/components/modals/SendTokenModal.svelte +++ b/src/frontend/src/lib/components/modals/SendTokensModal.svelte @@ -4,14 +4,14 @@ import Modal from '$lib/components/ui/Modal.svelte'; import SpinnerModal from '$lib/components/ui/SpinnerModal.svelte'; import { i18n } from '$lib/stores/i18n.store'; - import type { JunoModalDetail, JunoModalSendTokenDetail } from '$lib/types/modal'; - import WalletForm from '$lib/components/wallet/WalletForm.svelte'; - import WalletReview from '$lib/components/wallet/WalletReview.svelte'; + import type { JunoModalDetail, JunoModalSendTokensDetail } from '$lib/types/modal'; + import SendTokensForm from '$lib/components/tokens/SendTokensForm.svelte'; + import SendTokensReview from "$lib/components/tokens/SendTokensReview.svelte"; export let detail: JunoModalDetail; let balance: bigint | undefined; - $: ({ balance } = detail as JunoModalSendTokenDetail); + $: ({ balance } = detail as JunoModalSendTokensDetail); let steps: 'form' | 'review' | 'in_progress' | 'ready' | 'error'; @@ -33,7 +33,7 @@

{$i18n.canisters.upgrade_in_progress}

{:else if steps === 'review'} - (steps = 'form')} /> {:else} - (steps = 'review')} /> + (steps = 'review')} /> {/if} {/if} diff --git a/src/frontend/src/lib/components/wallet/WalletForm.svelte b/src/frontend/src/lib/components/tokens/SendTokensForm.svelte similarity index 100% rename from src/frontend/src/lib/components/wallet/WalletForm.svelte rename to src/frontend/src/lib/components/tokens/SendTokensForm.svelte diff --git a/src/frontend/src/lib/components/wallet/WalletReview.svelte b/src/frontend/src/lib/components/tokens/SendTokensReview.svelte similarity index 100% rename from src/frontend/src/lib/components/wallet/WalletReview.svelte rename to src/frontend/src/lib/components/tokens/SendTokensReview.svelte diff --git a/src/frontend/src/lib/types/modal.ts b/src/frontend/src/lib/types/modal.ts index cf95c100f..dcdbea21e 100644 --- a/src/frontend/src/lib/types/modal.ts +++ b/src/frontend/src/lib/types/modal.ts @@ -70,7 +70,7 @@ export interface JunoModalEditCanisterSettingsDetail { settings: CanisterSettings; } -export interface JunoModalSendTokenDetail { +export interface JunoModalSendTokensDetail { balance: bigint | undefined; } @@ -82,7 +82,7 @@ export type JunoModalDetail = | JunoModalCreateControllerDetail | JunoModalCyclesSatelliteDetail | JunoModalDeleteSatelliteDetail - | JunoModalSendTokenDetail; + | JunoModalSendTokensDetail; export interface JunoModal { type: @@ -102,6 +102,6 @@ export interface JunoModal { | 'upgrade_satellite' | 'upgrade_mission_control' | 'upgrade_orbiter' - | 'send_token'; + | 'send_tokens'; detail?: JunoModalDetail; } From 16564f1273f3bca38eaac10bafce44b78a6762fc Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 10 Oct 2024 19:22:24 +0200 Subject: [PATCH 07/31] feat: sending Signed-off-by: David Dal Busco --- .../components/modals/SendTokensModal.svelte | 13 +++++++---- .../components/tokens/SendTokensReview.svelte | 23 +++++++++++++++++-- src/frontend/src/lib/i18n/en.json | 1 + src/frontend/src/lib/types/i18n.d.ts | 1 + 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/lib/components/modals/SendTokensModal.svelte b/src/frontend/src/lib/components/modals/SendTokensModal.svelte index 7891385f3..4d8b3003a 100644 --- a/src/frontend/src/lib/components/modals/SendTokensModal.svelte +++ b/src/frontend/src/lib/components/modals/SendTokensModal.svelte @@ -6,7 +6,7 @@ import { i18n } from '$lib/stores/i18n.store'; import type { JunoModalDetail, JunoModalSendTokensDetail } from '$lib/types/modal'; import SendTokensForm from '$lib/components/tokens/SendTokensForm.svelte'; - import SendTokensReview from "$lib/components/tokens/SendTokensReview.svelte"; + import SendTokensReview from '$lib/components/tokens/SendTokensReview.svelte'; export let detail: JunoModalDetail; @@ -30,7 +30,7 @@
{:else if steps === 'in_progress'} -

{$i18n.canisters.upgrade_in_progress}

+

{$i18n.wallet.sending_in_progress}

{:else if steps === 'review'} (steps = 'form')} + on:junoNext={({ detail }) => (steps = detail)} /> {:else} - (steps = 'review')} /> + (steps = 'review')} + /> {/if} {/if} diff --git a/src/frontend/src/lib/components/tokens/SendTokensReview.svelte b/src/frontend/src/lib/components/tokens/SendTokensReview.svelte index 81c6286eb..2be19671d 100644 --- a/src/frontend/src/lib/components/tokens/SendTokensReview.svelte +++ b/src/frontend/src/lib/components/tokens/SendTokensReview.svelte @@ -10,6 +10,8 @@ import { amountToICPToken } from '$lib/utils/token.utils'; import { IC_TRANSACTION_FEE_ICP } from '$lib/constants/constants'; import { createEventDispatcher } from 'svelte'; + import { wizardBusy } from '$lib/stores/busy.store'; + import { toasts } from '$lib/stores/toasts.store'; export let missionControlId: Principal; export let balance: bigint | undefined; @@ -24,7 +26,24 @@ const dispatch = createEventDispatcher(); - const onSubmit = async () => {}; + const onSubmit = async () => { + wizardBusy.start(); + + dispatch('junoNext', 'in_progress'); + + try { + // dispatch('junoNext', 'ready'); + } catch (err: unknown) { + toasts.error({ + text: $i18n.errors.upgrade_error, + detail: err + }); + + dispatch('junoNext', 'error'); + } + + wizardBusy.stop(); + };

{$i18n.wallet.send}

@@ -96,7 +115,7 @@
- +
diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index c76013e3a..2320a0337 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -212,6 +212,7 @@ "amount_placeholder": "Enter an amount", "wallet_id": "Wallet ID", "sending": "Sending", + "sending_in_progress": "Sending...", "fee": "Fee", "review_and_confirm": "Review the details below and confirm to proceed with your transaction." }, diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index abf1c7557..b6d663b5b 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -220,6 +220,7 @@ interface I18nWallet { amount_placeholder: string; wallet_id: string; sending: string; + sending_in_progress: string; fee: string; review_and_confirm: string; } From e950ebc5a947cbf58d041606f6bcea3e346c5067 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 10 Oct 2024 20:32:49 +0200 Subject: [PATCH 08/31] feat: send mockup Signed-off-by: David Dal Busco --- .../api/{ledger.api.ts => icp-index.api.ts} | 0 src/frontend/src/lib/api/icp-ledger.api.ts | 60 +++++++++++++++ .../MissionControlWallet.svelte | 2 +- .../components/tokens/SendTokensReview.svelte | 16 ++-- src/frontend/src/lib/i18n/en.json | 3 +- .../src/lib/services/balance.services.ts | 2 +- .../src/lib/services/tokens.services.ts | 74 +++++++++++++++++++ src/frontend/src/lib/types/i18n.d.ts | 1 + src/frontend/src/lib/utils/date.utils.ts | 2 + src/frontend/src/lib/workers/wallet.worker.ts | 2 +- 10 files changed, 151 insertions(+), 11 deletions(-) rename src/frontend/src/lib/api/{ledger.api.ts => icp-index.api.ts} (100%) create mode 100644 src/frontend/src/lib/api/icp-ledger.api.ts create mode 100644 src/frontend/src/lib/services/tokens.services.ts diff --git a/src/frontend/src/lib/api/ledger.api.ts b/src/frontend/src/lib/api/icp-index.api.ts similarity index 100% rename from src/frontend/src/lib/api/ledger.api.ts rename to src/frontend/src/lib/api/icp-index.api.ts diff --git a/src/frontend/src/lib/api/icp-ledger.api.ts b/src/frontend/src/lib/api/icp-ledger.api.ts new file mode 100644 index 000000000..a283619c6 --- /dev/null +++ b/src/frontend/src/lib/api/icp-ledger.api.ts @@ -0,0 +1,60 @@ +import { getAgent } from '$lib/utils/agent.utils'; +import { nowInBigIntNanoSeconds } from '$lib/utils/date.utils'; +import type { Identity } from '@dfinity/agent'; +import { AccountIdentifier, LedgerCanister, type BlockHeight } from '@dfinity/ledger-icp'; +import type { IcrcAccount } from '@dfinity/ledger-icrc'; +import { assertNonNullish, toNullable } from '@dfinity/utils'; + +export const transfer = async ({ + identity, + to, + amount +}: { + identity: Identity | undefined | null; + to: string; + amount: bigint; +}): Promise => { + assertNonNullish(identity); + + const { transfer } = await ledgerCanister(identity); + + console.log(to, amount, identity.getPrincipal().toText()) + + return transfer({ + to: AccountIdentifier.fromHex(to), + amount + }); +}; + +export const icrc1Transfer = async ({ + identity, + to, + amount, + createdAt +}: { + identity: Identity | undefined | null; + to: IcrcAccount; + amount: bigint; + createdAt?: bigint; +}): Promise => { + assertNonNullish(identity); + + const { icrc1Transfer } = await ledgerCanister(identity); + + return icrc1Transfer({ + to: { + owner: to.owner, + subaccount: toNullable(to.subaccount) + }, + amount, + createdAt: createdAt ?? nowInBigIntNanoSeconds() + }); +}; + +const ledgerCanister = async (identity: Identity): Promise => { + const agent = await getAgent({ identity }); + + return LedgerCanister.create({ + agent + }); +}; diff --git a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte index 18a1d1ecb..3646b528b 100644 --- a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte +++ b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte @@ -9,7 +9,7 @@ import Identifier from '$lib/components/ui/Identifier.svelte'; import QRCodeContainer from '$lib/components/ui/QRCodeContainer.svelte'; import { onMount } from 'svelte'; - import { getAccountIdentifier, getTransactions } from '$lib/api/ledger.api'; + import { getAccountIdentifier, getTransactions } from '$lib/api/icp-index.api'; import { getCredits } from '$lib/api/console.api'; import { toasts } from '$lib/stores/toasts.store'; import Transactions from '$lib/components/transactions/Transactions.svelte'; diff --git a/src/frontend/src/lib/components/tokens/SendTokensReview.svelte b/src/frontend/src/lib/components/tokens/SendTokensReview.svelte index 2be19671d..e180302ed 100644 --- a/src/frontend/src/lib/components/tokens/SendTokensReview.svelte +++ b/src/frontend/src/lib/components/tokens/SendTokensReview.svelte @@ -5,13 +5,14 @@ import Value from '$lib/components/ui/Value.svelte'; import Identifier from '$lib/components/ui/Identifier.svelte'; import { i18n } from '$lib/stores/i18n.store'; - import { getAccountIdentifier } from '$lib/api/ledger.api'; + import { getAccountIdentifier } from '$lib/api/icp-index.api'; import { AccountIdentifier } from '@dfinity/ledger-icp'; import { amountToICPToken } from '$lib/utils/token.utils'; import { IC_TRANSACTION_FEE_ICP } from '$lib/constants/constants'; import { createEventDispatcher } from 'svelte'; import { wizardBusy } from '$lib/stores/busy.store'; - import { toasts } from '$lib/stores/toasts.store'; + import { authStore } from '$lib/stores/auth.store'; + import { sendTokens } from '$lib/services/tokens.services'; export let missionControlId: Principal; export let balance: bigint | undefined; @@ -32,13 +33,14 @@ dispatch('junoNext', 'in_progress'); try { - // dispatch('junoNext', 'ready'); - } catch (err: unknown) { - toasts.error({ - text: $i18n.errors.upgrade_error, - detail: err + await sendTokens({ + identity: $authStore.identity, + destination, + token }); + dispatch('junoNext', 'ready'); + } catch (err: unknown) { dispatch('junoNext', 'error'); } diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index 2320a0337..290ab6f56 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -428,7 +428,8 @@ "invalid_destination": "Please enter a valid ICP or ICRC destination address.", "empty_amount": "Please enter an amount for the transfer.", "invalid_amount": "The amount should be greater than zero and less than your balance minus the fee.", - "empty_balance": "Your wallet does not have any available funds." + "empty_balance": "Your wallet does not have any available funds.", + "sending_error": "Unexpected error(s) while sending the tokens." }, "document": { "owner": "Owner", diff --git a/src/frontend/src/lib/services/balance.services.ts b/src/frontend/src/lib/services/balance.services.ts index a9a0e6ad7..b167e991e 100644 --- a/src/frontend/src/lib/services/balance.services.ts +++ b/src/frontend/src/lib/services/balance.services.ts @@ -1,5 +1,5 @@ import { getCredits } from '$lib/api/console.api'; -import { getAccountIdentifier, getBalance } from '$lib/api/ledger.api'; +import { getAccountIdentifier, getBalance } from '$lib/api/icp-index.api'; import { authStore } from '$lib/stores/auth.store'; import { i18n } from '$lib/stores/i18n.store'; import { toasts } from '$lib/stores/toasts.store'; diff --git a/src/frontend/src/lib/services/tokens.services.ts b/src/frontend/src/lib/services/tokens.services.ts new file mode 100644 index 000000000..59b57c809 --- /dev/null +++ b/src/frontend/src/lib/services/tokens.services.ts @@ -0,0 +1,74 @@ +import { icrc1Transfer, transfer } from '$lib/api/icp-ledger.api'; +import { i18n } from '$lib/stores/i18n.store'; +import { toasts } from '$lib/stores/toasts.store'; +import { invalidIcpAddress } from '$lib/utils/icp-account.utils'; +import { invalidIcrcAddress } from '$lib/utils/icrc-account.utils'; +import type { Identity } from '@dfinity/agent'; +import { decodeIcrcAccount } from '@dfinity/ledger-icrc'; +import { isNullish, type TokenAmountV2 } from '@dfinity/utils'; +import { get } from 'svelte/store'; + +export const sendTokens = async ({ + destination, + token, + identity +}: { + destination: string; + token: TokenAmountV2 | undefined; + identity: Identity | undefined | null; +}): Promise<{ success: boolean }> => { + const notIcp = invalidIcpAddress(destination); + const notIcrc = invalidIcrcAddress(destination); + + console.log(token?.toE8s(), destination) + + if (notIcp && notIcrc) { + const labels = get(i18n); + + toasts.error({ + text: labels.errors.invalid_destination + }); + return { success: false }; + } + + if (isNullish(token)) { + const labels = get(i18n); + + toasts.error({ + text: labels.errors.empty_amount + }); + return { success: false }; + } + + const amount = token.toE8s(); + + try { + // TODO: the send should happens within the mission control + + if (!invalidIcrcAddress) { + await icrc1Transfer({ + identity, + to: decodeIcrcAccount(destination), + amount + }); + return { success: true }; + } + + await transfer({ + identity, + to: destination, + amount + }); + + return { success: true }; + } catch (err: unknown) { + const labels = get(i18n); + + toasts.error({ + text: labels.errors.sending_error, + detail: err + }); + + return { success: false }; + } +}; diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index b6d663b5b..1c4b35090 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -445,6 +445,7 @@ interface I18nErrors { empty_amount: string; invalid_amount: string; empty_balance: string; + sending_error: string; } interface I18nDocument { diff --git a/src/frontend/src/lib/utils/date.utils.ts b/src/frontend/src/lib/utils/date.utils.ts index e790259ac..f8a1b08de 100644 --- a/src/frontend/src/lib/utils/date.utils.ts +++ b/src/frontend/src/lib/utils/date.utils.ts @@ -1,3 +1,5 @@ +export const nowInBigIntNanoSeconds = (): bigint => BigInt(Date.now()) * BigInt(1e6); + export const formatToDate = (nanoseconds: bigint): string => { const options: Intl.DateTimeFormatOptions = { month: 'short', diff --git a/src/frontend/src/lib/workers/wallet.worker.ts b/src/frontend/src/lib/workers/wallet.worker.ts index 54776c623..1aaa06c0f 100644 --- a/src/frontend/src/lib/workers/wallet.worker.ts +++ b/src/frontend/src/lib/workers/wallet.worker.ts @@ -1,4 +1,4 @@ -import { getTransactions } from '$lib/api/ledger.api'; +import { getTransactions } from '$lib/api/icp-index.api'; import { PAGINATION, SYNC_WALLET_TIMER_INTERVAL } from '$lib/constants/constants'; import type { PostMessage, PostMessageDataRequest } from '$lib/types/post-message'; import { loadIdentity } from '$lib/utils/worker.utils'; From cf83cb4f1c10ab01801e41cde864b5913dda5b33 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 08:38:10 +0200 Subject: [PATCH 09/31] feat: icrc1 transfer Signed-off-by: David Dal Busco --- Cargo.lock | 104 ++++++++++++++++++++++++++--- Cargo.toml | 1 + src/libs/shared/Cargo.toml | 1 + src/libs/shared/src/icrc_ledger.rs | 23 +++++++ src/libs/shared/src/ledger.rs | 17 ++++- src/libs/shared/src/lib.rs | 1 + src/libs/shared/src/types.rs | 3 + src/mission_control/Cargo.toml | 1 + 8 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 src/libs/shared/src/icrc_ledger.rs diff --git a/Cargo.lock b/Cargo.lock index 93f120902..d858c058d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.13.1" @@ -546,11 +552,20 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "http" @@ -740,6 +755,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "icrc-ledger-types" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f7f6b54df25295dd0ce2722d583c15e2ee7eec9cef58c10b424feb54561b2" +dependencies = [ + "base32", + "candid", + "crc32fast", + "hex", + "itertools", + "num-bigint", + "num-traits", + "serde", + "serde_bytes", + "sha2", + "strum", + "time", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -774,6 +809,15 @@ dependencies = [ "regex", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -840,6 +884,7 @@ dependencies = [ "ic-cdk-macros", "ic-ledger-types", "ic-stable-structures", + "icrc-ledger-types", "regex", "serde", "time", @@ -909,6 +954,12 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "lock_api" version = "0.4.11" @@ -961,6 +1012,7 @@ dependencies = [ "ic-cdk", "ic-cdk-macros", "ic-ledger-types", + "icrc-ledger-types", "junobuild-shared", "serde", ] @@ -977,11 +1029,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", "serde", @@ -1009,6 +1060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1243,9 +1295,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -1271,9 +1323,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -1357,6 +1409,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + [[package]] name = "syn" version = "1.0.109" @@ -1410,15 +1484,17 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] @@ -1427,6 +1503,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 5a9bed9f3..66210441f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ candid = "0.10.2" ic-cdk = "0.15.1" ic-cdk-macros = "0.15.0" ic-ledger-types = "0.12.0" +icrc-ledger-types = "0.1.6" ic-cdk-timers = "0.9.0" ic-stable-structures = "0.6.4" serde = "1.0.190" diff --git a/src/libs/shared/Cargo.toml b/src/libs/shared/Cargo.toml index b58f23036..921c101bd 100644 --- a/src/libs/shared/Cargo.toml +++ b/src/libs/shared/Cargo.toml @@ -18,6 +18,7 @@ candid.workspace = true ic-cdk.workspace = true ic-cdk-macros.workspace = true ic-ledger-types.workspace = true +icrc-ledger-types.workspace = true ic-stable-structures.workspace = true serde.workspace = true futures = "0.3.28" diff --git a/src/libs/shared/src/icrc_ledger.rs b/src/libs/shared/src/icrc_ledger.rs new file mode 100644 index 000000000..29f3de219 --- /dev/null +++ b/src/libs/shared/src/icrc_ledger.rs @@ -0,0 +1,23 @@ +use candid::Principal; +use ic_cdk::api::call::CallResult; +use ic_cdk::call; +use icrc_ledger_types::icrc1::transfer::{TransferArg}; +use crate::env::LEDGER; +use crate::types::ledger::IcrcTransferResult; + +/// Initiates a transfer of tokens on the ICP ledger using the provided ICRC-1 arguments. +/// +/// This function performs a transfer using the `icrc1_transfer` method on the ICP ledger +/// and returns the result of the transfer operation. +/// +/// # Arguments +/// * `args` - A `TransferArg` struct containing the details of the ICRC-1 transfer. +/// +/// # Returns +/// A `CallResult` indicating either the success or failure of the ICRC-1 token transfer. +pub async fn icrc_transfer(args: TransferArg) -> CallResult { + let ledger = Principal::from_text(LEDGER).unwrap(); + + let (result,) = call(ledger, "icrc1_transfer", (args,)).await?; + Ok(result) +} \ No newline at end of file diff --git a/src/libs/shared/src/ledger.rs b/src/libs/shared/src/ledger.rs index 4166f63c9..25b8d90f6 100644 --- a/src/libs/shared/src/ledger.rs +++ b/src/libs/shared/src/ledger.rs @@ -47,15 +47,30 @@ pub async fn transfer_payment( amount: Tokens, fee: Tokens, ) -> CallResult { + let account_identifier: AccountIdentifier = principal_to_account_identifier(to, to_sub_account); + let args = TransferArgs { memo, amount, fee, from_subaccount: Some(SUB_ACCOUNT), - to: principal_to_account_identifier(to, to_sub_account), + to: account_identifier, created_at_time: None, }; + transfer_token(args).await +} + +/// Initiates a transfer of ICP tokens using the provided arguments and "old" ICP account identifier. +/// +/// # Arguments +/// * `args` - A `TransferArgs` struct containing the details of the ICP transfer. +/// +/// # Returns +/// A `CallResult` indicating either the success or failure of the ICP token transfer. +pub async fn transfer_token( + args: TransferArgs +) -> CallResult { let ledger = Principal::from_text(LEDGER).unwrap(); transfer(ledger, args).await diff --git a/src/libs/shared/src/lib.rs b/src/libs/shared/src/lib.rs index 282a7dc1e..7ff2b3c80 100644 --- a/src/libs/shared/src/lib.rs +++ b/src/libs/shared/src/lib.rs @@ -21,3 +21,4 @@ pub mod types; #[doc(hidden)] pub mod upgrade; pub mod utils; +mod icrc_ledger; diff --git a/src/libs/shared/src/types.rs b/src/libs/shared/src/types.rs index e825ab02f..db4f47be5 100644 --- a/src/libs/shared/src/types.rs +++ b/src/libs/shared/src/types.rs @@ -176,6 +176,7 @@ pub mod ledger { use serde::Deserialize; use ic_ledger_types::{Block, BlockIndex, Memo, Operation, Timestamp}; + use icrc_ledger_types::icrc1::transfer::TransferError; pub type BlockIndexed = (BlockIndex, Block); pub type Blocks = Vec; @@ -188,6 +189,8 @@ pub mod ledger { pub operation: Option, pub timestamp: Timestamp, } + + pub type IcrcTransferResult = Result; } pub mod ic { diff --git a/src/mission_control/Cargo.toml b/src/mission_control/Cargo.toml index 3d84fa5b8..b2030ec76 100644 --- a/src/mission_control/Cargo.toml +++ b/src/mission_control/Cargo.toml @@ -12,6 +12,7 @@ candid.workspace = true ic-cdk.workspace = true ic-cdk-macros.workspace = true ic-ledger-types.workspace = true +icrc-ledger-types.workspace = true serde.workspace = true futures = "0.3.28" junobuild-shared = { path = "../libs/shared" } From 8e1ec685d9503cbd8afe8e672f0b8a3316a3a05d Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 09:20:08 +0200 Subject: [PATCH 10/31] chore: merge main Signed-off-by: David Dal Busco --- src/libs/shared/src/{icrc_ledger.rs => ledger/icrc.rs} | 2 +- src/libs/shared/src/ledger/mod.rs | 1 + src/libs/shared/src/ledger/types.rs | 8 +++++++- src/libs/shared/src/lib.rs | 1 - 4 files changed, 9 insertions(+), 3 deletions(-) rename src/libs/shared/src/{icrc_ledger.rs => ledger/icrc.rs} (94%) diff --git a/src/libs/shared/src/icrc_ledger.rs b/src/libs/shared/src/ledger/icrc.rs similarity index 94% rename from src/libs/shared/src/icrc_ledger.rs rename to src/libs/shared/src/ledger/icrc.rs index 29f3de219..d0a038cc9 100644 --- a/src/libs/shared/src/icrc_ledger.rs +++ b/src/libs/shared/src/ledger/icrc.rs @@ -3,7 +3,7 @@ use ic_cdk::api::call::CallResult; use ic_cdk::call; use icrc_ledger_types::icrc1::transfer::{TransferArg}; use crate::env::LEDGER; -use crate::types::ledger::IcrcTransferResult; +use crate::ledger::types::icrc::IcrcTransferResult; /// Initiates a transfer of tokens on the ICP ledger using the provided ICRC-1 arguments. /// diff --git a/src/libs/shared/src/ledger/mod.rs b/src/libs/shared/src/ledger/mod.rs index a11918d17..621ff8541 100644 --- a/src/libs/shared/src/ledger/mod.rs +++ b/src/libs/shared/src/ledger/mod.rs @@ -1,3 +1,4 @@ pub mod icp; mod types; mod utils; +pub mod icrc; diff --git a/src/libs/shared/src/ledger/types.rs b/src/libs/shared/src/ledger/types.rs index e8b6fe020..ef52840e0 100644 --- a/src/libs/shared/src/ledger/types.rs +++ b/src/libs/shared/src/ledger/types.rs @@ -1,4 +1,10 @@ use ic_ledger_types::{Block, BlockIndex}; pub type BlockIndexed = (BlockIndex, Block); -pub type Blocks = Vec; \ No newline at end of file +pub type Blocks = Vec; + +pub mod icrc { + use icrc_ledger_types::icrc1::transfer::TransferError; + + pub type IcrcTransferResult = Result; +} \ No newline at end of file diff --git a/src/libs/shared/src/lib.rs b/src/libs/shared/src/lib.rs index 7ff2b3c80..282a7dc1e 100644 --- a/src/libs/shared/src/lib.rs +++ b/src/libs/shared/src/lib.rs @@ -21,4 +21,3 @@ pub mod types; #[doc(hidden)] pub mod upgrade; pub mod utils; -mod icrc_ledger; From 5701af50ffd0bea0564c74e690d5770f867dc736 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 09:55:07 +0200 Subject: [PATCH 11/31] feat: transfer icp and icrc Signed-off-by: David Dal Busco --- src/libs/shared/src/ledger/icp.rs | 4 +--- src/libs/shared/src/ledger/icrc.rs | 21 +++++++++++---------- src/libs/shared/src/ledger/mod.rs | 4 ++-- src/libs/shared/src/ledger/types.rs | 5 +++-- src/mission_control/src/lib.rs | 24 +++++++++++++++++++++++- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/libs/shared/src/ledger/icp.rs b/src/libs/shared/src/ledger/icp.rs index b638b1c40..da7e53edf 100644 --- a/src/libs/shared/src/ledger/icp.rs +++ b/src/libs/shared/src/ledger/icp.rs @@ -68,9 +68,7 @@ pub async fn transfer_payment( /// /// # Returns /// A `CallResult` indicating either the success or failure of the ICP token transfer. -pub async fn transfer_token( - args: TransferArgs -) -> CallResult { +pub async fn transfer_token(args: TransferArgs) -> CallResult { let ledger = Principal::from_text(LEDGER).unwrap(); transfer(ledger, args).await diff --git a/src/libs/shared/src/ledger/icrc.rs b/src/libs/shared/src/ledger/icrc.rs index d0a038cc9..5557ac841 100644 --- a/src/libs/shared/src/ledger/icrc.rs +++ b/src/libs/shared/src/ledger/icrc.rs @@ -1,23 +1,24 @@ +use crate::ledger::types::icrc::IcrcTransferResult; use candid::Principal; use ic_cdk::api::call::CallResult; use ic_cdk::call; -use icrc_ledger_types::icrc1::transfer::{TransferArg}; -use crate::env::LEDGER; -use crate::ledger::types::icrc::IcrcTransferResult; +use icrc_ledger_types::icrc1::transfer::TransferArg; -/// Initiates a transfer of tokens on the ICP ledger using the provided ICRC-1 arguments. +/// Initiates a transfer of tokens on a specified ledger using the provided ICRC-1 arguments. /// -/// This function performs a transfer using the `icrc1_transfer` method on the ICP ledger +/// This function performs a transfer using the `icrc1_transfer` method on the specified ledger /// and returns the result of the transfer operation. /// /// # Arguments +/// * `ledger_id` - A `Principal` representing the ID of the ledger where the transfer will be executed. /// * `args` - A `TransferArg` struct containing the details of the ICRC-1 transfer. /// /// # Returns /// A `CallResult` indicating either the success or failure of the ICRC-1 token transfer. -pub async fn icrc_transfer(args: TransferArg) -> CallResult { - let ledger = Principal::from_text(LEDGER).unwrap(); - - let (result,) = call(ledger, "icrc1_transfer", (args,)).await?; +pub async fn icrc_transfer_token( + ledger_id: Principal, + args: TransferArg, +) -> CallResult { + let (result,) = call(ledger_id, "icrc1_transfer", (args,)).await?; Ok(result) -} \ No newline at end of file +} diff --git a/src/libs/shared/src/ledger/mod.rs b/src/libs/shared/src/ledger/mod.rs index 621ff8541..e3100fc68 100644 --- a/src/libs/shared/src/ledger/mod.rs +++ b/src/libs/shared/src/ledger/mod.rs @@ -1,4 +1,4 @@ pub mod icp; -mod types; -mod utils; pub mod icrc; +pub mod types; +mod utils; diff --git a/src/libs/shared/src/ledger/types.rs b/src/libs/shared/src/ledger/types.rs index 19d55b4c0..2e80275a1 100644 --- a/src/libs/shared/src/ledger/types.rs +++ b/src/libs/shared/src/ledger/types.rs @@ -8,5 +8,6 @@ pub mod icp { pub mod icrc { use icrc_ledger_types::icrc1::transfer::TransferError; - pub type IcrcTransferResult = Result; -} \ No newline at end of file + pub type IcrcTransferResult = + Result; +} diff --git a/src/mission_control/src/lib.rs b/src/mission_control/src/lib.rs index f04c15b9a..e42b4c0b1 100644 --- a/src/mission_control/src/lib.rs +++ b/src/mission_control/src/lib.rs @@ -43,8 +43,12 @@ use candid::Principal; use ic_cdk::api::call::{arg_data, ArgDecoderConfig}; use ic_cdk::{id, storage, trap}; use ic_cdk_macros::{export_candid, init, post_upgrade, pre_upgrade, query, update}; -use ic_ledger_types::Tokens; +use ic_ledger_types::{Tokens, TransferArgs, TransferResult}; +use icrc_ledger_types::icrc1::transfer::TransferArg; use junobuild_shared::ic::deposit_cycles as deposit_cycles_shared; +use junobuild_shared::ledger::icp::transfer_token; +use junobuild_shared::ledger::icrc::icrc_transfer_token; +use junobuild_shared::ledger::types::icrc::IcrcTransferResult; use junobuild_shared::types::interface::{ DepositCyclesArgs, MissionControlArgs, SetController, StatusesArgs, }; @@ -362,6 +366,24 @@ fn list_orbiter_statuses(orbiter_id: OrbiterId) -> Option { list_orbiter_statuses_store(&orbiter_id) } +/// +/// Wallet +/// + +#[query(guard = "caller_is_user_or_admin_controller")] +async fn icp_transfer(args: TransferArgs) -> TransferResult { + transfer_token(args) + .await + .map_err(|e| format!("Failed to call ledger: {:?}", e))? +} + +#[query(guard = "caller_is_user_or_admin_controller")] +async fn icrc_transfer(ledger_id: Principal, args: TransferArg) -> IcrcTransferResult { + icrc_transfer_token(ledger_id, args) + .await + .map_err(|e| format!("Failed to call ICRC ledger: {:?}", e))? +} + // Generate did files export_candid!(); From e723e2a7ff84efa85eea87f9a5a2d7b4d0c90ca4 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 15:01:35 +0200 Subject: [PATCH 12/31] chore: fmt Signed-off-by: David Dal Busco --- src/frontend/src/lib/api/icp-ledger.api.ts | 2 +- src/frontend/src/lib/services/tokens.services.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/lib/api/icp-ledger.api.ts b/src/frontend/src/lib/api/icp-ledger.api.ts index a283619c6..b769d96f4 100644 --- a/src/frontend/src/lib/api/icp-ledger.api.ts +++ b/src/frontend/src/lib/api/icp-ledger.api.ts @@ -18,7 +18,7 @@ export const transfer = async ({ const { transfer } = await ledgerCanister(identity); - console.log(to, amount, identity.getPrincipal().toText()) + console.log(to, amount, identity.getPrincipal().toText()); return transfer({ to: AccountIdentifier.fromHex(to), diff --git a/src/frontend/src/lib/services/tokens.services.ts b/src/frontend/src/lib/services/tokens.services.ts index 59b57c809..3557e9de7 100644 --- a/src/frontend/src/lib/services/tokens.services.ts +++ b/src/frontend/src/lib/services/tokens.services.ts @@ -20,7 +20,7 @@ export const sendTokens = async ({ const notIcp = invalidIcpAddress(destination); const notIcrc = invalidIcrcAddress(destination); - console.log(token?.toE8s(), destination) + console.log(token?.toE8s(), destination); if (notIcp && notIcrc) { const labels = get(i18n); From e477dfdd1d0a2242bcd0c93aee0c0856b29f375e Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 15:02:15 +0200 Subject: [PATCH 13/31] fix: update Signed-off-by: David Dal Busco --- src/mission_control/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mission_control/src/lib.rs b/src/mission_control/src/lib.rs index e42b4c0b1..a6a65d144 100644 --- a/src/mission_control/src/lib.rs +++ b/src/mission_control/src/lib.rs @@ -370,14 +370,14 @@ fn list_orbiter_statuses(orbiter_id: OrbiterId) -> Option { /// Wallet /// -#[query(guard = "caller_is_user_or_admin_controller")] +#[update(guard = "caller_is_user_or_admin_controller")] async fn icp_transfer(args: TransferArgs) -> TransferResult { transfer_token(args) .await .map_err(|e| format!("Failed to call ledger: {:?}", e))? } -#[query(guard = "caller_is_user_or_admin_controller")] +#[update(guard = "caller_is_user_or_admin_controller")] async fn icrc_transfer(ledger_id: Principal, args: TransferArg) -> IcrcTransferResult { icrc_transfer_token(ledger_id, args) .await From c1e20cd5615bc59792be8975b94dc096c0507e68 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 15:08:06 +0200 Subject: [PATCH 14/31] chore: no dev Signed-off-by: David Dal Busco --- src/libs/shared/src/ledger/types.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/shared/src/ledger/types.rs b/src/libs/shared/src/ledger/types.rs index 2e80275a1..e52417ea0 100644 --- a/src/libs/shared/src/ledger/types.rs +++ b/src/libs/shared/src/ledger/types.rs @@ -6,8 +6,7 @@ pub mod icp { } pub mod icrc { - use icrc_ledger_types::icrc1::transfer::TransferError; + use icrc_ledger_types::icrc1::transfer::{BlockIndex, TransferError}; - pub type IcrcTransferResult = - Result; + pub type IcrcTransferResult = Result; } From 7fc69645d8dd15493a198220332fff43ff7cd6e8 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 15:22:06 +0200 Subject: [PATCH 15/31] fix: trap on CallResult error Signed-off-by: David Dal Busco --- src/mission_control/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mission_control/src/lib.rs b/src/mission_control/src/lib.rs index a6a65d144..3cd20619f 100644 --- a/src/mission_control/src/lib.rs +++ b/src/mission_control/src/lib.rs @@ -374,14 +374,14 @@ fn list_orbiter_statuses(orbiter_id: OrbiterId) -> Option { async fn icp_transfer(args: TransferArgs) -> TransferResult { transfer_token(args) .await - .map_err(|e| format!("Failed to call ledger: {:?}", e))? + .map_err(|e| trap(&format!("Failed to call ledger: {:?}", e)))? } #[update(guard = "caller_is_user_or_admin_controller")] async fn icrc_transfer(ledger_id: Principal, args: TransferArg) -> IcrcTransferResult { icrc_transfer_token(ledger_id, args) .await - .map_err(|e| format!("Failed to call ICRC ledger: {:?}", e))? + .map_err(|e| trap(&format!("Failed to call ICRC ledger: {:?}", e)))? } // Generate did files From fd74932a7124ce01722092750b00ae69e88e5f47 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 15:23:48 +0200 Subject: [PATCH 16/31] feat: generate did Signed-off-by: David Dal Busco --- .../mission_control/mission_control.did.d.ts | 60 +++++++++++++++-- .../mission_control.factory.did.js | 65 ++++++++++++++++--- src/mission_control/mission_control.did | 53 +++++++++++++-- 3 files changed, 156 insertions(+), 22 deletions(-) diff --git a/src/declarations/mission_control/mission_control.did.d.ts b/src/declarations/mission_control/mission_control.did.d.ts index c2df47a9b..8b0e34827 100644 --- a/src/declarations/mission_control/mission_control.did.d.ts +++ b/src/declarations/mission_control/mission_control.did.d.ts @@ -2,6 +2,10 @@ import type { ActorMethod } from '@dfinity/agent'; import type { IDL } from '@dfinity/candid'; import type { Principal } from '@dfinity/principal'; +export interface Account { + owner: Principal; + subaccount: [] | [Uint8Array | number[]]; +} export type CanisterStatusType = { stopped: null } | { stopping: null } | { running: null }; export interface Controller { updated_at: bigint; @@ -25,7 +29,9 @@ export interface Orbiter { metadata: Array<[string, string]>; created_at: bigint; } -export type Result = { Ok: SegmentStatus } | { Err: string }; +export type Result = { Ok: bigint } | { Err: TransferError }; +export type Result_1 = { Ok: bigint } | { Err: TransferError_1 }; +export type Result_2 = { Ok: SegmentStatus } | { Err: string }; export interface Satellite { updated_at: bigint; metadata: Array<[string, string]>; @@ -53,9 +59,9 @@ export interface SegmentStatus { status_at: bigint; } export interface SegmentsStatuses { - orbiters: [] | [Array]; - satellites: [] | [Array]; - mission_control: Result; + orbiters: [] | [Array]; + satellites: [] | [Array]; + mission_control: Result_2; } export interface SetController { metadata: Array<[string, string]>; @@ -68,9 +74,47 @@ export interface StatusesArgs { satellites: Array<[Principal, CronJobStatusesConfig]>; cycles_threshold: [] | [bigint]; } +export interface Timestamp { + timestamp_nanos: bigint; +} export interface Tokens { e8s: bigint; } +export interface TransferArg { + to: Account; + fee: [] | [bigint]; + memo: [] | [Uint8Array | number[]]; + from_subaccount: [] | [Uint8Array | number[]]; + created_at_time: [] | [bigint]; + amount: bigint; +} +export interface TransferArgs { + to: Uint8Array | number[]; + fee: Tokens; + memo: bigint; + from_subaccount: [] | [Uint8Array | number[]]; + created_at_time: [] | [Timestamp]; + amount: Tokens; +} +export type TransferError = + | { + TxTooOld: { allowed_window_nanos: bigint }; + } + | { BadFee: { expected_fee: Tokens } } + | { TxDuplicate: { duplicate_of: bigint } } + | { TxCreatedInFuture: null } + | { InsufficientFunds: { balance: Tokens } }; +export type TransferError_1 = + | { + GenericError: { message: string; error_code: bigint }; + } + | { TemporarilyUnavailable: null } + | { BadBurn: { min_burn_amount: bigint } } + | { Duplicate: { duplicate_of: bigint } } + | { BadFee: { expected_fee: bigint } } + | { CreatedInFuture: { ledger_time: bigint } } + | { TooOld: null } + | { InsufficientFunds: { balance: bigint } }; export interface _SERVICE { add_mission_control_controllers: ActorMethod<[Array], undefined>; add_satellites_controllers: ActorMethod<[Array, Array], undefined>; @@ -83,11 +127,13 @@ export interface _SERVICE { del_satellites_controllers: ActorMethod<[Array, Array], undefined>; deposit_cycles: ActorMethod<[DepositCyclesArgs], undefined>; get_user: ActorMethod<[], Principal>; + icp_transfer: ActorMethod<[TransferArgs], Result>; + icrc_transfer: ActorMethod<[Principal, TransferArg], Result_1>; list_mission_control_controllers: ActorMethod<[], Array<[Principal, Controller]>>; - list_mission_control_statuses: ActorMethod<[], Array<[bigint, Result]>>; - list_orbiter_statuses: ActorMethod<[Principal], [] | [Array<[bigint, Result]>]>; + list_mission_control_statuses: ActorMethod<[], Array<[bigint, Result_2]>>; + list_orbiter_statuses: ActorMethod<[Principal], [] | [Array<[bigint, Result_2]>]>; list_orbiters: ActorMethod<[], Array<[Principal, Orbiter]>>; - list_satellite_statuses: ActorMethod<[Principal], [] | [Array<[bigint, Result]>]>; + list_satellite_statuses: ActorMethod<[Principal], [] | [Array<[bigint, Result_2]>]>; list_satellites: ActorMethod<[], Array<[Principal, Satellite]>>; remove_mission_control_controllers: ActorMethod<[Array], undefined>; remove_satellites_controllers: ActorMethod<[Array, Array], undefined>; diff --git a/src/declarations/mission_control/mission_control.factory.did.js b/src/declarations/mission_control/mission_control.factory.did.js index 1253f6d19..a6c166f4c 100644 --- a/src/declarations/mission_control/mission_control.factory.did.js +++ b/src/declarations/mission_control/mission_control.factory.did.js @@ -16,6 +16,50 @@ export const idlFactory = ({ IDL }) => { cycles: IDL.Nat, destination_id: IDL.Principal }); + const Tokens = IDL.Record({ e8s: IDL.Nat64 }); + const Timestamp = IDL.Record({ timestamp_nanos: IDL.Nat64 }); + const TransferArgs = IDL.Record({ + to: IDL.Vec(IDL.Nat8), + fee: Tokens, + memo: IDL.Nat64, + from_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)), + created_at_time: IDL.Opt(Timestamp), + amount: Tokens + }); + const TransferError = IDL.Variant({ + TxTooOld: IDL.Record({ allowed_window_nanos: IDL.Nat64 }), + BadFee: IDL.Record({ expected_fee: Tokens }), + TxDuplicate: IDL.Record({ duplicate_of: IDL.Nat64 }), + TxCreatedInFuture: IDL.Null, + InsufficientFunds: IDL.Record({ balance: Tokens }) + }); + const Result = IDL.Variant({ Ok: IDL.Nat64, Err: TransferError }); + const Account = IDL.Record({ + owner: IDL.Principal, + subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)) + }); + const TransferArg = IDL.Record({ + to: Account, + fee: IDL.Opt(IDL.Nat), + memo: IDL.Opt(IDL.Vec(IDL.Nat8)), + from_subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)), + created_at_time: IDL.Opt(IDL.Nat64), + amount: IDL.Nat + }); + const TransferError_1 = IDL.Variant({ + GenericError: IDL.Record({ + message: IDL.Text, + error_code: IDL.Nat + }), + TemporarilyUnavailable: IDL.Null, + BadBurn: IDL.Record({ min_burn_amount: IDL.Nat }), + Duplicate: IDL.Record({ duplicate_of: IDL.Nat }), + BadFee: IDL.Record({ expected_fee: IDL.Nat }), + CreatedInFuture: IDL.Record({ ledger_time: IDL.Nat64 }), + TooOld: IDL.Null, + InsufficientFunds: IDL.Record({ balance: IDL.Nat }) + }); + const Result_1 = IDL.Variant({ Ok: IDL.Nat, Err: TransferError_1 }); const ControllerScope = IDL.Variant({ Write: IDL.Null, Admin: IDL.Null @@ -52,7 +96,7 @@ export const idlFactory = ({ IDL }) => { metadata: IDL.Opt(IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))), status_at: IDL.Nat64 }); - const Result = IDL.Variant({ Ok: SegmentStatus, Err: IDL.Text }); + const Result_2 = IDL.Variant({ Ok: SegmentStatus, Err: IDL.Text }); const SetController = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), scope: ControllerScope, @@ -69,11 +113,10 @@ export const idlFactory = ({ IDL }) => { cycles_threshold: IDL.Opt(IDL.Nat64) }); const SegmentsStatuses = IDL.Record({ - orbiters: IDL.Opt(IDL.Vec(Result)), - satellites: IDL.Opt(IDL.Vec(Result)), - mission_control: Result + orbiters: IDL.Opt(IDL.Vec(Result_2)), + satellites: IDL.Opt(IDL.Vec(Result_2)), + mission_control: Result_2 }); - const Tokens = IDL.Record({ e8s: IDL.Nat64 }); return IDL.Service({ add_mission_control_controllers: IDL.Func([IDL.Vec(IDL.Principal)], [], []), add_satellites_controllers: IDL.Func([IDL.Vec(IDL.Principal), IDL.Vec(IDL.Principal)], [], []), @@ -86,21 +129,27 @@ export const idlFactory = ({ IDL }) => { del_satellites_controllers: IDL.Func([IDL.Vec(IDL.Principal), IDL.Vec(IDL.Principal)], [], []), deposit_cycles: IDL.Func([DepositCyclesArgs], [], []), get_user: IDL.Func([], [IDL.Principal], ['query']), + icp_transfer: IDL.Func([TransferArgs], [Result], []), + icrc_transfer: IDL.Func([IDL.Principal, TransferArg], [Result_1], []), list_mission_control_controllers: IDL.Func( [], [IDL.Vec(IDL.Tuple(IDL.Principal, Controller))], ['query'] ), - list_mission_control_statuses: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Nat64, Result))], ['query']), + list_mission_control_statuses: IDL.Func( + [], + [IDL.Vec(IDL.Tuple(IDL.Nat64, Result_2))], + ['query'] + ), list_orbiter_statuses: IDL.Func( [IDL.Principal], - [IDL.Opt(IDL.Vec(IDL.Tuple(IDL.Nat64, Result)))], + [IDL.Opt(IDL.Vec(IDL.Tuple(IDL.Nat64, Result_2)))], ['query'] ), list_orbiters: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Principal, Orbiter))], ['query']), list_satellite_statuses: IDL.Func( [IDL.Principal], - [IDL.Opt(IDL.Vec(IDL.Tuple(IDL.Nat64, Result)))], + [IDL.Opt(IDL.Vec(IDL.Tuple(IDL.Nat64, Result_2)))], ['query'] ), list_satellites: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Principal, Satellite))], ['query']), diff --git a/src/mission_control/mission_control.did b/src/mission_control/mission_control.did index 623843aee..89aaf3a12 100644 --- a/src/mission_control/mission_control.did +++ b/src/mission_control/mission_control.did @@ -1,3 +1,4 @@ +type Account = record { owner : principal; subaccount : opt blob }; type CanisterStatusType = variant { stopped; stopping; running }; type Controller = record { updated_at : nat64; @@ -18,7 +19,9 @@ type Orbiter = record { metadata : vec record { text; text }; created_at : nat64; }; -type Result = variant { Ok : SegmentStatus; Err : text }; +type Result = variant { Ok : nat64; Err : TransferError }; +type Result_1 = variant { Ok : nat; Err : TransferError_1 }; +type Result_2 = variant { Ok : SegmentStatus; Err : text }; type Satellite = record { updated_at : nat64; metadata : vec record { text; text }; @@ -46,9 +49,9 @@ type SegmentStatus = record { status_at : nat64; }; type SegmentsStatuses = record { - orbiters : opt vec Result; - satellites : opt vec Result; - mission_control : Result; + orbiters : opt vec Result_2; + satellites : opt vec Result_2; + mission_control : Result_2; }; type SetController = record { metadata : vec record { text; text }; @@ -61,7 +64,41 @@ type StatusesArgs = record { satellites : vec record { principal; CronJobStatusesConfig }; cycles_threshold : opt nat64; }; +type Timestamp = record { timestamp_nanos : nat64 }; type Tokens = record { e8s : nat64 }; +type TransferArg = record { + to : Account; + fee : opt nat; + memo : opt blob; + from_subaccount : opt blob; + created_at_time : opt nat64; + amount : nat; +}; +type TransferArgs = record { + to : blob; + fee : Tokens; + memo : nat64; + from_subaccount : opt blob; + created_at_time : opt Timestamp; + amount : Tokens; +}; +type TransferError = variant { + TxTooOld : record { allowed_window_nanos : nat64 }; + BadFee : record { expected_fee : Tokens }; + TxDuplicate : record { duplicate_of : nat64 }; + TxCreatedInFuture; + InsufficientFunds : record { balance : Tokens }; +}; +type TransferError_1 = variant { + GenericError : record { message : text; error_code : nat }; + TemporarilyUnavailable; + BadBurn : record { min_burn_amount : nat }; + Duplicate : record { duplicate_of : nat }; + BadFee : record { expected_fee : nat }; + CreatedInFuture : record { ledger_time : nat64 }; + TooOld; + InsufficientFunds : record { balance : nat }; +}; service : () -> { add_mission_control_controllers : (vec principal) -> (); add_satellites_controllers : (vec principal, vec principal) -> (); @@ -74,16 +111,18 @@ service : () -> { del_satellites_controllers : (vec principal, vec principal) -> (); deposit_cycles : (DepositCyclesArgs) -> (); get_user : () -> (principal) query; + icp_transfer : (TransferArgs) -> (Result); + icrc_transfer : (principal, TransferArg) -> (Result_1); list_mission_control_controllers : () -> ( vec record { principal; Controller }, ) query; - list_mission_control_statuses : () -> (vec record { nat64; Result }) query; + list_mission_control_statuses : () -> (vec record { nat64; Result_2 }) query; list_orbiter_statuses : (principal) -> ( - opt vec record { nat64; Result }, + opt vec record { nat64; Result_2 }, ) query; list_orbiters : () -> (vec record { principal; Orbiter }) query; list_satellite_statuses : (principal) -> ( - opt vec record { nat64; Result }, + opt vec record { nat64; Result_2 }, ) query; list_satellites : () -> (vec record { principal; Satellite }) query; remove_mission_control_controllers : (vec principal) -> (); From 779d0b5e1019d4c837387197c24de0afcb78b7a2 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 15:28:33 +0200 Subject: [PATCH 17/31] feat: adapt to type Signed-off-by: David Dal Busco --- src/frontend/src/lib/api/mission-control.api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/lib/api/mission-control.api.ts b/src/frontend/src/lib/api/mission-control.api.ts index a2c798d80..d18ac2935 100644 --- a/src/frontend/src/lib/api/mission-control.api.ts +++ b/src/frontend/src/lib/api/mission-control.api.ts @@ -1,7 +1,7 @@ import type { Controller, Orbiter, - Result, + Result_2, Satellite } from '$declarations/mission_control/mission_control.did'; import type { SetControllerParams } from '$lib/types/controllers'; @@ -361,7 +361,7 @@ export const listSatelliteStatuses = async ({ missionControlId: Principal; identity: OptionIdentity; satelliteId: Principal; -}): Promise<[] | [[bigint, Result][]]> => { +}): Promise<[] | [[bigint, Result_2][]]> => { const { list_satellite_statuses } = await getMissionControlActor({ missionControlId, identity }); return list_satellite_statuses(satelliteId); }; @@ -374,7 +374,7 @@ export const listOrbiterStatuses = async ({ missionControlId: Principal; identity: OptionIdentity; orbiterId: Principal; -}): Promise<[] | [[bigint, Result][]]> => { +}): Promise<[] | [[bigint, Result_2][]]> => { const { list_orbiter_statuses } = await getMissionControlActor({ missionControlId, identity }); return list_orbiter_statuses(orbiterId); }; @@ -385,7 +385,7 @@ export const listMissionControlStatuses = async ({ }: { missionControlId: Principal; identity: OptionIdentity; -}): Promise<[] | [[bigint, Result][]]> => { +}): Promise<[] | [[bigint, Result_2][]]> => { const { list_mission_control_statuses } = await getMissionControlActor({ missionControlId, identity From 28fb61506f70752b076f7281e3a06a5e50ada64b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 15:35:12 +0200 Subject: [PATCH 18/31] feat: in progress Signed-off-by: David Dal Busco --- src/frontend/src/lib/api/icp-ledger.api.ts | 60 ------------------- .../src/lib/api/mission-control.api.ts | 34 ++++++++++- .../src/lib/services/tokens.services.ts | 35 ++++++++--- 3 files changed, 60 insertions(+), 69 deletions(-) delete mode 100644 src/frontend/src/lib/api/icp-ledger.api.ts diff --git a/src/frontend/src/lib/api/icp-ledger.api.ts b/src/frontend/src/lib/api/icp-ledger.api.ts deleted file mode 100644 index b769d96f4..000000000 --- a/src/frontend/src/lib/api/icp-ledger.api.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { getAgent } from '$lib/utils/agent.utils'; -import { nowInBigIntNanoSeconds } from '$lib/utils/date.utils'; -import type { Identity } from '@dfinity/agent'; -import { AccountIdentifier, LedgerCanister, type BlockHeight } from '@dfinity/ledger-icp'; -import type { IcrcAccount } from '@dfinity/ledger-icrc'; -import { assertNonNullish, toNullable } from '@dfinity/utils'; - -export const transfer = async ({ - identity, - to, - amount -}: { - identity: Identity | undefined | null; - to: string; - amount: bigint; -}): Promise => { - assertNonNullish(identity); - - const { transfer } = await ledgerCanister(identity); - - console.log(to, amount, identity.getPrincipal().toText()); - - return transfer({ - to: AccountIdentifier.fromHex(to), - amount - }); -}; - -export const icrc1Transfer = async ({ - identity, - to, - amount, - createdAt -}: { - identity: Identity | undefined | null; - to: IcrcAccount; - amount: bigint; - createdAt?: bigint; -}): Promise => { - assertNonNullish(identity); - - const { icrc1Transfer } = await ledgerCanister(identity); - - return icrc1Transfer({ - to: { - owner: to.owner, - subaccount: toNullable(to.subaccount) - }, - amount, - createdAt: createdAt ?? nowInBigIntNanoSeconds() - }); -}; - -const ledgerCanister = async (identity: Identity): Promise => { - const agent = await getAgent({ identity }); - - return LedgerCanister.create({ - agent - }); -}; diff --git a/src/frontend/src/lib/api/mission-control.api.ts b/src/frontend/src/lib/api/mission-control.api.ts index d18ac2935..19f005dba 100644 --- a/src/frontend/src/lib/api/mission-control.api.ts +++ b/src/frontend/src/lib/api/mission-control.api.ts @@ -1,8 +1,12 @@ import type { Controller, Orbiter, + Result, + Result_1, Result_2, - Satellite + Satellite, + TransferArg, + TransferArgs } from '$declarations/mission_control/mission_control.did'; import type { SetControllerParams } from '$lib/types/controllers'; import type { OptionIdentity } from '$lib/types/itentity'; @@ -392,3 +396,31 @@ export const listMissionControlStatuses = async ({ }); return [await list_mission_control_statuses()]; }; + +export const icpTransfer = async ({ + missionControlId, + args, + identity +}: { + missionControlId: Principal; + args: TransferArgs; + identity: OptionIdentity; +}): Promise => { + const { icp_transfer } = await getMissionControlActor({ missionControlId, identity }); + return icp_transfer(args); +}; + +export const icrcTransfer = async ({ + ledgerId, + missionControlId, + args, + identity +}: { + ledgerId: Principal; + missionControlId: Principal; + args: TransferArg; + identity: OptionIdentity; +}): Promise => { + const { icrc_transfer } = await getMissionControlActor({ missionControlId, identity }); + return icrc_transfer(ledgerId, args); +}; diff --git a/src/frontend/src/lib/services/tokens.services.ts b/src/frontend/src/lib/services/tokens.services.ts index 3557e9de7..fc6e80c92 100644 --- a/src/frontend/src/lib/services/tokens.services.ts +++ b/src/frontend/src/lib/services/tokens.services.ts @@ -1,4 +1,3 @@ -import { icrc1Transfer, transfer } from '$lib/api/icp-ledger.api'; import { i18n } from '$lib/stores/i18n.store'; import { toasts } from '$lib/stores/toasts.store'; import { invalidIcpAddress } from '$lib/utils/icp-account.utils'; @@ -7,6 +6,10 @@ import type { Identity } from '@dfinity/agent'; import { decodeIcrcAccount } from '@dfinity/ledger-icrc'; import { isNullish, type TokenAmountV2 } from '@dfinity/utils'; import { get } from 'svelte/store'; +import {icpTransfer} from "$lib/api/mission-control.api"; +import type {TransferArgs} from "$declarations/mission_control/mission_control.did"; +import {AccountIdentifier} from "@dfinity/ledger-icp"; +import {toTransferRawRequest} from "@dfinity/ledger-icp/dist/types/canisters/ledger/ledger.request.converts"; export const sendTokens = async ({ destination, @@ -20,8 +23,6 @@ export const sendTokens = async ({ const notIcp = invalidIcpAddress(destination); const notIcrc = invalidIcrcAddress(destination); - console.log(token?.toE8s(), destination); - if (notIcp && notIcrc) { const labels = get(i18n); @@ -54,11 +55,11 @@ export const sendTokens = async ({ return { success: true }; } - await transfer({ - identity, - to: destination, - amount - }); + await sendIcp({ + destination, + token, + identity + }) return { success: true }; } catch (err: unknown) { @@ -72,3 +73,21 @@ export const sendTokens = async ({ return { success: false }; } }; + +export const sendIcp = async ({ + destination, + token, + identity + }: { + destination: string; + token: TokenAmountV2; + identity: Identity | undefined | null; +}): Promise => { + const args: TransferArgs = toTransferRawRequest() + + await icpTransfer({ + identity, + to: destination, + amount + }); +} \ No newline at end of file From 4d22af09ba9e14908de44bdeb91d5bdf1b7f70fd Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 15:53:26 +0200 Subject: [PATCH 19/31] feat: ledger-icrc at runtime Signed-off-by: David Dal Busco --- package-lock.json | 4 +--- package.json | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index de74aa481..795eefb01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@dfinity/cmc": "^3.1.0", "@dfinity/identity": "^1.4.0", "@dfinity/ledger-icp": "^2.4.0", + "@dfinity/ledger-icrc": "^2.4.0", "@dfinity/principal": "^1.4.0", "@dfinity/utils": "^2.4.0", "@junobuild/admin": "^0.0.55", @@ -30,7 +31,6 @@ "devDependencies": { "@dfinity/ic-management": "^5.1.0", "@dfinity/identity-secp256k1": "^1.4.0", - "@dfinity/ledger-icrc": "^2.4.0", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@hadronous/pic": "^0.8.1", "@junobuild/cli-tools": "^0.0.14", @@ -863,7 +863,6 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/@dfinity/ledger-icrc/-/ledger-icrc-2.4.0.tgz", "integrity": "sha512-7NMFU3uoQ0QOarIzGEjmXkVEJZYSp9L4dj5ZR+TM5S0Dvd1TMmMb8GOW8TCN9WGDQUfuhd2Pe3l6dRykRq23gQ==", - "dev": true, "license": "Apache-2.0", "peerDependencies": { "@dfinity/agent": "^1.4.0", @@ -7843,7 +7842,6 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/@dfinity/ledger-icrc/-/ledger-icrc-2.4.0.tgz", "integrity": "sha512-7NMFU3uoQ0QOarIzGEjmXkVEJZYSp9L4dj5ZR+TM5S0Dvd1TMmMb8GOW8TCN9WGDQUfuhd2Pe3l6dRykRq23gQ==", - "dev": true, "requires": {} }, "@dfinity/principal": { diff --git a/package.json b/package.json index c38754e03..c18d6304e 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "devDependencies": { "@dfinity/ic-management": "^5.1.0", "@dfinity/identity-secp256k1": "^1.4.0", - "@dfinity/ledger-icrc": "^2.4.0", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@hadronous/pic": "^0.8.1", "@junobuild/cli-tools": "^0.0.14", @@ -89,6 +88,7 @@ "@dfinity/cmc": "^3.1.0", "@dfinity/identity": "^1.4.0", "@dfinity/ledger-icp": "^2.4.0", + "@dfinity/ledger-icrc": "^2.4.0", "@dfinity/principal": "^1.4.0", "@dfinity/utils": "^2.4.0", "@junobuild/admin": "^0.0.55", From 17d1318643b0a3be6b4b02db1b8f30b4facd7749 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 16:08:47 +0200 Subject: [PATCH 20/31] feat: send icrc and icp Signed-off-by: David Dal Busco --- src/frontend/src/lib/constants/constants.ts | 1 + .../src/lib/services/tokens.services.ts | 92 +++++++++++++------ 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/src/frontend/src/lib/constants/constants.ts b/src/frontend/src/lib/constants/constants.ts index 636b19d30..ddba449b9 100644 --- a/src/frontend/src/lib/constants/constants.ts +++ b/src/frontend/src/lib/constants/constants.ts @@ -35,6 +35,7 @@ export const INTERNET_IDENTITY_CANISTER_ID = 'rdmx6-jaaaa-aaaaa-aaadq-cai'; export const CMC_CANISTER_ID = 'rkp4c-7iaaa-aaaaa-aaaca-cai'; export const CONSOLE_CANISTER_ID = 'cokmz-oiaaa-aaaal-aby6q-cai'; export const OBSERVATORY_CANISTER_ID = 'klbfr-lqaaa-aaaak-qbwsa-cai'; +export const ICP_LEDGER_CANISTER_ID = 'ryjl3-tyaaa-aaaaa-aaaba-cai'; /** * Revoked principals that must not be used. diff --git a/src/frontend/src/lib/services/tokens.services.ts b/src/frontend/src/lib/services/tokens.services.ts index fc6e80c92..e3a1a16e1 100644 --- a/src/frontend/src/lib/services/tokens.services.ts +++ b/src/frontend/src/lib/services/tokens.services.ts @@ -1,24 +1,28 @@ +import type { TransferArg, TransferArgs } from '$declarations/mission_control/mission_control.did'; +import { icpTransfer, icrcTransfer } from '$lib/api/mission-control.api'; +import { ICP_LEDGER_CANISTER_ID, IC_TRANSACTION_FEE_ICP } from '$lib/constants/constants'; import { i18n } from '$lib/stores/i18n.store'; import { toasts } from '$lib/stores/toasts.store'; +import { nowInBigIntNanoSeconds } from '$lib/utils/date.utils'; import { invalidIcpAddress } from '$lib/utils/icp-account.utils'; import { invalidIcrcAddress } from '$lib/utils/icrc-account.utils'; import type { Identity } from '@dfinity/agent'; +import { AccountIdentifier } from '@dfinity/ledger-icp'; import { decodeIcrcAccount } from '@dfinity/ledger-icrc'; -import { isNullish, type TokenAmountV2 } from '@dfinity/utils'; +import { Principal } from '@dfinity/principal'; +import { isNullish, toNullable, type TokenAmountV2 } from '@dfinity/utils'; import { get } from 'svelte/store'; -import {icpTransfer} from "$lib/api/mission-control.api"; -import type {TransferArgs} from "$declarations/mission_control/mission_control.did"; -import {AccountIdentifier} from "@dfinity/ledger-icp"; -import {toTransferRawRequest} from "@dfinity/ledger-icp/dist/types/canisters/ledger/ledger.request.converts"; export const sendTokens = async ({ destination, token, - identity + identity, + missionControlId }: { destination: string; token: TokenAmountV2 | undefined; identity: Identity | undefined | null; + missionControlId: Principal; }): Promise<{ success: boolean }> => { const notIcp = invalidIcpAddress(destination); const notIcrc = invalidIcrcAddress(destination); @@ -41,25 +45,15 @@ export const sendTokens = async ({ return { success: false }; } - const amount = token.toE8s(); - try { - // TODO: the send should happens within the mission control - - if (!invalidIcrcAddress) { - await icrc1Transfer({ - identity, - to: decodeIcrcAccount(destination), - amount - }); - return { success: true }; - } + const fn = !invalidIcrcAddress ? sendIcrc : sendIcp; - await sendIcp({ + await fn({ destination, token, - identity - }) + identity, + missionControlId + }); return { success: true }; } catch (err: unknown) { @@ -74,20 +68,58 @@ export const sendTokens = async ({ } }; +export const sendIcrc = async ({ + destination, + token, + ...rest +}: { + destination: string; + token: TokenAmountV2; + identity: Identity | undefined | null; + missionControlId: Principal; +}): Promise => { + const { owner, subaccount } = decodeIcrcAccount(destination); + + const args: TransferArg = { + to: { + owner, + subaccount: toNullable(subaccount) + }, + amount: token.toE8s(), + fee: toNullable(IC_TRANSACTION_FEE_ICP), + memo: toNullable(), + from_subaccount: toNullable(), + created_at_time: toNullable(nowInBigIntNanoSeconds()) + }; + + await icrcTransfer({ + args, + ledgerId: Principal.fromText(ICP_LEDGER_CANISTER_ID), + ...rest + }); +}; + export const sendIcp = async ({ - destination, - token, - identity - }: { + destination, + token, + ...rest +}: { destination: string; token: TokenAmountV2; identity: Identity | undefined | null; + missionControlId: Principal; }): Promise => { - const args: TransferArgs = toTransferRawRequest() + const args: TransferArgs = { + to: AccountIdentifier.fromHex(destination).toUint8Array(), + amount: { e8s: token.toE8s() }, + fee: { e8s: IC_TRANSACTION_FEE_ICP }, + memo: 0n, + created_at_time: toNullable({ timestamp_nanos: nowInBigIntNanoSeconds() }), + from_subaccount: toNullable() + }; await icpTransfer({ - identity, - to: destination, - amount + args, + ...rest }); -} \ No newline at end of file +}; From f3b2d0e7680579b5137396c1de9041e904d0b18a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 16:14:09 +0200 Subject: [PATCH 21/31] fix: missing id Signed-off-by: David Dal Busco --- src/frontend/src/lib/components/tokens/SendTokensReview.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend/src/lib/components/tokens/SendTokensReview.svelte b/src/frontend/src/lib/components/tokens/SendTokensReview.svelte index e180302ed..c7fa4cc10 100644 --- a/src/frontend/src/lib/components/tokens/SendTokensReview.svelte +++ b/src/frontend/src/lib/components/tokens/SendTokensReview.svelte @@ -34,6 +34,7 @@ try { await sendTokens({ + missionControlId, identity: $authStore.identity, destination, token From 9eb56fbe781e9ea2227743646f63007a74923f21 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 16:15:40 +0200 Subject: [PATCH 22/31] fix: check Signed-off-by: David Dal Busco --- src/frontend/src/lib/services/tokens.services.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/lib/services/tokens.services.ts b/src/frontend/src/lib/services/tokens.services.ts index e3a1a16e1..560c30bb2 100644 --- a/src/frontend/src/lib/services/tokens.services.ts +++ b/src/frontend/src/lib/services/tokens.services.ts @@ -46,7 +46,7 @@ export const sendTokens = async ({ } try { - const fn = !invalidIcrcAddress ? sendIcrc : sendIcp; + const fn = !invalidIcpAddress(destination) ? sendIcp : sendIcrc; await fn({ destination, @@ -92,6 +92,8 @@ export const sendIcrc = async ({ created_at_time: toNullable(nowInBigIntNanoSeconds()) }; + console.log(args); + await icrcTransfer({ args, ledgerId: Principal.fromText(ICP_LEDGER_CANISTER_ID), From 809296df91ac23fd487ab304c6c0f633f88aca7f Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 16:16:15 +0200 Subject: [PATCH 23/31] chore: remove console Signed-off-by: David Dal Busco --- src/frontend/src/lib/services/tokens.services.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/frontend/src/lib/services/tokens.services.ts b/src/frontend/src/lib/services/tokens.services.ts index 560c30bb2..06efbeb4a 100644 --- a/src/frontend/src/lib/services/tokens.services.ts +++ b/src/frontend/src/lib/services/tokens.services.ts @@ -92,8 +92,6 @@ export const sendIcrc = async ({ created_at_time: toNullable(nowInBigIntNanoSeconds()) }; - console.log(args); - await icrcTransfer({ args, ledgerId: Principal.fromText(ICP_LEDGER_CANISTER_ID), From f646bdb3a795d8e43f09f2813264b2b43e91cf58 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 16:57:11 +0200 Subject: [PATCH 24/31] feat: flow and success Signed-off-by: David Dal Busco --- .../mission-control/MissionControl.svelte | 6 +-- .../components/modals/SendTokensModal.svelte | 49 +++++++++++++++---- .../src/lib/components/ui/Identifier.svelte | 4 ++ src/frontend/src/lib/i18n/en.json | 3 +- src/frontend/src/lib/types/i18n.d.ts | 1 + 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/frontend/src/lib/components/mission-control/MissionControl.svelte b/src/frontend/src/lib/components/mission-control/MissionControl.svelte index 23060d7db..382623645 100644 --- a/src/frontend/src/lib/components/mission-control/MissionControl.svelte +++ b/src/frontend/src/lib/components/mission-control/MissionControl.svelte @@ -20,11 +20,7 @@
{$i18n.mission_control.id} - +
diff --git a/src/frontend/src/lib/components/modals/SendTokensModal.svelte b/src/frontend/src/lib/components/modals/SendTokensModal.svelte index 4d8b3003a..0c10578d4 100644 --- a/src/frontend/src/lib/components/modals/SendTokensModal.svelte +++ b/src/frontend/src/lib/components/modals/SendTokensModal.svelte @@ -7,6 +7,8 @@ import type { JunoModalDetail, JunoModalSendTokensDetail } from '$lib/types/modal'; import SendTokensForm from '$lib/components/tokens/SendTokensForm.svelte'; import SendTokensReview from '$lib/components/tokens/SendTokensReview.svelte'; + import Confetti from '$lib/components/ui/Confetti.svelte'; + import { fade } from 'svelte/transition'; export let detail: JunoModalDetail; @@ -24,8 +26,10 @@ {#if nonNullish($missionControlStore)} {#if steps === 'ready'} -
-

Done

+ + +
+

{$i18n.wallet.icp_on_its_way}

{:else if steps === 'in_progress'} @@ -33,13 +37,15 @@

{$i18n.wallet.sending_in_progress}

{:else if steps === 'review'} - (steps = detail)} - /> +
+ (steps = detail)} + /> +
{:else} {/if} + + \ No newline at end of file diff --git a/src/frontend/src/lib/components/ui/Identifier.svelte b/src/frontend/src/lib/components/ui/Identifier.svelte index f389e94ab..a7a053a03 100644 --- a/src/frontend/src/lib/components/ui/Identifier.svelte +++ b/src/frontend/src/lib/components/ui/Identifier.svelte @@ -34,5 +34,9 @@ .nomargin { margin: 0; + + span { + margin: 0; + } } diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index 290ab6f56..3e7d38608 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -214,7 +214,8 @@ "sending": "Sending", "sending_in_progress": "Sending...", "fee": "Fee", - "review_and_confirm": "Review the details below and confirm to proceed with your transaction." + "review_and_confirm": "Review the details below and confirm to proceed with your transaction.", + "icp_on_its_way": "Your ICP is on its way!" }, "authentication": { "title": "Authentication", diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index 1c4b35090..b3618e2ce 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -223,6 +223,7 @@ interface I18nWallet { sending_in_progress: string; fee: string; review_and_confirm: string; + icp_on_its_way: string; } interface I18nAuthentication { From 22b6c1114f9a1fc04597417b6c770a8c910bb032 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 17:02:23 +0200 Subject: [PATCH 25/31] style: spacing Signed-off-by: David Dal Busco --- src/frontend/src/lib/components/ui/Identifier.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/lib/components/ui/Identifier.svelte b/src/frontend/src/lib/components/ui/Identifier.svelte index c13c70f07..e7d8e3f21 100644 --- a/src/frontend/src/lib/components/ui/Identifier.svelte +++ b/src/frontend/src/lib/components/ui/Identifier.svelte @@ -11,7 +11,7 @@

- {shortIdentifier} + {shortIdentifier}

@@ -22,7 +22,9 @@ word-break: break-all; @include text.truncate; - margin: 0 0 var(--padding-0_5x); + &.small { + margin: 0 0 var(--padding-0_5x); + } } p { From 9d44a48e12fc9e05348f8cde386d8f21a94a07ee Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 17:23:33 +0200 Subject: [PATCH 26/31] feat: close Signed-off-by: David Dal Busco --- .../lib/components/modals/SendTokensModal.svelte | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/lib/components/modals/SendTokensModal.svelte b/src/frontend/src/lib/components/modals/SendTokensModal.svelte index 0c10578d4..e500185aa 100644 --- a/src/frontend/src/lib/components/modals/SendTokensModal.svelte +++ b/src/frontend/src/lib/components/modals/SendTokensModal.svelte @@ -9,6 +9,7 @@ import SendTokensReview from '$lib/components/tokens/SendTokensReview.svelte'; import Confetti from '$lib/components/ui/Confetti.svelte'; import { fade } from 'svelte/transition'; + import { createEventDispatcher } from 'svelte'; export let detail: JunoModalDetail; @@ -19,6 +20,9 @@ let destination = ''; let amount: string | undefined; + + const dispatch = createEventDispatcher(); + const close = () => dispatch('junoClose'); (balance = syncBalance)} /> @@ -39,11 +43,11 @@ {:else if steps === 'review'}
(steps = detail)} + missionControlId={$missionControlStore} + {balance} + bind:amount + bind:destination + on:junoNext={({ detail }) => (steps = detail)} />
{:else} @@ -80,4 +84,4 @@ button { margin-top: var(--padding-2x); } - \ No newline at end of file + From 782f5b67a001645863d43663bc58edc85e407092 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 18:32:49 +0200 Subject: [PATCH 27/31] feat: send if newer version Signed-off-by: David Dal Busco --- .../mission-control/MissionControlWallet.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte index 3646b528b..8b3fc9437 100644 --- a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte +++ b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte @@ -19,6 +19,8 @@ import type { TransactionWithId } from '@dfinity/ledger-icp'; import Wallet from '$lib/components/core/Wallet.svelte'; import { emit } from '$lib/utils/events.utils'; + import { versionStore } from '$lib/stores/version.store'; + import { compare } from 'semver'; export let missionControlId: Principal; @@ -112,6 +114,10 @@ } }); }; + + let send = false; + $: send = + balance > 0n && compare($versionStore?.missionControl?.current ?? '0.0.0', '0.0.12') > 0; {#if $authSignedInStore} @@ -163,7 +169,7 @@
- {#if balance > 0n} + {#if send} {/if} From 6d03b207e59ff30c804549ac9b17c248f2420e4b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 11 Oct 2024 19:06:54 +0200 Subject: [PATCH 28/31] test: only owner can call wallet Signed-off-by: David Dal Busco --- src/tests/mission-control-wallet.spec.ts | 101 +++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/tests/mission-control-wallet.spec.ts diff --git a/src/tests/mission-control-wallet.spec.ts b/src/tests/mission-control-wallet.spec.ts new file mode 100644 index 000000000..c387335bf --- /dev/null +++ b/src/tests/mission-control-wallet.spec.ts @@ -0,0 +1,101 @@ +import type { + _SERVICE as MissionControlActor, + TransferArg, + TransferArgs +} from '$declarations/mission_control/mission_control.did'; +import { idlFactory as idlFactorMissionControl } from '$declarations/mission_control/mission_control.factory.did'; +import { AnonymousIdentity } from '@dfinity/agent'; +import { IDL } from '@dfinity/candid'; +import { Ed25519KeyIdentity } from '@dfinity/identity'; +import { AccountIdentifier } from '@dfinity/ledger-icp'; +import { Principal } from '@dfinity/principal'; +import { PocketIc, type Actor } from '@hadronous/pic'; +import { beforeAll, describe, expect, inject } from 'vitest'; +import { CONTROLLER_ERROR_MSG } from './constants/mission-control-tests.constants'; +import { MISSION_CONTROL_WASM_PATH } from './utils/setup-tests.utils'; + +describe('Mission Control - Wallet', () => { + let pic: PocketIc; + let actor: Actor; + + const to = Ed25519KeyIdentity.generate(); + + const LEDGER_ID = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); + + const args: TransferArgs = { + to: AccountIdentifier.fromPrincipal({ principal: to.getPrincipal() }).toUint8Array(), + amount: { e8s: 100_000n }, + fee: { e8s: 10_000n }, + memo: 0n, + created_at_time: [], + from_subaccount: [] + }; + + const arg: TransferArg = { + to: { + owner: to.getPrincipal(), + subaccount: [] + }, + amount: 100_000n, + fee: [10_000n], + memo: [], + from_subaccount: [], + created_at_time: [] + }; + + const incorrectUser = Ed25519KeyIdentity.generate(); + const controller = Ed25519KeyIdentity.generate(); + + beforeAll(async () => { + pic = await PocketIc.create(inject('PIC_URL')); + + const userInitArgs = (): ArrayBuffer => + IDL.encode( + [ + IDL.Record({ + user: IDL.Principal + }) + ], + [{ user: incorrectUser.getPrincipal() }] + ); + + const { actor: c } = await pic.setupCanister({ + idlFactory: idlFactorMissionControl, + wasm: MISSION_CONTROL_WASM_PATH, + arg: userInitArgs(), + sender: controller.getPrincipal() + }); + + actor = c; + }); + + const testIdentity = () => { + it('should throw errors on icp transfer', async () => { + const { icp_transfer } = actor; + + await expect(icp_transfer(args)).rejects.toThrow(CONTROLLER_ERROR_MSG); + }); + + it('should throw errors on icrc transfer', async () => { + const { icrc_transfer } = actor; + + await expect(icrc_transfer(LEDGER_ID, arg)).rejects.toThrow(CONTROLLER_ERROR_MSG); + }); + }; + + describe('anonymous', () => { + beforeAll(() => { + actor.setIdentity(new AnonymousIdentity()); + }); + + testIdentity(); + }); + + describe('unknown identity', () => { + beforeAll(() => { + actor.setIdentity(Ed25519KeyIdentity.generate()); + }); + + testIdentity(); + }); +}); From 7a9e7331fc7184df2bfea05f3e6d6bbad4db290c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 12 Oct 2024 08:57:23 +0200 Subject: [PATCH 29/31] test: transfer icp error and success Signed-off-by: David Dal Busco --- src/tests/constants/ledger-tests.contants.ts | 3 + src/tests/mission-control-wallet.spec.ts | 151 ++++++++++++++++--- src/tests/utils/ledger-tests.utils.ts | 77 ++++++++++ src/tests/utils/setup-tests.utils.ts | 2 +- 4 files changed, 208 insertions(+), 25 deletions(-) create mode 100644 src/tests/constants/ledger-tests.contants.ts create mode 100644 src/tests/utils/ledger-tests.utils.ts diff --git a/src/tests/constants/ledger-tests.contants.ts b/src/tests/constants/ledger-tests.contants.ts new file mode 100644 index 000000000..dfacaf25e --- /dev/null +++ b/src/tests/constants/ledger-tests.contants.ts @@ -0,0 +1,3 @@ +import { Principal } from '@dfinity/principal'; + +export const LEDGER_ID = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); diff --git a/src/tests/mission-control-wallet.spec.ts b/src/tests/mission-control-wallet.spec.ts index c387335bf..f4c5759f4 100644 --- a/src/tests/mission-control-wallet.spec.ts +++ b/src/tests/mission-control-wallet.spec.ts @@ -8,20 +8,22 @@ import { AnonymousIdentity } from '@dfinity/agent'; import { IDL } from '@dfinity/candid'; import { Ed25519KeyIdentity } from '@dfinity/identity'; import { AccountIdentifier } from '@dfinity/ledger-icp'; +import type { _SERVICE as LedgerActor } from '@dfinity/ledger-icp/dist/candid/ledger'; import { Principal } from '@dfinity/principal'; import { PocketIc, type Actor } from '@hadronous/pic'; -import { beforeAll, describe, expect, inject } from 'vitest'; +import { afterAll, beforeAll, describe, expect, inject } from 'vitest'; +import { LEDGER_ID } from './constants/ledger-tests.contants'; import { CONTROLLER_ERROR_MSG } from './constants/mission-control-tests.constants'; +import { setupLedger } from './utils/ledger-tests.utils'; import { MISSION_CONTROL_WASM_PATH } from './utils/setup-tests.utils'; describe('Mission Control - Wallet', () => { let pic: PocketIc; let actor: Actor; + let missionControlId: Principal; const to = Ed25519KeyIdentity.generate(); - const LEDGER_ID = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); - const args: TransferArgs = { to: AccountIdentifier.fromPrincipal({ principal: to.getPrincipal() }).toUint8Array(), amount: { e8s: 100_000n }, @@ -47,8 +49,14 @@ describe('Mission Control - Wallet', () => { const controller = Ed25519KeyIdentity.generate(); beforeAll(async () => { - pic = await PocketIc.create(inject('PIC_URL')); + pic = await PocketIc.create(inject('PIC_URL'), { nns: true }); + }); + afterAll(async () => { + await pic?.tearDown(); + }); + + const initMissionControl = async (owner: Principal) => { const userInitArgs = (): ArrayBuffer => IDL.encode( [ @@ -56,10 +64,10 @@ describe('Mission Control - Wallet', () => { user: IDL.Principal }) ], - [{ user: incorrectUser.getPrincipal() }] + [{ user: owner }] ); - const { actor: c } = await pic.setupCanister({ + const { actor: c, canisterId } = await pic.setupCanister({ idlFactory: idlFactorMissionControl, wasm: MISSION_CONTROL_WASM_PATH, arg: userInitArgs(), @@ -67,35 +75,130 @@ describe('Mission Control - Wallet', () => { }); actor = c; - }); + missionControlId = canisterId; + }; + + describe('Guards', () => { + beforeAll(async () => { + await initMissionControl(incorrectUser.getPrincipal()); + }); + + const testIdentity = () => { + it('should throw errors on icp transfer', async () => { + const { icp_transfer } = actor; - const testIdentity = () => { - it('should throw errors on icp transfer', async () => { - const { icp_transfer } = actor; + await expect(icp_transfer(args)).rejects.toThrow(CONTROLLER_ERROR_MSG); + }); - await expect(icp_transfer(args)).rejects.toThrow(CONTROLLER_ERROR_MSG); + it('should throw errors on icrc transfer', async () => { + const { icrc_transfer } = actor; + + await expect(icrc_transfer(LEDGER_ID, arg)).rejects.toThrow(CONTROLLER_ERROR_MSG); + }); + }; + + describe('anonymous', () => { + beforeAll(() => { + actor.setIdentity(new AnonymousIdentity()); + }); + + testIdentity(); }); - it('should throw errors on icrc transfer', async () => { - const { icrc_transfer } = actor; + describe('unknown identity', () => { + beforeAll(() => { + actor.setIdentity(Ed25519KeyIdentity.generate()); + }); - await expect(icrc_transfer(LEDGER_ID, arg)).rejects.toThrow(CONTROLLER_ERROR_MSG); + testIdentity(); }); - }; + }); + + describe('owner', () => { + let ledgerActor: Actor; + + beforeAll(async () => { + await initMissionControl(controller.getPrincipal()); + + actor.setIdentity(controller); - describe('anonymous', () => { - beforeAll(() => { - actor.setIdentity(new AnonymousIdentity()); + const { actor: c } = await setupLedger({ pic, controller }); + ledgerActor = c; }); - testIdentity(); - }); + describe('InsufficientFunds', () => { + it('should fail at icp transfer', async () => { + const { icp_transfer } = actor; + + const result = await icp_transfer(args); + + if ('Ok' in result) { + throw new Error('Unexpected result. Icp transfer should have failed.'); + } + + expect(result.Err).toEqual({ + InsufficientFunds: { + balance: { + e8s: 0n + } + } + }); + }); + + it('should fail at icrc transfer', async () => { + const { icrc_transfer } = actor; - describe('unknown identity', () => { - beforeAll(() => { - actor.setIdentity(Ed25519KeyIdentity.generate()); + const result = await icrc_transfer(LEDGER_ID, arg); + + if ('Ok' in result) { + throw new Error('Unexpected result. Icrc transfer should have failed.'); + } + + expect(result.Err).toEqual({ + InsufficientFunds: { + balance: 0n + } + }); + }); }); - testIdentity(); + describe('Transfer success', () => { + beforeAll(async () => { + const { icrc1_transfer } = ledgerActor; + + await icrc1_transfer({ + amount: 5_500_010_000n, + to: { owner: missionControlId, subaccount: [] }, + fee: [], + memo: [], + from_subaccount: [], + created_at_time: [] + }); + }); + + it('should execute icp transfer', async () => { + const { icp_transfer } = actor; + + const result = await icp_transfer(args); + + if ('Err' in result) { + throw new Error('Unexpected result. Icrc transfer should have succeeded.'); + } + + expect(result.Ok).toEqual(2n); + }); + + it('should execute icrc transfer', async () => { + const { icrc_transfer } = actor; + + const result = await icrc_transfer(LEDGER_ID, arg); + + if ('Err' in result) { + throw new Error('Unexpected result. Icrc transfer should have succeeded.'); + } + + expect(result.Ok).toEqual(3n); + }); + }); }); }); diff --git a/src/tests/utils/ledger-tests.utils.ts b/src/tests/utils/ledger-tests.utils.ts new file mode 100644 index 000000000..b4566f775 --- /dev/null +++ b/src/tests/utils/ledger-tests.utils.ts @@ -0,0 +1,77 @@ +import type { Identity } from '@dfinity/agent'; +import { IDL } from '@dfinity/candid'; +import { Ed25519KeyIdentity } from '@dfinity/identity'; +import { AccountIdentifier } from '@dfinity/ledger-icp'; +import type { _SERVICE as LedgerActor } from '@dfinity/ledger-icp/dist/candid/ledger'; +// @ts-expect-error +import { idlFactory, init } from '@dfinity/ledger-icp/dist/candid/ledger.idl.js'; +import type { CanisterFixture, PocketIc } from '@hadronous/pic'; +import { assertNonNullish } from '@junobuild/utils'; +import { LEDGER_ID } from '../constants/ledger-tests.contants'; +import { download } from './setup-tests.utils'; + +const IC_VERSION = 'b0ade55f7e8999e2842fe3f49df163ba224b71a2'; +const url = `https://download.dfinity.systems/ic/${IC_VERSION}/canisters/ledger-canister.wasm.gz`; + +export const setupLedger = async ({ + pic, + controller +}: { + pic: PocketIc; + controller: Identity; +}): Promise> => { + const destination = await download({ + wasm: 'ledger.wasm.gz', + url + }); + + const minter = Ed25519KeyIdentity.generate(); + + const minterAccountIdentifier = AccountIdentifier.fromPrincipal({ + principal: minter.getPrincipal() + }).toHex(); + + const ledgerAccountIdentifier = AccountIdentifier.fromPrincipal({ + principal: controller.getPrincipal() + }).toHex(); + + const initArgs = { + send_whitelist: [], + token_symbol: ['ICP'], + transfer_fee: [{ e8s: 10_000n }], + minting_account: minterAccountIdentifier, + maximum_number_of_accounts: [], + accounts_overflow_trim_quantity: [], + transaction_window: [], + max_message_size_bytes: [], + icrc1_minting_account: [], + archive_options: [], + initial_values: [[ledgerAccountIdentifier, { e8s: 100_000_000_000n }]], + token_name: ['Internet Computer'], + feature_flags: [] + }; + + // Type definitions generated by Candid are not clean enough. + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const arg = IDL.encode(init({ IDL }), [{ Init: initArgs }]); + + const subnet = pic.getNnsSubnet(); + + assertNonNullish(subnet); + + const { actor, ...rest } = await pic.setupCanister({ + idlFactory, + wasm: destination, + sender: controller.getPrincipal(), + arg, + targetCanisterId: LEDGER_ID, + targetSubnetId: subnet.id + }); + + actor.setIdentity(controller); + + return { + actor, + ...rest + }; +}; diff --git a/src/tests/utils/setup-tests.utils.ts b/src/tests/utils/setup-tests.utils.ts index f83495feb..197672c3f 100644 --- a/src/tests/utils/setup-tests.utils.ts +++ b/src/tests/utils/setup-tests.utils.ts @@ -101,7 +101,7 @@ const downloadGitHub = async ({ url: `https://github.com/junobuild/juno/releases/download/v${junoVersion}/${wasm}` }); -const download = async ({ wasm, url }: { wasm: string; url: string }): Promise => { +export const download = async ({ wasm, url }: { wasm: string; url: string }): Promise => { const destination = join(process.cwd(), wasm); if (existsSync(destination)) { From 1c76f151f1040fa44aad6b67bbc86e9bc77fdf70 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 12 Oct 2024 09:03:59 +0200 Subject: [PATCH 30/31] chore: lint Signed-off-by: David Dal Busco --- .../src/lib/components/mission-control/MissionControl.svelte | 5 +---- .../components/mission-control/MissionControlWallet.svelte | 4 +++- src/frontend/src/lib/components/ui/Input.svelte | 4 ---- src/tests/utils/ledger-tests.utils.ts | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/frontend/src/lib/components/mission-control/MissionControl.svelte b/src/frontend/src/lib/components/mission-control/MissionControl.svelte index b5bcbc7dc..8216aada7 100644 --- a/src/frontend/src/lib/components/mission-control/MissionControl.svelte +++ b/src/frontend/src/lib/components/mission-control/MissionControl.svelte @@ -20,10 +20,7 @@
{$i18n.mission_control.id} - +
diff --git a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte index 8b3fc9437..c57fc8126 100644 --- a/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte +++ b/src/frontend/src/lib/components/mission-control/MissionControlWallet.svelte @@ -117,7 +117,9 @@ let send = false; $: send = - balance > 0n && compare($versionStore?.missionControl?.current ?? '0.0.0', '0.0.12') > 0; + nonNullish(balance) && + balance > 0n && + compare($versionStore?.missionControl?.current ?? '0.0.0', '0.0.12') > 0; {#if $authSignedInStore} diff --git a/src/frontend/src/lib/components/ui/Input.svelte b/src/frontend/src/lib/components/ui/Input.svelte index 2f64a3ecf..18eb94db9 100644 --- a/src/frontend/src/lib/components/ui/Input.svelte +++ b/src/frontend/src/lib/components/ui/Input.svelte @@ -144,9 +144,6 @@ $: step = inputType === 'number' ? step ?? 'any' : undefined; $: autocomplete = inputType !== 'number' && !currency ? autocomplete ?? 'off' : undefined; - - let displayInnerEnd: boolean; - $: displayInnerEnd = nonNullish($$slots['inner-end']); diff --git a/src/tests/utils/ledger-tests.utils.ts b/src/tests/utils/ledger-tests.utils.ts index b4566f775..9cd9d3e6d 100644 --- a/src/tests/utils/ledger-tests.utils.ts +++ b/src/tests/utils/ledger-tests.utils.ts @@ -3,7 +3,7 @@ import { IDL } from '@dfinity/candid'; import { Ed25519KeyIdentity } from '@dfinity/identity'; import { AccountIdentifier } from '@dfinity/ledger-icp'; import type { _SERVICE as LedgerActor } from '@dfinity/ledger-icp/dist/candid/ledger'; -// @ts-expect-error +// @ts-expect-error init is not packaged / exposed import { idlFactory, init } from '@dfinity/ledger-icp/dist/candid/ledger.idl.js'; import type { CanisterFixture, PocketIc } from '@hadronous/pic'; import { assertNonNullish } from '@junobuild/utils'; From 250bbb7951599e62283e637e1435a055745afa44 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 12 Oct 2024 09:04:18 +0200 Subject: [PATCH 31/31] chore: lint Signed-off-by: David Dal Busco --- src/frontend/src/lib/components/ui/Input.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/lib/components/ui/Input.svelte b/src/frontend/src/lib/components/ui/Input.svelte index 18eb94db9..9ffdedb05 100644 --- a/src/frontend/src/lib/components/ui/Input.svelte +++ b/src/frontend/src/lib/components/ui/Input.svelte @@ -1,6 +1,6 @@