Skip to content

Commit

Permalink
feat: icrc2 approve support for icp ledger (#639)
Browse files Browse the repository at this point in the history
# Motivation

In the signer standard working group, the common example of usage of the new wallet standards is the ICRC2 approve function. While this function is available in the ledger ICRC library, it has not been exposed so far in the ICP ledger library.

# Changes

- Add `icrc2Approve` function (following `icrc1Transfer` pattern) to the ICP ledger lib.
  • Loading branch information
peterpeterparker authored Jun 3, 2024
1 parent e1b961c commit ceaab97
Show file tree
Hide file tree
Showing 9 changed files with 573 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Canister status response extended with query statistics.
- Add `metadata` function to ledger ICP.
- Add optional parameters to ICP ledger `transactionFee`.
- Add support for `icrc2_approve` on the ICP ledger canister in `@dfinity/ledger-icp`.

# 2024.05.14-0630Z

Expand Down
31 changes: 24 additions & 7 deletions packages/ledger-icp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ const data = await metadata();

### :factory: LedgerCanister

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

#### Methods

Expand All @@ -172,14 +172,15 @@ const data = await metadata();
- [transactionFee](#gear-transactionfee)
- [transfer](#gear-transfer)
- [icrc1Transfer](#gear-icrc1transfer)
- [icrc2Approve](#gear-icrc2approve)

##### :gear: create

| Method | Type |
| -------- | ----------------------------------------------------- |
| `create` | `(options?: LedgerCanisterOptions) => LedgerCanister` |

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L25)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L32)

##### :gear: accountBalance

Expand All @@ -198,7 +199,7 @@ Parameters:
- `params.accountIdentifier`: The account identifier provided either as hex string or as an AccountIdentifier.
- `params.certified`: query or update call.

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L53)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L60)

##### :gear: metadata

Expand All @@ -212,7 +213,7 @@ Parameters:

- `params`: - The parameters used to fetch the metadata, notably query or certified call.

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L72)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L79)

##### :gear: transactionFee

Expand All @@ -226,7 +227,7 @@ Parameters:

- `params`: - Optional query parameters for the request, defaulting to `{ certified: false }` for backwards compatibility reason.

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L83)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L90)

##### :gear: transfer

Expand All @@ -237,7 +238,7 @@ Returns the index of the block containing the tx if it was successful.
| ---------- | ----------------------------------------------- |
| `transfer` | `(request: TransferRequest) => Promise<bigint>` |

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L101)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L108)

##### :gear: icrc1Transfer

Expand All @@ -248,7 +249,23 @@ Returns the index of the block containing the tx if it was successful.
| --------------- | ---------------------------------------------------- |
| `icrc1Transfer` | `(request: Icrc1TransferRequest) => Promise<bigint>` |

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L121)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L128)

##### :gear: icrc2Approve

This method entitles the `spender` to transfer token `amount` on behalf of the caller from account `{ owner = caller; subaccount = from_subaccount }`.

Reference: https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md#icrc2_approve

| Method | Type |
| -------------- | -------------------------------------------------- |
| `icrc2Approve` | `(params: Icrc2ApproveRequest) => Promise<bigint>` |

Parameters:

- `params`: - The parameters to approve.

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/ledger.canister.ts#L148)

### :factory: IndexCanister

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { arrayOfNumberToUint8Array, toNullable } from "@dfinity/utils";
import type {
TransferArg as Icrc1TransferRawRequest,
ApproveArgs as Icrc2ApproveRawRequest,
Tokens,
TransferArgs as TransferRawRequest,
} from "../../../candid/ledger";
import { TRANSACTION_FEE } from "../../constants/constants";
import type {
Icrc1TransferRequest,
Icrc2ApproveRequest,
TransferRequest,
} from "../../types/ledger_converters";

Expand Down Expand Up @@ -53,3 +55,23 @@ export const toIcrc1TransferRawRequest = ({
created_at_time: toNullable(createdAt),
from_subaccount: toNullable(fromSubAccount),
});

export const toIcrc2ApproveRawRequest = ({
fee,
createdAt,
icrc1Memo,
fromSubAccount,
expected_allowance,
expires_at,
amount,
...rest
}: Icrc2ApproveRequest): Icrc2ApproveRawRequest => ({
...rest,
fee: toNullable(fee ?? TRANSACTION_FEE),
memo: toNullable(icrc1Memo),
from_subaccount: toNullable(fromSubAccount),
created_at_time: toNullable(createdAt),
amount,
expected_allowance: toNullable(expected_allowance),
expires_at: toNullable(expires_at),
});
99 changes: 97 additions & 2 deletions packages/ledger-icp/src/errors/ledger.errors.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import type {
Icrc1BlockIndex,
Icrc1Tokens,
ApproveError as RawApproveError,
Icrc1TransferError as RawIcrc1TransferError,
TransferError as RawTransferError,
} from "../../candid/ledger";
import type { BlockHeight } from "../types/common";

export class TransferError extends Error {}
export class IcrcError extends Error {}

export class TransferError extends IcrcError {}
export class ApproveError extends IcrcError {}

export class InvalidSenderError extends TransferError {}

Expand All @@ -30,12 +36,44 @@ export class TxDuplicateError extends TransferError {
}
}

export class BadFeeError extends TransferError {
export class BadFeeError extends IcrcError {
constructor(public readonly expectedFee: bigint) {
super();
}
}

export class GenericError extends ApproveError {
constructor(
public readonly message: string,
public readonly error_code: bigint,
) {
super();
}
}

export class TemporarilyUnavailableError extends ApproveError {}

export class DuplicateError extends ApproveError {
constructor(public readonly duplicateOf: Icrc1BlockIndex) {
super();
}
}

export class AllowanceChangedError extends ApproveError {
constructor(public readonly currentAllowance: Icrc1Tokens) {
super();
}
}

export class CreatedInFutureError extends ApproveError {}
export class TooOldError extends ApproveError {}

export class ExpiredError extends ApproveError {
constructor(public readonly ledgerTime: bigint) {
super();
}
}

export const mapTransferError = (
rawTransferError: RawTransferError,
): TransferError => {
Expand Down Expand Up @@ -89,3 +127,60 @@ export const mapIcrc1TransferError = (
`Unknown error type ${JSON.stringify(rawTransferError)}`,
);
};

export const mapIcrc2ApproveError = (
rawApproveError: RawApproveError,
): ApproveError => {
/**
* export type ApproveError =
* | { InsufficientFunds: { balance: Icrc1Tokens } };
*/

if ("GenericError" in rawApproveError) {
return new GenericError(
rawApproveError.GenericError.message,
rawApproveError.GenericError.error_code,
);
}

if ("TemporarilyUnavailable" in rawApproveError) {
return new TemporarilyUnavailableError();
}

if ("Duplicate" in rawApproveError) {
return new DuplicateError(rawApproveError.Duplicate.duplicate_of);
}

if ("BadFee" in rawApproveError) {
return new BadFeeError(rawApproveError.BadFee.expected_fee);
}

if ("AllowanceChanged" in rawApproveError) {
return new AllowanceChangedError(
rawApproveError.AllowanceChanged.current_allowance,
);
}

if ("CreatedInFuture" in rawApproveError) {
return new CreatedInFutureError();
}

if ("TooOld" in rawApproveError) {
return new TooOldError();
}

if ("Expired" in rawApproveError) {
return new ExpiredError(rawApproveError.Expired.ledger_time);
}

if ("InsufficientFunds" in rawApproveError) {
return new InsufficientFundsError(
rawApproveError.InsufficientFunds.balance,
);
}

// Edge case
return new ApproveError(
`Unknown error type ${JSON.stringify(rawApproveError)}`,
);
};
7 changes: 6 additions & 1 deletion packages/ledger-icp/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export type * from "../candid/index";
export type { Value } from "../candid/ledger";
export type {
Icrc1BlockIndex,
Icrc1Timestamp,
Icrc1Tokens,
Value,
} from "../candid/ledger";
export { AccountIdentifier, SubAccount } from "./account_identifier";
export * from "./errors/ledger.errors";
export { IndexCanister } from "./index.canister";
Expand Down
Loading

0 comments on commit ceaab97

Please sign in to comment.