Skip to content

Commit

Permalink
feat: update ckETH minter withdrawals with subaccounts (#751)
Browse files Browse the repository at this point in the history
# Motivation

The ckETH minter withdrawl functions have been extended with optional
subaccounts.

# Changes

- Update DID files as generated by #750 (did were stripped from the PR
to create this PR).
- Add subaccount options to ckEth minter withdrawl features.

---------

Signed-off-by: David Dal Busco <[email protected]>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
peterpeterparker and github-actions[bot] authored Nov 19, 2024
1 parent aed8e3b commit 2353883
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 20 deletions.
32 changes: 17 additions & 15 deletions packages/cketh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const address = await getSmartContractAddress({});

### :factory: CkETHMinterCanister

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L23)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L29)

#### Methods

Expand All @@ -71,7 +71,7 @@ const address = await getSmartContractAddress({});
| -------- | ------------------------------------------------------------------------ |
| `create` | `(options: CkETHMinterCanisterOptions<_SERVICE>) => CkETHMinterCanister` |

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L24)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L30)

##### :gear: getSmartContractAddress

Expand All @@ -86,7 +86,7 @@ Parameters:
- `params`: The parameters to resolve the ckETH smart contract address.
- `params.certified`: query or update call

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L42)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L48)

##### :gear: withdrawEth

Expand All @@ -97,17 +97,18 @@ Preconditions:
The caller allowed the minter's principal to spend its funds using
[icrc2_approve] on the ckETH ledger.

| Method | Type |
| ------------- | --------------------------------------------------------------------------------------------- |
| `withdrawEth` | `({ address, ...rest }: { address: string; amount: bigint; }) => Promise<RetrieveEthRequest>` |
| Method | Type |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withdrawEth` | `({ address, fromSubaccount, ...rest }: { address: string; amount: bigint; fromSubaccount?: Subaccount or undefined; }) => Promise<RetrieveEthRequest>` |

Parameters:

- `params`: The parameters to withdrawal ckETH to ETH.
- `params.address`: The destination ETH address.
- `params.amount`: The ETH amount in wei.
- `params.fromSubaccount`: The optional subaccount to burn ckETH from.

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L62)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L69)

##### :gear: withdrawErc20

Expand All @@ -118,18 +119,19 @@ Preconditions:
The caller allowed the minter's principal to spend its funds using
[icrc2_approve] on the ckErc20 ledger and to burn some of the user’s ckETH tokens to pay for the transaction fees on the CkETH ledger.

| Method | Type |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `withdrawErc20` | `({ address, ledgerCanisterId, ...rest }: { address: string; amount: bigint; ledgerCanisterId: Principal; }) => Promise<RetrieveErc20Request>` |
| Method | Type |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withdrawErc20` | `({ address, ledgerCanisterId, fromCkEthSubaccount, fromCkErc20Subaccount, ...rest }: { address: string; amount: bigint; ledgerCanisterId: Principal; fromCkEthSubaccount?: Subaccount or undefined; fromCkErc20Subaccount?: Subaccount or undefined; }) => Promise<...>` |

Parameters:

- `params`: The parameters to withdrawal ckErc20 to Erc20.
- `params.address`: The destination ETH address.
- `params.amount`: The ETH amount in wei.
- `params.ledgerCanisterId`: The ledger canister ID of the ckErc20.
- `params.fromCkEthSubaccount`: The optional subaccount to burn ckETH from to pay for the transaction fee.
- `params.fromCkEthSubaccount`: The optional subaccount to burn ckERC20 from.

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L99)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L110)

##### :gear: eip1559TransactionPrice

Expand All @@ -145,7 +147,7 @@ Parameters:
- `params.ckErc20LedgerId`: - The optional identifier for a particular ckERC20 ledger.
- `params.certified`: - Indicates whether this is a certified query or an update call.

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L134)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L151)

##### :gear: retrieveEthStatus

Expand All @@ -155,7 +157,7 @@ Retrieve the status of a withdrawal request.
| ------------------- | ---------------------------------------------------- |
| `retrieveEthStatus` | `(blockIndex: bigint) => Promise<RetrieveEthStatus>` |

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L149)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L166)

##### :gear: getMinterInfo

Expand All @@ -170,7 +172,7 @@ Parameters:
- `params`: The parameters to get the minter info.
- `params.certified`: query or update call

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L162)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/cketh/src/minter.canister.ts#L179)

### :factory: CkETHOrchestratorCanister

Expand Down
4 changes: 4 additions & 0 deletions packages/cketh/candid/minter.certified.idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export const idlFactory = ({ IDL }) => {
)
),
'minter_address' : IDL.Opt(IDL.Text),
'last_deposit_with_subaccount_scraped_block_number' : IDL.Opt(IDL.Nat),
'ethereum_block_height' : IDL.Opt(BlockTag),
});
const EthTransaction = IDL.Record({ 'transaction_hash' : IDL.Text });
Expand All @@ -289,6 +290,8 @@ export const idlFactory = ({ IDL }) => {
const WithdrawErc20Arg = IDL.Record({
'ckerc20_ledger_id' : IDL.Principal,
'recipient' : IDL.Text,
'from_cketh_subaccount' : IDL.Opt(Subaccount),
'from_ckerc20_subaccount' : IDL.Opt(Subaccount),
'amount' : IDL.Nat,
});
const RetrieveErc20Request = IDL.Record({
Expand Down Expand Up @@ -330,6 +333,7 @@ export const idlFactory = ({ IDL }) => {
});
const WithdrawalArg = IDL.Record({
'recipient' : IDL.Text,
'from_subaccount' : IDL.Opt(Subaccount),
'amount' : IDL.Nat,
});
const RetrieveEthRequest = IDL.Record({ 'block_index' : IDL.Nat });
Expand Down
4 changes: 4 additions & 0 deletions packages/cketh/candid/minter.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export interface MinterInfo {
| []
| [Array<{ balance: bigint; erc20_contract_address: string }>];
minter_address: [] | [string];
last_deposit_with_subaccount_scraped_block_number: [] | [bigint];
ethereum_block_height: [] | [BlockTag];
}
export interface QueryStats {
Expand Down Expand Up @@ -339,6 +340,8 @@ export interface UpgradeArg {
export interface WithdrawErc20Arg {
ckerc20_ledger_id: Principal;
recipient: string;
from_cketh_subaccount: [] | [Subaccount];
from_ckerc20_subaccount: [] | [Subaccount];
amount: bigint;
}
export type WithdrawErc20Error =
Expand All @@ -356,6 +359,7 @@ export type WithdrawErc20Error =
| { RecipientAddressBlocked: { address: string } };
export interface WithdrawalArg {
recipient: string;
from_subaccount: [] | [Subaccount];
amount: bigint;
}
export interface WithdrawalDetail {
Expand Down
22 changes: 20 additions & 2 deletions packages/cketh/candid/minter.did
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Generated from IC repo commit c47e172 (2024-10-25 tags: release-2024-11-07_03-07-6.11-kernel) 'rs/ethereum/cketh/minter/cketh_minter.did' by import-candid
// Generated from IC repo commit cb3cb61 (2024-11-14 tags: release-2024-11-14_03-07-base) 'rs/ethereum/cketh/minter/cketh_minter.did' by import-candid
type EthereumNetwork = variant {
// The public Ethereum mainnet.
Mainnet;
Expand Down Expand Up @@ -202,6 +202,9 @@ type MinterInfo = record {
// Last scraped block number for logs of the ERC20 helper contract.
last_erc20_scraped_block_number: opt nat;

// Last scraped block number for logs of the deposit with subaccount helper contract.
last_deposit_with_subaccount_scraped_block_number: opt nat;

// Canister ID of the ckETH ledger.
cketh_ledger_id: opt principal;

Expand Down Expand Up @@ -263,7 +266,16 @@ type RetrieveEthStatus = variant {
TxFinalized : TxFinalizedStatus;
};

type WithdrawalArg = record { recipient : text; amount : nat };
type WithdrawalArg = record {
// The address to which the minter should deposit ETH.
recipient : text;

// The amount of ckETH in Wei that the client wants to withdraw.
amount : nat;

// The subaccount to burn ckETH from.
from_subaccount : opt Subaccount;
};

// Details of a withdrawal request and its status.
type WithdrawalDetail = record {
Expand Down Expand Up @@ -351,6 +363,12 @@ type WithdrawErc20Arg = record {

// Ethereum address to withdraw to.
recipient : text;

// The subaccount to burn ckETH from to pay for the transaction fee.
from_cketh_subaccount : opt Subaccount;

// The subaccount to burn ckERC20 from.
from_ckerc20_subaccount : opt Subaccount;
};

type RetrieveErc20Request = record {
Expand Down
4 changes: 4 additions & 0 deletions packages/cketh/candid/minter.idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export const idlFactory = ({ IDL }) => {
)
),
'minter_address' : IDL.Opt(IDL.Text),
'last_deposit_with_subaccount_scraped_block_number' : IDL.Opt(IDL.Nat),
'ethereum_block_height' : IDL.Opt(BlockTag),
});
const EthTransaction = IDL.Record({ 'transaction_hash' : IDL.Text });
Expand All @@ -289,6 +290,8 @@ export const idlFactory = ({ IDL }) => {
const WithdrawErc20Arg = IDL.Record({
'ckerc20_ledger_id' : IDL.Principal,
'recipient' : IDL.Text,
'from_cketh_subaccount' : IDL.Opt(Subaccount),
'from_ckerc20_subaccount' : IDL.Opt(Subaccount),
'amount' : IDL.Nat,
});
const RetrieveErc20Request = IDL.Record({
Expand Down Expand Up @@ -330,6 +333,7 @@ export const idlFactory = ({ IDL }) => {
});
const WithdrawalArg = IDL.Record({
'recipient' : IDL.Text,
'from_subaccount' : IDL.Opt(Subaccount),
'amount' : IDL.Nat,
});
const RetrieveEthRequest = IDL.Record({ 'block_index' : IDL.Nat });
Expand Down
127 changes: 126 additions & 1 deletion packages/cketh/src/minter.canister.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ActorSubclass } from "@dfinity/agent";
import { Principal } from "@dfinity/principal";
import { toNullable } from "@dfinity/utils";
import { arrayOfNumberToUint8Array, toNullable } from "@dfinity/utils";
import { mock } from "jest-mock-extended";
import {
_SERVICE as CkETHMinterService,
Expand Down Expand Up @@ -100,12 +100,59 @@ describe("ckETH minter canister", () => {
const { address, ...rest } = params;
expect(service.withdraw_eth).toBeCalledWith({
recipient: address,
from_subaccount: toNullable(),
...rest,
});

expect(res).toEqual(success);
});

it("should call with subaccount numbers", async () => {
const service = mock<ActorSubclass<CkETHMinterService>>();
service.withdraw_eth.mockResolvedValue(ok);

const canister = minter(service);

const fromSubaccount = [1, 2, 3];

await canister.withdrawEth({
...params,
fromSubaccount,
});

expect(service.withdraw_eth).toBeCalledTimes(1);

const { address, ...rest } = params;
expect(service.withdraw_eth).toBeCalledWith({
recipient: address,
from_subaccount: toNullable(fromSubaccount),
...rest,
});
});

it("should call with subaccount uintarray", async () => {
const service = mock<ActorSubclass<CkETHMinterService>>();
service.withdraw_eth.mockResolvedValue(ok);

const canister = minter(service);

const fromSubaccount = arrayOfNumberToUint8Array([1, 2, 3]);

await canister.withdrawEth({
...params,
fromSubaccount,
});

expect(service.withdraw_eth).toBeCalledTimes(1);

const { address, ...rest } = params;
expect(service.withdraw_eth).toHaveBeenCalledWith({
recipient: address,
from_subaccount: toNullable(fromSubaccount),
...rest,
});
});

it("should throw MinterTemporarilyUnavailable", async () => {
const service = mock<ActorSubclass<CkETHMinterService>>();

Expand Down Expand Up @@ -236,12 +283,89 @@ describe("ckETH minter canister", () => {
expect(service.withdraw_erc20).toBeCalledWith({
recipient: address,
ckerc20_ledger_id: ledgerCanisterIdMock,
from_cketh_subaccount: toNullable(),
from_ckerc20_subaccount: toNullable(),
...rest,
});

expect(res).toEqual(success);
});

describe.each([[4, 5, 6], arrayOfNumberToUint8Array([7, 8, 9])])(
"should call with expected subaccount",
(account) => {
it("should call with ckEth subaccount", async () => {
const service = mock<ActorSubclass<CkETHMinterService>>();
service.withdraw_erc20.mockResolvedValue(ok);

const canister = minter(service);

await canister.withdrawErc20({
...params,
fromCkEthSubaccount: account,
});

expect(service.withdraw_erc20).toBeCalledTimes(1);

const { address, ledgerCanisterId, ...rest } = params;
expect(service.withdraw_erc20).toHaveBeenCalledWith({
recipient: address,
ckerc20_ledger_id: ledgerCanisterIdMock,
from_cketh_subaccount: toNullable(account),
from_ckerc20_subaccount: toNullable(),
...rest,
});
});

it("should call with ckErc20 subaccount", async () => {
const service = mock<ActorSubclass<CkETHMinterService>>();
service.withdraw_erc20.mockResolvedValue(ok);

const canister = minter(service);

await canister.withdrawErc20({
...params,
fromCkErc20Subaccount: account,
});

expect(service.withdraw_erc20).toBeCalledTimes(1);

const { address, ledgerCanisterId, ...rest } = params;
expect(service.withdraw_erc20).toHaveBeenCalledWith({
recipient: address,
ckerc20_ledger_id: ledgerCanisterIdMock,
from_cketh_subaccount: toNullable(),
from_ckerc20_subaccount: toNullable(account),
...rest,
});
});

it("should call with ckEth and ckErc20 subaccount", async () => {
const service = mock<ActorSubclass<CkETHMinterService>>();
service.withdraw_erc20.mockResolvedValue(ok);

const canister = minter(service);

await canister.withdrawErc20({
...params,
fromCkEthSubaccount: account,
fromCkErc20Subaccount: account,
});

expect(service.withdraw_erc20).toBeCalledTimes(1);

const { address, ledgerCanisterId, ...rest } = params;
expect(service.withdraw_erc20).toHaveBeenCalledWith({
recipient: address,
ckerc20_ledger_id: ledgerCanisterIdMock,
from_cketh_subaccount: toNullable(account),
from_ckerc20_subaccount: toNullable(account),
...rest,
});
});
},
);

it("should throw MinterTemporarilyUnavailable", async () => {
const service = mock<ActorSubclass<CkETHMinterService>>();

Expand Down Expand Up @@ -744,6 +868,7 @@ describe("ckETH minter canister", () => {
evm_rpc_id: toNullable(
Principal.fromText("7hfb6-caaaa-aaaar-qadga-cai"),
),
last_deposit_with_subaccount_scraped_block_number: [],
};

const service = mock<ActorSubclass<CkETHMinterService>>();
Expand Down
Loading

0 comments on commit 2353883

Please sign in to comment.