Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update ckETH minter withdrawals with subaccounts #751

Merged
merged 6 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
117 changes: 116 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,55 @@ 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,
});

const { address, ...rest } = params;
expect(service.withdraw_eth).toHaveBeenNthCalledWith(1, {
peterpeterparker marked this conversation as resolved.
Show resolved Hide resolved
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,
});

const { address, ...rest } = params;
expect(service.withdraw_eth).toHaveBeenNthCalledWith(1, {
peterpeterparker marked this conversation as resolved.
Show resolved Hide resolved
recipient: address,
from_subaccount: toNullable(fromSubaccount),
...rest,
});
});

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

Expand Down Expand Up @@ -236,12 +279,83 @@ 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,
});

const { address, ledgerCanisterId, ...rest } = params;
expect(service.withdraw_erc20).toHaveBeenNthCalledWith(1, {
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,
});

const { address, ledgerCanisterId, ...rest } = params;
expect(service.withdraw_erc20).toHaveBeenNthCalledWith(1, {
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,
});

const { address, ledgerCanisterId, ...rest } = params;
expect(service.withdraw_erc20).toHaveBeenNthCalledWith(1, {
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 +858,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