From cec0dc35454c24f24957c4190332b7719d5dd65b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 21 May 2024 09:51:29 +0200 Subject: [PATCH] feat: ckEth orchestrator (#632) # Motivation Add the ckEth orchestrator ([source](https://github.com/dfinity/ic/tree/master/rs/ethereum/ledger-suite-orchestrator)) to `@dfinity/ledger-cketh` and expose function `get_orchestrator_info`. This additional canister is responsible for spinning up Ledger and Index canisters for ckERC20 tokens when (and if) the respective proposals are approved and executed (for example, proposal [129750](https://nns.ic0.app/proposal/?u=qoctq-giaaa-aaaaa-aaaea-cai&proposal=129750)). One might want to use this canister to fetch at runtime the list of ledgers and indexes or the list of supported ckERC20 tokens, but most probably, as in Oisy, one might use this feature to set up a job that periodically checks automatically if new tokens were approved and deployed. # Changes - set-up did and idl generation for `orchestrator.did` - init and expose `CkETHOrchestratorCanister` (no particular options) - implement `get_orchestrator_info` (nothing particular, readonly function) - add canister to `docs.js` to generate its documentation --- packages/cketh/README.md | 40 ++++ .../candid/orchestrator.certified.idl.d.ts | 2 + .../candid/orchestrator.certified.idl.js | 159 +++++++++++++++ packages/cketh/candid/orchestrator.d.ts | 96 +++++++++ packages/cketh/candid/orchestrator.did | 188 ++++++++++++++++++ packages/cketh/candid/orchestrator.idl.d.ts | 2 + packages/cketh/candid/orchestrator.idl.js | 159 +++++++++++++++ packages/cketh/src/index.ts | 8 + .../cketh/src/orchestrator.canister.spec.ts | 84 ++++++++ packages/cketh/src/orchestrator.canister.ts | 46 +++++ packages/cketh/src/types/canister.options.ts | 2 + scripts/docs.js | 1 + scripts/import-candid | 1 + 13 files changed, 788 insertions(+) create mode 100644 packages/cketh/candid/orchestrator.certified.idl.d.ts create mode 100644 packages/cketh/candid/orchestrator.certified.idl.js create mode 100644 packages/cketh/candid/orchestrator.d.ts create mode 100644 packages/cketh/candid/orchestrator.did create mode 100644 packages/cketh/candid/orchestrator.idl.d.ts create mode 100644 packages/cketh/candid/orchestrator.idl.js create mode 100644 packages/cketh/src/orchestrator.canister.spec.ts create mode 100644 packages/cketh/src/orchestrator.canister.ts diff --git a/packages/cketh/README.md b/packages/cketh/README.md index c72b03d27..1b5af5800 100644 --- a/packages/cketh/README.md +++ b/packages/cketh/README.md @@ -171,6 +171,46 @@ Parameters: [:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L157) +### :factory: CkETHOrchestratorCanister + +Class representing the CkETH Orchestrator Canister, which manages the Ledger and Index canisters of ckERC20 tokens. + +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/orchestrator.canister.ts#L15) + +#### Methods + +- [create](#gear-create) +- [getOrchestratorInfo](#gear-getorchestratorinfo) + +##### :gear: create + +Creates an instance of CkETHOrchestratorCanister. + +| Method | Type | +| -------- | ------------------------------------------------------------------------------------ | +| `create` | `(options: CkETHOrchestratorCanisterOptions<_SERVICE>) => CkETHOrchestratorCanister` | + +Parameters: + +- `options`: - Options for creating the canister. + +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/orchestrator.canister.ts#L21) + +##### :gear: getOrchestratorInfo + +Retrieves orchestrator information, which contains the list of existing ckERC20 Ledger and Index canisters. + +| Method | Type | +| --------------------- | ------------------------------------------------------------- | +| `getOrchestratorInfo` | `({ certified, }?: QueryParams) => Promise` | + +Parameters: + +- `params`: - The query parameters. +- `params.certified`: - Whether to execute a certified (update) call. + +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/orchestrator.canister.ts#L40) + ## Resources diff --git a/packages/cketh/candid/orchestrator.certified.idl.d.ts b/packages/cketh/candid/orchestrator.certified.idl.d.ts new file mode 100644 index 000000000..8e1474b8d --- /dev/null +++ b/packages/cketh/candid/orchestrator.certified.idl.d.ts @@ -0,0 +1,2 @@ +import type { IDL } from "@dfinity/candid"; +export const idlFactory: IDL.InterfaceFactory; diff --git a/packages/cketh/candid/orchestrator.certified.idl.js b/packages/cketh/candid/orchestrator.certified.idl.js new file mode 100644 index 000000000..e6c738d33 --- /dev/null +++ b/packages/cketh/candid/orchestrator.certified.idl.js @@ -0,0 +1,159 @@ +/* Do not edit. Compiled with ./scripts/compile-idl-js from packages/cketh/candid/orchestrator.did */ +export const idlFactory = ({ IDL }) => { + const UpdateCyclesManagement = IDL.Record({ + 'cycles_top_up_increment' : IDL.Opt(IDL.Nat), + 'cycles_for_ledger_creation' : IDL.Opt(IDL.Nat), + 'cycles_for_archive_creation' : IDL.Opt(IDL.Nat), + 'cycles_for_index_creation' : IDL.Opt(IDL.Nat), + }); + const UpgradeArg = IDL.Record({ + 'cycles_management' : IDL.Opt(UpdateCyclesManagement), + 'archive_compressed_wasm_hash' : IDL.Opt(IDL.Text), + 'git_commit_hash' : IDL.Opt(IDL.Text), + 'ledger_compressed_wasm_hash' : IDL.Opt(IDL.Text), + 'index_compressed_wasm_hash' : IDL.Opt(IDL.Text), + }); + const CyclesManagement = IDL.Record({ + 'cycles_top_up_increment' : IDL.Nat, + 'cycles_for_ledger_creation' : IDL.Nat, + 'cycles_for_archive_creation' : IDL.Nat, + 'cycles_for_index_creation' : IDL.Nat, + }); + const InitArg = IDL.Record({ + 'cycles_management' : IDL.Opt(CyclesManagement), + 'more_controller_ids' : IDL.Vec(IDL.Principal), + 'minter_id' : IDL.Opt(IDL.Principal), + }); + const Erc20Contract = IDL.Record({ + 'chain_id' : IDL.Nat, + 'address' : IDL.Text, + }); + const LedgerSubaccount = IDL.Vec(IDL.Nat8); + const LedgerAccount = IDL.Record({ + 'owner' : IDL.Principal, + 'subaccount' : IDL.Opt(LedgerSubaccount), + }); + const LedgerFeatureFlags = IDL.Record({ 'icrc2' : IDL.Bool }); + const LedgerInitArg = IDL.Record({ + 'decimals' : IDL.Opt(IDL.Nat8), + 'token_symbol' : IDL.Text, + 'transfer_fee' : IDL.Nat, + 'minting_account' : LedgerAccount, + 'initial_balances' : IDL.Vec(IDL.Tuple(LedgerAccount, IDL.Nat)), + 'maximum_number_of_accounts' : IDL.Opt(IDL.Nat64), + 'accounts_overflow_trim_quantity' : IDL.Opt(IDL.Nat64), + 'fee_collector_account' : IDL.Opt(LedgerAccount), + 'max_memo_length' : IDL.Opt(IDL.Nat16), + 'token_logo' : IDL.Text, + 'token_name' : IDL.Text, + 'feature_flags' : IDL.Opt(LedgerFeatureFlags), + }); + const AddErc20Arg = IDL.Record({ + 'contract' : Erc20Contract, + 'ledger_init_arg' : LedgerInitArg, + 'git_commit_hash' : IDL.Text, + 'ledger_compressed_wasm_hash' : IDL.Text, + 'index_compressed_wasm_hash' : IDL.Text, + }); + const OrchestratorArg = IDL.Variant({ + 'UpgradeArg' : UpgradeArg, + 'InitArg' : InitArg, + 'AddErc20Arg' : AddErc20Arg, + }); + const ManagedCanisterIds = IDL.Record({ + 'ledger' : IDL.Opt(IDL.Principal), + 'index' : IDL.Opt(IDL.Principal), + 'archives' : IDL.Vec(IDL.Principal), + }); + const ManagedCanisterStatus = IDL.Variant({ + 'Created' : IDL.Record({ 'canister_id' : IDL.Principal }), + 'Installed' : IDL.Record({ + 'canister_id' : IDL.Principal, + 'installed_wasm_hash' : IDL.Text, + }), + }); + const ManagedCanisters = IDL.Record({ + 'erc20_contract' : Erc20Contract, + 'ledger' : IDL.Opt(ManagedCanisterStatus), + 'index' : IDL.Opt(ManagedCanisterStatus), + 'archives' : IDL.Vec(IDL.Principal), + 'ckerc20_token_symbol' : IDL.Text, + }); + const OrchestratorInfo = IDL.Record({ + 'cycles_management' : CyclesManagement, + 'managed_canisters' : IDL.Vec(ManagedCanisters), + 'more_controller_ids' : IDL.Vec(IDL.Principal), + 'minter_id' : IDL.Opt(IDL.Principal), + }); + return IDL.Service({ + 'canister_ids' : IDL.Func( + [Erc20Contract], + [IDL.Opt(ManagedCanisterIds)], + [], + ), + 'get_orchestrator_info' : IDL.Func([], [OrchestratorInfo], []), + }); +}; +export const init = ({ IDL }) => { + const UpdateCyclesManagement = IDL.Record({ + 'cycles_top_up_increment' : IDL.Opt(IDL.Nat), + 'cycles_for_ledger_creation' : IDL.Opt(IDL.Nat), + 'cycles_for_archive_creation' : IDL.Opt(IDL.Nat), + 'cycles_for_index_creation' : IDL.Opt(IDL.Nat), + }); + const UpgradeArg = IDL.Record({ + 'cycles_management' : IDL.Opt(UpdateCyclesManagement), + 'archive_compressed_wasm_hash' : IDL.Opt(IDL.Text), + 'git_commit_hash' : IDL.Opt(IDL.Text), + 'ledger_compressed_wasm_hash' : IDL.Opt(IDL.Text), + 'index_compressed_wasm_hash' : IDL.Opt(IDL.Text), + }); + const CyclesManagement = IDL.Record({ + 'cycles_top_up_increment' : IDL.Nat, + 'cycles_for_ledger_creation' : IDL.Nat, + 'cycles_for_archive_creation' : IDL.Nat, + 'cycles_for_index_creation' : IDL.Nat, + }); + const InitArg = IDL.Record({ + 'cycles_management' : IDL.Opt(CyclesManagement), + 'more_controller_ids' : IDL.Vec(IDL.Principal), + 'minter_id' : IDL.Opt(IDL.Principal), + }); + const Erc20Contract = IDL.Record({ + 'chain_id' : IDL.Nat, + 'address' : IDL.Text, + }); + const LedgerSubaccount = IDL.Vec(IDL.Nat8); + const LedgerAccount = IDL.Record({ + 'owner' : IDL.Principal, + 'subaccount' : IDL.Opt(LedgerSubaccount), + }); + const LedgerFeatureFlags = IDL.Record({ 'icrc2' : IDL.Bool }); + const LedgerInitArg = IDL.Record({ + 'decimals' : IDL.Opt(IDL.Nat8), + 'token_symbol' : IDL.Text, + 'transfer_fee' : IDL.Nat, + 'minting_account' : LedgerAccount, + 'initial_balances' : IDL.Vec(IDL.Tuple(LedgerAccount, IDL.Nat)), + 'maximum_number_of_accounts' : IDL.Opt(IDL.Nat64), + 'accounts_overflow_trim_quantity' : IDL.Opt(IDL.Nat64), + 'fee_collector_account' : IDL.Opt(LedgerAccount), + 'max_memo_length' : IDL.Opt(IDL.Nat16), + 'token_logo' : IDL.Text, + 'token_name' : IDL.Text, + 'feature_flags' : IDL.Opt(LedgerFeatureFlags), + }); + const AddErc20Arg = IDL.Record({ + 'contract' : Erc20Contract, + 'ledger_init_arg' : LedgerInitArg, + 'git_commit_hash' : IDL.Text, + 'ledger_compressed_wasm_hash' : IDL.Text, + 'index_compressed_wasm_hash' : IDL.Text, + }); + const OrchestratorArg = IDL.Variant({ + 'UpgradeArg' : UpgradeArg, + 'InitArg' : InitArg, + 'AddErc20Arg' : AddErc20Arg, + }); + return [OrchestratorArg]; +}; diff --git a/packages/cketh/candid/orchestrator.d.ts b/packages/cketh/candid/orchestrator.d.ts new file mode 100644 index 000000000..ce82a608d --- /dev/null +++ b/packages/cketh/candid/orchestrator.d.ts @@ -0,0 +1,96 @@ +import type { ActorMethod } from "@dfinity/agent"; +import type { IDL } from "@dfinity/candid"; +import type { Principal } from "@dfinity/principal"; + +export interface AddErc20Arg { + contract: Erc20Contract; + ledger_init_arg: LedgerInitArg; + git_commit_hash: string; + ledger_compressed_wasm_hash: string; + index_compressed_wasm_hash: string; +} +export interface CyclesManagement { + cycles_top_up_increment: bigint; + cycles_for_ledger_creation: bigint; + cycles_for_archive_creation: bigint; + cycles_for_index_creation: bigint; +} +export interface Erc20Contract { + chain_id: bigint; + address: string; +} +export interface InitArg { + cycles_management: [] | [CyclesManagement]; + more_controller_ids: Array; + minter_id: [] | [Principal]; +} +export interface LedgerAccount { + owner: Principal; + subaccount: [] | [LedgerSubaccount]; +} +export interface LedgerFeatureFlags { + icrc2: boolean; +} +export interface LedgerInitArg { + decimals: [] | [number]; + token_symbol: string; + transfer_fee: bigint; + minting_account: LedgerAccount; + initial_balances: Array<[LedgerAccount, bigint]>; + maximum_number_of_accounts: [] | [bigint]; + accounts_overflow_trim_quantity: [] | [bigint]; + fee_collector_account: [] | [LedgerAccount]; + max_memo_length: [] | [number]; + token_logo: string; + token_name: string; + feature_flags: [] | [LedgerFeatureFlags]; +} +export type LedgerSubaccount = Uint8Array | number[]; +export interface ManagedCanisterIds { + ledger: [] | [Principal]; + index: [] | [Principal]; + archives: Array; +} +export type ManagedCanisterStatus = + | { + Created: { canister_id: Principal }; + } + | { + Installed: { canister_id: Principal; installed_wasm_hash: string }; + }; +export interface ManagedCanisters { + erc20_contract: Erc20Contract; + ledger: [] | [ManagedCanisterStatus]; + index: [] | [ManagedCanisterStatus]; + archives: Array; + ckerc20_token_symbol: string; +} +export type OrchestratorArg = + | { UpgradeArg: UpgradeArg } + | { InitArg: InitArg } + | { AddErc20Arg: AddErc20Arg }; +export interface OrchestratorInfo { + cycles_management: CyclesManagement; + managed_canisters: Array; + more_controller_ids: Array; + minter_id: [] | [Principal]; +} +export interface UpdateCyclesManagement { + cycles_top_up_increment: [] | [bigint]; + cycles_for_ledger_creation: [] | [bigint]; + cycles_for_archive_creation: [] | [bigint]; + cycles_for_index_creation: [] | [bigint]; +} +export interface UpgradeArg { + cycles_management: [] | [UpdateCyclesManagement]; + archive_compressed_wasm_hash: [] | [string]; + git_commit_hash: [] | [string]; + ledger_compressed_wasm_hash: [] | [string]; + index_compressed_wasm_hash: [] | [string]; +} +export interface _SERVICE { + canister_ids: ActorMethod<[Erc20Contract], [] | [ManagedCanisterIds]>; + get_orchestrator_info: ActorMethod<[], OrchestratorInfo>; +} +export declare const idlFactory: IDL.InterfaceFactory; +export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[]; diff --git a/packages/cketh/candid/orchestrator.did b/packages/cketh/candid/orchestrator.did new file mode 100644 index 000000000..57c0c5bcc --- /dev/null +++ b/packages/cketh/candid/orchestrator.did @@ -0,0 +1,188 @@ +// Generated from IC repo commit 8776fd1c1c (2024-05-20) 'rs/ethereum/ledger-suite-orchestrator/ledger_suite_orchestrator.did' by import-candid +type OrchestratorArg = variant { + UpgradeArg : UpgradeArg; + InitArg : InitArg; + AddErc20Arg : AddErc20Arg; +}; + +type InitArg = record { + // All canisters that will be spawned off by the orchestrator will be controlled by the orchestrator + // and *additionally* by the following controllers. + more_controller_ids : vec principal; + + // Canister ID of the minter that will be notified when new ERC-20 tokens are added. + minter_id: opt principal; + + // Controls the cycles management of the canisters managed by the orchestrator. + cycles_management: opt CyclesManagement; +}; + +type UpgradeArg = record { + // Hexadecimal encoding of the SHA-1 git commit hash used for this upgrade, e.g., + // "51d01d3936498d4010de54505d6433e9ad5cc62b", corresponding to a git revision in the + // [IC repository](https://github.com/dfinity/ic). + // This field is expected to be present, if any of the wasm hashes below is present. + git_commit_hash: opt text; + + // Hexadecimal encoding of the SHA2-256 ledger compressed wasm hash, e.g., + // "3148f7a9f1b0ee39262c8abe3b08813480cf78551eee5a60ab1cf38433b5d9b0". + // This exact version will be used for upgrading all existing ledger canisters managed by the orchestrator. + // Leaving this field empty will not upgrade the ledger canisters. + ledger_compressed_wasm_hash: opt text; + + // Hexadecimal encoding of the SHA2-256 index compressed wasm hash, e.g., + // "3a6d39b5e94cdef5203bca62720e75a28cd071ff434d22b9746403ac7ae59614". + // This exact version will be used for upgrading all existing index canisters managed by the orchestrator. + // Leaving this field empty will not upgrade the index canisters. + index_compressed_wasm_hash: opt text; + + // Hexadecimal encoding of the SHA2-256 archive compressed wasm hash, e.g., + // "b24812976b2cc64f12faf813cf592631f3062bfda837334f77ab807361d64e82". + // This exact version will be used for upgrading all existing archive canisters managed by the orchestrator. + // Leaving this field empty will not upgrade the archive canisters. + archive_compressed_wasm_hash: opt text; + + // Update the cycles management of the canisters managed by the orchestrator. + cycles_management: opt UpdateCyclesManagement; +}; + +type AddErc20Arg = record { + contract: Erc20Contract; + ledger_init_arg: LedgerInitArg; + + // Hexadecimal encoding of the SHA-1 git commit hash used for this upgrade, e.g., + // "51d01d3936498d4010de54505d6433e9ad5cc62b", corresponding to a git revision in the + // [IC repository](https://github.com/dfinity/ic). + git_commit_hash: text; + + // Hexadecimal encoding of the SHA2-256 ledger compressed wasm hash, e.g., + // "3148f7a9f1b0ee39262c8abe3b08813480cf78551eee5a60ab1cf38433b5d9b0". + // This exact version will be used for the new ledger canister created for this ERC-20 token. + ledger_compressed_wasm_hash: text; + + // Hexadecimal encoding of the SHA2-256 index compressed wasm hash, e.g., + // "3a6d39b5e94cdef5203bca62720e75a28cd071ff434d22b9746403ac7ae59614". + // This exact version will be used for the new index canister created for this ERC-20 token. + index_compressed_wasm_hash: text; +}; + +type Erc20Contract = record { + chain_id: nat; + address: text; +}; + +// ICRC1 ledger initialization argument that will be used when the orchestrator spawns a new ledger canister. +// The `archive_options` field will be set by the orchestrator. +type LedgerInitArg = record { + minting_account : LedgerAccount; + fee_collector_account : opt LedgerAccount; + transfer_fee : nat; + decimals : opt nat8; + max_memo_length : opt nat16; + token_symbol : text; + token_name : text; + token_logo : text; + initial_balances : vec record { LedgerAccount; nat }; + feature_flags : opt LedgerFeatureFlags; + maximum_number_of_accounts : opt nat64; + accounts_overflow_trim_quantity : opt nat64; +}; + +type LedgerAccount = record { + owner : principal; + subaccount : opt LedgerSubaccount; +}; + +type LedgerSubaccount = blob; + +type LedgerFeatureFlags = record { + icrc2 : bool; +}; + +type ManagedCanisterIds = record { + ledger: opt principal; + index: opt principal; + archives: vec principal; +}; + +type CyclesManagement = record { + //Number of cycles when creating a new ICRC1 ledger canister. + cycles_for_ledger_creation: nat; + + //Number of cycles when creating a new ICRC1 archive canister. + cycles_for_archive_creation: nat; + + //Number of cycles when creating a new ICRC1 index canister. + cycles_for_index_creation: nat; + + //Number of cycles to add to a canister managed by the orchestrator whose cycles balance is running low. + cycles_top_up_increment: nat; +}; + +type ManagedCanisterStatus = variant { + // Canister created with the given principal but wasm module is not yet installed. + Created : record { canister_id : principal }; + + // Canister created and wasm module installed. + // The wasm_hash reflects the installed wasm module by the orchestrator + // but *may differ* from the one being currently deployed (if another controller did an upgrade) + Installed : record { canister_id : principal; installed_wasm_hash : text }; +}; + +type ManagedCanisters = record { + // Corresponding ERC20 contract + erc20_contract: Erc20Contract; + + // ckERC20 Token symbol + ckerc20_token_symbol : text; + + // Status of the ledger canister + ledger : opt ManagedCanisterStatus; + + // Status of the index canister + index : opt ManagedCanisterStatus; + + // List of archive canister ids + archives : vec principal; +}; + +type OrchestratorInfo = record { + // List of managed canisters data for each ERC20 contract. + managed_canisters : vec ManagedCanisters; + + // Cycle management parameters. + cycles_management : CyclesManagement; + + // Additional controllers that new canisters will be spawned with. + more_controller_ids : vec principal; + + // ckETH minter canister id. + minter_id : opt principal; +}; + +type UpdateCyclesManagement = record { + // Change the number of cycles when creating a new ICRC1 ledger canister. + // Previously created canisters are not affected. + cycles_for_ledger_creation: opt nat; + + // Change the number of cycles when creating a new ICRC1 archive canister. + // Previously created canisters are not affected. + cycles_for_archive_creation: opt nat; + + // Change the number of cycles when creating a new ICRC1 index canister. + // Previously created canisters are not affected. + cycles_for_index_creation: opt nat; + + // Change the number of cycles to add to a canister managed by the orchestrator whose cycles balance is running low. + cycles_top_up_increment: opt nat; +}; + +service : (OrchestratorArg) -> { + + // Managed canister IDs for a given ERC20 contract + canister_ids : (Erc20Contract) -> (opt ManagedCanisterIds) query; + + // Return internal orchestrator parameters + get_orchestrator_info : () -> (OrchestratorInfo) query; + +} diff --git a/packages/cketh/candid/orchestrator.idl.d.ts b/packages/cketh/candid/orchestrator.idl.d.ts new file mode 100644 index 000000000..8e1474b8d --- /dev/null +++ b/packages/cketh/candid/orchestrator.idl.d.ts @@ -0,0 +1,2 @@ +import type { IDL } from "@dfinity/candid"; +export const idlFactory: IDL.InterfaceFactory; diff --git a/packages/cketh/candid/orchestrator.idl.js b/packages/cketh/candid/orchestrator.idl.js new file mode 100644 index 000000000..cf21176ae --- /dev/null +++ b/packages/cketh/candid/orchestrator.idl.js @@ -0,0 +1,159 @@ +/* Do not edit. Compiled with ./scripts/compile-idl-js from packages/cketh/candid/orchestrator.did */ +export const idlFactory = ({ IDL }) => { + const UpdateCyclesManagement = IDL.Record({ + 'cycles_top_up_increment' : IDL.Opt(IDL.Nat), + 'cycles_for_ledger_creation' : IDL.Opt(IDL.Nat), + 'cycles_for_archive_creation' : IDL.Opt(IDL.Nat), + 'cycles_for_index_creation' : IDL.Opt(IDL.Nat), + }); + const UpgradeArg = IDL.Record({ + 'cycles_management' : IDL.Opt(UpdateCyclesManagement), + 'archive_compressed_wasm_hash' : IDL.Opt(IDL.Text), + 'git_commit_hash' : IDL.Opt(IDL.Text), + 'ledger_compressed_wasm_hash' : IDL.Opt(IDL.Text), + 'index_compressed_wasm_hash' : IDL.Opt(IDL.Text), + }); + const CyclesManagement = IDL.Record({ + 'cycles_top_up_increment' : IDL.Nat, + 'cycles_for_ledger_creation' : IDL.Nat, + 'cycles_for_archive_creation' : IDL.Nat, + 'cycles_for_index_creation' : IDL.Nat, + }); + const InitArg = IDL.Record({ + 'cycles_management' : IDL.Opt(CyclesManagement), + 'more_controller_ids' : IDL.Vec(IDL.Principal), + 'minter_id' : IDL.Opt(IDL.Principal), + }); + const Erc20Contract = IDL.Record({ + 'chain_id' : IDL.Nat, + 'address' : IDL.Text, + }); + const LedgerSubaccount = IDL.Vec(IDL.Nat8); + const LedgerAccount = IDL.Record({ + 'owner' : IDL.Principal, + 'subaccount' : IDL.Opt(LedgerSubaccount), + }); + const LedgerFeatureFlags = IDL.Record({ 'icrc2' : IDL.Bool }); + const LedgerInitArg = IDL.Record({ + 'decimals' : IDL.Opt(IDL.Nat8), + 'token_symbol' : IDL.Text, + 'transfer_fee' : IDL.Nat, + 'minting_account' : LedgerAccount, + 'initial_balances' : IDL.Vec(IDL.Tuple(LedgerAccount, IDL.Nat)), + 'maximum_number_of_accounts' : IDL.Opt(IDL.Nat64), + 'accounts_overflow_trim_quantity' : IDL.Opt(IDL.Nat64), + 'fee_collector_account' : IDL.Opt(LedgerAccount), + 'max_memo_length' : IDL.Opt(IDL.Nat16), + 'token_logo' : IDL.Text, + 'token_name' : IDL.Text, + 'feature_flags' : IDL.Opt(LedgerFeatureFlags), + }); + const AddErc20Arg = IDL.Record({ + 'contract' : Erc20Contract, + 'ledger_init_arg' : LedgerInitArg, + 'git_commit_hash' : IDL.Text, + 'ledger_compressed_wasm_hash' : IDL.Text, + 'index_compressed_wasm_hash' : IDL.Text, + }); + const OrchestratorArg = IDL.Variant({ + 'UpgradeArg' : UpgradeArg, + 'InitArg' : InitArg, + 'AddErc20Arg' : AddErc20Arg, + }); + const ManagedCanisterIds = IDL.Record({ + 'ledger' : IDL.Opt(IDL.Principal), + 'index' : IDL.Opt(IDL.Principal), + 'archives' : IDL.Vec(IDL.Principal), + }); + const ManagedCanisterStatus = IDL.Variant({ + 'Created' : IDL.Record({ 'canister_id' : IDL.Principal }), + 'Installed' : IDL.Record({ + 'canister_id' : IDL.Principal, + 'installed_wasm_hash' : IDL.Text, + }), + }); + const ManagedCanisters = IDL.Record({ + 'erc20_contract' : Erc20Contract, + 'ledger' : IDL.Opt(ManagedCanisterStatus), + 'index' : IDL.Opt(ManagedCanisterStatus), + 'archives' : IDL.Vec(IDL.Principal), + 'ckerc20_token_symbol' : IDL.Text, + }); + const OrchestratorInfo = IDL.Record({ + 'cycles_management' : CyclesManagement, + 'managed_canisters' : IDL.Vec(ManagedCanisters), + 'more_controller_ids' : IDL.Vec(IDL.Principal), + 'minter_id' : IDL.Opt(IDL.Principal), + }); + return IDL.Service({ + 'canister_ids' : IDL.Func( + [Erc20Contract], + [IDL.Opt(ManagedCanisterIds)], + ['query'], + ), + 'get_orchestrator_info' : IDL.Func([], [OrchestratorInfo], ['query']), + }); +}; +export const init = ({ IDL }) => { + const UpdateCyclesManagement = IDL.Record({ + 'cycles_top_up_increment' : IDL.Opt(IDL.Nat), + 'cycles_for_ledger_creation' : IDL.Opt(IDL.Nat), + 'cycles_for_archive_creation' : IDL.Opt(IDL.Nat), + 'cycles_for_index_creation' : IDL.Opt(IDL.Nat), + }); + const UpgradeArg = IDL.Record({ + 'cycles_management' : IDL.Opt(UpdateCyclesManagement), + 'archive_compressed_wasm_hash' : IDL.Opt(IDL.Text), + 'git_commit_hash' : IDL.Opt(IDL.Text), + 'ledger_compressed_wasm_hash' : IDL.Opt(IDL.Text), + 'index_compressed_wasm_hash' : IDL.Opt(IDL.Text), + }); + const CyclesManagement = IDL.Record({ + 'cycles_top_up_increment' : IDL.Nat, + 'cycles_for_ledger_creation' : IDL.Nat, + 'cycles_for_archive_creation' : IDL.Nat, + 'cycles_for_index_creation' : IDL.Nat, + }); + const InitArg = IDL.Record({ + 'cycles_management' : IDL.Opt(CyclesManagement), + 'more_controller_ids' : IDL.Vec(IDL.Principal), + 'minter_id' : IDL.Opt(IDL.Principal), + }); + const Erc20Contract = IDL.Record({ + 'chain_id' : IDL.Nat, + 'address' : IDL.Text, + }); + const LedgerSubaccount = IDL.Vec(IDL.Nat8); + const LedgerAccount = IDL.Record({ + 'owner' : IDL.Principal, + 'subaccount' : IDL.Opt(LedgerSubaccount), + }); + const LedgerFeatureFlags = IDL.Record({ 'icrc2' : IDL.Bool }); + const LedgerInitArg = IDL.Record({ + 'decimals' : IDL.Opt(IDL.Nat8), + 'token_symbol' : IDL.Text, + 'transfer_fee' : IDL.Nat, + 'minting_account' : LedgerAccount, + 'initial_balances' : IDL.Vec(IDL.Tuple(LedgerAccount, IDL.Nat)), + 'maximum_number_of_accounts' : IDL.Opt(IDL.Nat64), + 'accounts_overflow_trim_quantity' : IDL.Opt(IDL.Nat64), + 'fee_collector_account' : IDL.Opt(LedgerAccount), + 'max_memo_length' : IDL.Opt(IDL.Nat16), + 'token_logo' : IDL.Text, + 'token_name' : IDL.Text, + 'feature_flags' : IDL.Opt(LedgerFeatureFlags), + }); + const AddErc20Arg = IDL.Record({ + 'contract' : Erc20Contract, + 'ledger_init_arg' : LedgerInitArg, + 'git_commit_hash' : IDL.Text, + 'ledger_compressed_wasm_hash' : IDL.Text, + 'index_compressed_wasm_hash' : IDL.Text, + }); + const OrchestratorArg = IDL.Variant({ + 'UpgradeArg' : UpgradeArg, + 'InitArg' : InitArg, + 'AddErc20Arg' : AddErc20Arg, + }); + return [OrchestratorArg]; +}; diff --git a/packages/cketh/src/index.ts b/packages/cketh/src/index.ts index 4b5c94536..017778aae 100644 --- a/packages/cketh/src/index.ts +++ b/packages/cketh/src/index.ts @@ -6,6 +6,14 @@ export type { RetrieveEthStatus, TxFinalizedStatus, } from "../candid/minter"; +export type { + CyclesManagement, + Erc20Contract, + ManagedCanisterStatus, + ManagedCanisters, + OrchestratorInfo, +} from "../candid/orchestrator"; export * from "./errors/minter.errors"; export { CkETHMinterCanister } from "./minter.canister"; +export { CkETHOrchestratorCanister } from "./orchestrator.canister"; export * from "./utils/minter.utils"; diff --git a/packages/cketh/src/orchestrator.canister.spec.ts b/packages/cketh/src/orchestrator.canister.spec.ts new file mode 100644 index 000000000..51e27a758 --- /dev/null +++ b/packages/cketh/src/orchestrator.canister.spec.ts @@ -0,0 +1,84 @@ +import { ActorSubclass } from "@dfinity/agent"; +import { Principal } from "@dfinity/principal"; +import { mock } from "jest-mock-extended"; +import { + _SERVICE as CkETHOrchestratorService, + ManagedCanisters, + OrchestratorInfo, +} from "../candid/orchestrator"; +import { minterCanisterIdMock } from "./mocks/minter.mock"; +import { CkETHOrchestratorCanister } from "./orchestrator.canister"; + +describe("ckETH orchestrator canister", () => { + const orchestrator = ( + service: ActorSubclass, + ): CkETHOrchestratorCanister => + CkETHOrchestratorCanister.create({ + // ckSepoliaETH Orchestrator Canister ID on mainnet + canisterId: Principal.from("2s5qh-7aaaa-aaaar-qadya-cai"), + certifiedServiceOverride: service, + }); + + describe("Get orchestrator info", () => { + it("should return the info", async () => { + const ckSepoliaUSDCInfoMock: ManagedCanisters = { + ledger: [ + { + Installed: { + canister_id: Principal.from("yfumr-cyaaa-aaaar-qaela-cai"), + installed_wasm_hash: + "57e2a728f9ffcb1a7d9e101dbd1260f8b9f3246bf5aa2ad3e2c750e125446838", + }, + }, + ], + index: [ + { + Installed: { + canister_id: Principal.from("ycvkf-paaaa-aaaar-qaelq-cai"), + installed_wasm_hash: + "6fb62c7e9358ca5c937a5d25f55700459ed09a293d0826c09c631b64ba756594", + }, + }, + ], + archives: [], + ckerc20_token_symbol: "ckSepoliaUSDC", + erc20_contract: { + chain_id: 11_155_111n, + address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + }, + }; + + const orchestratorInfoMock: OrchestratorInfo = { + minter_id: [minterCanisterIdMock], + more_controller_ids: [], + cycles_management: { + cycles_for_archive_creation: 1_000_000_000_000n, + cycles_for_index_creation: 1_000_000_000_000n, + cycles_top_up_increment: 500_000_000_000n, + cycles_for_ledger_creation: 2_000_000_000_000n, + }, + managed_canisters: [ckSepoliaUSDCInfoMock], + }; + + const service = mock>(); + service.get_orchestrator_info.mockResolvedValue(orchestratorInfoMock); + + const canister = orchestrator(service); + + const res = await canister.getOrchestratorInfo(); + expect(service.get_orchestrator_info).toHaveBeenCalled(); + expect(res).toEqual(orchestratorInfoMock); + }); + + it("should bubble errors", () => { + const service = mock>(); + service.get_orchestrator_info.mockImplementation(() => { + throw new Error(); + }); + + const canister = orchestrator(service); + + expect(() => canister.getOrchestratorInfo()).toThrow(); + }); + }); +}); diff --git a/packages/cketh/src/orchestrator.canister.ts b/packages/cketh/src/orchestrator.canister.ts new file mode 100644 index 000000000..4a4966a6a --- /dev/null +++ b/packages/cketh/src/orchestrator.canister.ts @@ -0,0 +1,46 @@ +import { Canister, createServices, type QueryParams } from "@dfinity/utils"; +import type { + _SERVICE as CkETHOrchestratorService, + OrchestratorInfo, +} from "../candid/orchestrator"; +import { idlFactory as certifiedIdlFactory } from "../candid/orchestrator.certified.idl"; +import { idlFactory } from "../candid/orchestrator.idl"; +import type { CkETHOrchestratorCanisterOptions } from "./types/canister.options"; + +/** + * Class representing the CkETH Orchestrator Canister, which manages the Ledger and Index canisters of ckERC20 tokens. + * @extends {Canister} + * @see {@link https://github.com/dfinity/ic/tree/master/rs/ethereum/ledger-suite-orchestrator|Source Code} + */ +export class CkETHOrchestratorCanister extends Canister { + /** + * Creates an instance of CkETHOrchestratorCanister. + * @param {CkETHOrchestratorCanisterOptions} options - Options for creating the canister. + * @returns {CkETHOrchestratorCanister} A new instance of CkETHOrchestratorCanister. + */ + static create( + options: CkETHOrchestratorCanisterOptions, + ): CkETHOrchestratorCanister { + const { service, certifiedService, canisterId } = + createServices({ + options, + idlFactory, + certifiedIdlFactory, + }); + + return new CkETHOrchestratorCanister(canisterId, service, certifiedService); + } + + /** + * Retrieves orchestrator information, which contains the list of existing ckERC20 Ledger and Index canisters. + * @param {QueryParams} [params={}] - The query parameters. + * @param {boolean} [params.certified] - Whether to execute a certified (update) call. + * @returns {Promise} A promise that resolves to the orchestrator information. + */ + getOrchestratorInfo = ({ + certified, + }: QueryParams = {}): Promise => { + const { get_orchestrator_info } = this.caller({ certified }); + return get_orchestrator_info(); + }; +} diff --git a/packages/cketh/src/types/canister.options.ts b/packages/cketh/src/types/canister.options.ts index f6cdb2101..f2675333a 100644 --- a/packages/cketh/src/types/canister.options.ts +++ b/packages/cketh/src/types/canister.options.ts @@ -6,3 +6,5 @@ export interface CkETHMinterCanisterOptions // The canister's ID is mandatory to instantiate an ckETH minter. canisterId: Principal; } + +export type CkETHOrchestratorCanisterOptions = CkETHMinterCanisterOptions; diff --git a/scripts/docs.js b/scripts/docs.js index 897e8488d..683f862c2 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -44,6 +44,7 @@ const ckBTCInputFiles = [ const ckETHInputFiles = [ "./packages/cketh/src/minter.canister.ts", + "./packages/cketh/src/orchestrator.canister.ts", "./packages/ledger-icrc/src/utils/minter.utils.ts", ]; diff --git a/scripts/import-candid b/scripts/import-candid index 796be5157..f18c15910 100755 --- a/scripts/import-candid +++ b/scripts/import-candid @@ -85,6 +85,7 @@ import_did "rs/bitcoin/ckbtc/minter/ckbtc_minter.did" "minter.did" "ckbtc" mkdir -p packages/cketh/candid import_did "rs/ethereum/cketh/minter/cketh_minter.did" "minter.did" "cketh" +import_did "rs/ethereum/ledger-suite-orchestrator/ledger_suite_orchestrator.did" "orchestrator.did" "cketh" mkdir -p packages/ic-management/candid curl https://raw.githubusercontent.com/dfinity/interface-spec/master/spec/_attachments/ic.did -o packages/ic-management/candid/ic-management.did