diff --git a/frontend/src/lib/api/canisters.api.ts b/frontend/src/lib/api/canisters.api.ts index 0423e613ad..498a22ab93 100644 --- a/frontend/src/lib/api/canisters.api.ts +++ b/frontend/src/lib/api/canisters.api.ts @@ -21,6 +21,7 @@ import { CYCLES_MINTING_CANISTER_ID } from "$lib/constants/canister-ids.constant import { MAX_CANISTER_NAME_LENGTH } from "$lib/constants/canisters.constants"; import { HOST } from "$lib/constants/environment.constants"; import { ApiErrorKey } from "$lib/types/api.errors"; +import { getCanisterCreationCmcAccountIdentifierHex } from "$lib/utils/canisters.utils"; import { nowInBigIntNanoSeconds } from "$lib/utils/date.utils"; import { logWithTimestamp } from "$lib/utils/dev.utils"; import { poll, pollingLimit } from "$lib/utils/utils"; @@ -232,12 +233,8 @@ export const createCanister = async ({ const { cmc, nnsDapp } = await canisters(identity); const principal = identity.getPrincipal(); - const toSubAccount = principalToSubAccount(principal); - // To create a canister you need to send ICP to an account owned by the CMC, so that the CMC can burn those funds. - // To ensure everyone uses a unique address, the intended controller of the new canister is used to calculate the subaccount. - const recipient = AccountIdentifier.fromPrincipal({ - principal: CYCLES_MINTING_CANISTER_ID, - subAccount: SubAccount.fromBytes(toSubAccount) as SubAccount, + const recipientHex = getCanisterCreationCmcAccountIdentifierHex({ + controller: principal, }); const createdAt = nowInBigIntNanoSeconds(); @@ -245,7 +242,7 @@ export const createCanister = async ({ const blockHeight = await sendICP({ memo: CREATE_CANISTER_MEMO, identity, - to: recipient.toHex(), + to: recipientHex, amount, fromSubAccount, createdAt, diff --git a/frontend/src/lib/utils/canisters.utils.ts b/frontend/src/lib/utils/canisters.utils.ts index 600f9475c4..502b6e52e9 100644 --- a/frontend/src/lib/utils/canisters.utils.ts +++ b/frontend/src/lib/utils/canisters.utils.ts @@ -1,14 +1,16 @@ import type { CanisterDetails } from "$lib/canisters/ic-management/ic-management.canister.types"; import { CanisterStatus } from "$lib/canisters/ic-management/ic-management.canister.types"; import type { CanisterDetails as CanisterInfo } from "$lib/canisters/nns-dapp/nns-dapp.types"; +import { CYCLES_MINTING_CANISTER_ID } from "$lib/constants/canister-ids.constants"; import { MAX_CANISTER_NAME_LENGTH } from "$lib/constants/canisters.constants"; import { ONE_TRILLION } from "$lib/constants/icp.constants"; import type { AuthStoreData } from "$lib/stores/auth.store"; import type { CanistersStore } from "$lib/stores/canisters.store"; import { i18n } from "$lib/stores/i18n"; import type { CanisterId } from "$lib/types/canister"; +import { AccountIdentifier, SubAccount } from "@dfinity/ledger-icp"; import { Principal } from "@dfinity/principal"; -import { nonNullish } from "@dfinity/utils"; +import { nonNullish, principalToSubAccount } from "@dfinity/utils"; import { get } from "svelte/store"; import { formatNumber } from "./format.utils"; import { replacePlaceholders } from "./i18n.utils"; @@ -98,3 +100,18 @@ export const areEnoughCyclesSelected = ({ amountCycles: number | undefined; }): boolean => (amountCycles ?? 0) >= (minimumCycles ?? 0) && (amountCycles ?? 0) > 0; + +export const getCanisterCreationCmcAccountIdentifierHex = ({ + controller, +}: { + controller: Principal; +}): string => { + const subAccountBytes = principalToSubAccount(controller); + // To create a canister you need to send ICP to an account owned by the CMC, so that the CMC can burn those funds and generate cycles. + // To ensure everyone uses a unique address, the intended controller of the new canister is used to calculate the subaccount. + const accountId = AccountIdentifier.fromPrincipal({ + principal: CYCLES_MINTING_CANISTER_ID, + subAccount: SubAccount.fromBytes(subAccountBytes) as SubAccount, + }); + return accountId.toHex(); +}; diff --git a/frontend/src/tests/lib/utils/canisters.utils.spec.ts b/frontend/src/tests/lib/utils/canisters.utils.spec.ts index 8cba98097f..1ec9752106 100644 --- a/frontend/src/tests/lib/utils/canisters.utils.spec.ts +++ b/frontend/src/tests/lib/utils/canisters.utils.spec.ts @@ -5,6 +5,7 @@ import { canisterStatusToText, errorCanisterNameMessage, formatCyclesToTCycles, + getCanisterCreationCmcAccountIdentifierHex, getCanisterFromStore, isController, isUserController, @@ -267,4 +268,25 @@ describe("canister-utils", () => { ).toBeTruthy(); }); }); + + describe("getCanisterCreationCmcAccountIdentifierHex", () => { + it("should return the account identifier", () => { + const controller = Principal.fromText( + "efwjn-odjlf-7q4oi-62p6e-55cgt-opqxz-hwp7t-bp3d3-c2ykh-qrwth-6ae" + ); + // The account identifier is created through hashing. It's not really + // useful to do the same hashing here in the test so we just test a + // hardcoded value. + // This value can be recreated as follows: + // CMC_CANISTER_ID="rkp4c-7iaaa-aaaaa-aaaca-cai" + // CONTROLLER="efwjn-odjlf-7q4oi-62p6e-55cgt-opqxz-hwp7t-bp3d3-c2ykh-qrwth-6ae" + // scripts/convert-id --input text --subaccount_format text --output account_identifier "$CMC_CANISTER_ID" "$CONTROLLER" + const expectedAccountId = + "c13de767ead7f7bfa4522847eab1385532e19ff1e79419c34f3999e1ca9be9a1"; + + expect(getCanisterCreationCmcAccountIdentifierHex({ controller })).toBe( + expectedAccountId + ); + }); + }); });