diff --git a/CHANGELOG.md b/CHANGELOG.md index c69ff055c..4be2aec14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/ledger-icp/README.md b/packages/ledger-icp/README.md index 98bf80b87..117def2d3 100644 --- a/packages/ledger-icp/README.md +++ b/packages/ledger-icp/README.md @@ -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 @@ -172,6 +172,7 @@ const data = await metadata(); - [transactionFee](#gear-transactionfee) - [transfer](#gear-transfer) - [icrc1Transfer](#gear-icrc1transfer) +- [icrc2Approve](#gear-icrc2approve) ##### :gear: create @@ -179,7 +180,7 @@ const data = await metadata(); | -------- | ----------------------------------------------------- | | `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 @@ -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 @@ -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 @@ -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 @@ -237,7 +238,7 @@ Returns the index of the block containing the tx if it was successful. | ---------- | ----------------------------------------------- | | `transfer` | `(request: TransferRequest) => Promise` | -[: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 @@ -248,7 +249,23 @@ Returns the index of the block containing the tx if it was successful. | --------------- | ---------------------------------------------------- | | `icrc1Transfer` | `(request: Icrc1TransferRequest) => Promise` | -[: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` | + +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 diff --git a/packages/ledger-icp/src/canisters/ledger/ledger.request.converts.ts b/packages/ledger-icp/src/canisters/ledger/ledger.request.converts.ts index cf007c8c6..7ab49d8b8 100644 --- a/packages/ledger-icp/src/canisters/ledger/ledger.request.converts.ts +++ b/packages/ledger-icp/src/canisters/ledger/ledger.request.converts.ts @@ -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"; @@ -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), +}); diff --git a/packages/ledger-icp/src/errors/ledger.errors.ts b/packages/ledger-icp/src/errors/ledger.errors.ts index 1ff8d8a36..7ef679e4e 100644 --- a/packages/ledger-icp/src/errors/ledger.errors.ts +++ b/packages/ledger-icp/src/errors/ledger.errors.ts @@ -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 {} @@ -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 => { @@ -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)}`, + ); +}; diff --git a/packages/ledger-icp/src/index.ts b/packages/ledger-icp/src/index.ts index 11b24ea96..e8f8c769e 100644 --- a/packages/ledger-icp/src/index.ts +++ b/packages/ledger-icp/src/index.ts @@ -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"; diff --git a/packages/ledger-icp/src/ledger.canister.spec.ts b/packages/ledger-icp/src/ledger.canister.spec.ts index aaefd0611..82168de49 100644 --- a/packages/ledger-icp/src/ledger.canister.spec.ts +++ b/packages/ledger-icp/src/ledger.canister.spec.ts @@ -2,17 +2,30 @@ import { ActorSubclass } from "@dfinity/agent"; import { Principal } from "@dfinity/principal"; import { arrayOfNumberToUint8Array } from "@dfinity/utils"; import { mock } from "jest-mock-extended"; -import { _SERVICE as LedgerService, Value } from "../candid/ledger"; +import { + _SERVICE as LedgerService, + Value, + type Account, + type ApproveArgs as Icrc2ApproveRawRequest, +} from "../candid/ledger"; import { TRANSACTION_FEE } from "./constants/constants"; import { + AllowanceChangedError, BadFeeError, + CreatedInFutureError, + DuplicateError, + ExpiredError, + GenericError, InsufficientFundsError, + TemporarilyUnavailableError, + TooOldError, TxCreatedInFutureError, TxDuplicateError, TxTooOldError, } from "./errors/ledger.errors"; import { LedgerCanister } from "./ledger.canister"; -import { mockAccountIdentifier } from "./mocks/ledger.mock"; +import { mockAccountIdentifier, mockPrincipal } from "./mocks/ledger.mock"; +import type { Icrc2ApproveRequest } from "./types/ledger_converters"; describe("LedgerCanister", () => { describe("accountBalance", () => { @@ -683,4 +696,357 @@ describe("LedgerCanister", () => { }); }); }); + + describe("icrc2Approve", () => { + const approveRequest: Icrc2ApproveRequest = { + spender: { + owner: mockPrincipal, + subaccount: [], + }, + amount: BigInt(100_000_000), + expires_at: 123n, + }; + + const approveRawRequest: Icrc2ApproveRawRequest = { + expected_allowance: [], + expires_at: [123n], + from_subaccount: [], + spender: { + owner: mockPrincipal, + subaccount: [], + }, + fee: [], + memo: [], + created_at_time: [], + amount: BigInt(100_000_000), + }; + + it("should return the block height successfully", async () => { + const service = mock>(); + const blockHeight = BigInt(100); + service.icrc2_approve.mockResolvedValue({ Ok: blockHeight }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + }); + + const res = await ledger.icrc2Approve(approveRequest); + expect(res).toEqual(blockHeight); + expect(service.icrc2_approve).toBeCalledWith({ + ...approveRawRequest, + fee: [TRANSACTION_FEE], + }); + }); + + it("should call approve with default fee", async () => { + const service = mock>(); + const blockHeight = BigInt(100); + service.icrc2_approve.mockResolvedValue({ Ok: blockHeight }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + }); + + const res = await ledger.icrc2Approve(approveRequest); + expect(res).toEqual(blockHeight); + expect(service.icrc2_approve).toBeCalledWith({ + ...approveRawRequest, + fee: [TRANSACTION_FEE], + }); + }); + + it("should call approve with custom fee", async () => { + const service = mock>(); + const blockHeight = BigInt(100); + service.icrc2_approve.mockResolvedValue({ Ok: blockHeight }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + }); + + const res = await ledger.icrc2Approve({ + ...approveRequest, + fee: 123n, + }); + expect(res).toEqual(blockHeight); + expect(service.icrc2_approve).toBeCalledWith({ + ...approveRawRequest, + fee: [123n], + }); + }); + + it("should call approve with memo", async () => { + const service = mock>(); + const blockHeight = BigInt(100); + service.icrc2_approve.mockResolvedValue({ Ok: blockHeight }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + }); + + const icrc1Memo = arrayOfNumberToUint8Array([1, 2, 3, 4, 5]); + + const res = await ledger.icrc2Approve({ + ...approveRequest, + icrc1Memo, + }); + expect(res).toEqual(blockHeight); + expect(service.icrc2_approve).toBeCalledWith({ + ...approveRawRequest, + fee: [TRANSACTION_FEE], + memo: [icrc1Memo], + }); + }); + + it("should call approve with created at", async () => { + const service = mock>(); + const blockHeight = BigInt(100); + service.icrc2_approve.mockResolvedValue({ Ok: blockHeight }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + }); + + const res = await ledger.icrc2Approve({ + ...approveRequest, + createdAt: 456n, + }); + expect(res).toEqual(blockHeight); + expect(service.icrc2_approve).toBeCalledWith({ + ...approveRawRequest, + fee: [TRANSACTION_FEE], + created_at_time: [456n], + }); + }); + + it("should call approve with expected allowance", async () => { + const service = mock>(); + const blockHeight = BigInt(100); + service.icrc2_approve.mockResolvedValue({ Ok: blockHeight }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + }); + + const res = await ledger.icrc2Approve({ + ...approveRequest, + expected_allowance: 999n, + }); + expect(res).toEqual(blockHeight); + expect(service.icrc2_approve).toBeCalledWith({ + ...approveRawRequest, + fee: [TRANSACTION_FEE], + expected_allowance: [999n], + }); + }); + + it("should call approve with a from subaccount", async () => { + const service = mock>(); + const blockHeight = BigInt(100); + service.icrc2_approve.mockResolvedValue({ Ok: blockHeight }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + }); + + const res = await ledger.icrc2Approve({ + ...approveRequest, + fromSubAccount: arrayOfNumberToUint8Array([4, 3, 2, 1]), + }); + expect(res).toEqual(blockHeight); + expect(service.icrc2_approve).toBeCalledWith({ + ...approveRawRequest, + fee: [TRANSACTION_FEE], + from_subaccount: [arrayOfNumberToUint8Array([4, 3, 2, 1])], + }); + }); + + it("should call approve with a spender with subaccount", async () => { + const service = mock>(); + const blockHeight = BigInt(100); + service.icrc2_approve.mockResolvedValue({ Ok: blockHeight }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + }); + + const spender: Account = { + owner: mockPrincipal, + subaccount: [arrayOfNumberToUint8Array([9, 8, 7, 6])], + }; + + const res = await ledger.icrc2Approve({ + ...approveRequest, + spender, + }); + expect(res).toEqual(blockHeight); + expect(service.icrc2_approve).toBeCalledWith({ + ...approveRawRequest, + fee: [TRANSACTION_FEE], + spender, + }); + }); + + it("should raise GenericError", async () => { + const service = mock>(); + service.icrc2_approve.mockResolvedValue({ + Err: { + GenericError: { message: "This is a test", error_code: 123n }, + }, + }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + serviceOverride: service, + }); + + const call = async () => await ledger.icrc2Approve(approveRequest); + + expect(call).rejects.toThrowError(GenericError); + }); + + it("should raise TemporarilyUnavailableError", async () => { + const service = mock>(); + service.icrc2_approve.mockResolvedValue({ + Err: { + TemporarilyUnavailable: null, + }, + }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + serviceOverride: service, + }); + + const call = async () => await ledger.icrc2Approve(approveRequest); + + expect(call).rejects.toThrowError(TemporarilyUnavailableError); + }); + + it("should raise DuplicateError", async () => { + const service = mock>(); + service.icrc2_approve.mockResolvedValue({ + Err: { + Duplicate: { duplicate_of: 888n }, + }, + }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + serviceOverride: service, + }); + + const call = async () => await ledger.icrc2Approve(approveRequest); + + expect(call).rejects.toThrowError(DuplicateError); + }); + + it("should raise BadFeeError", async () => { + const service = mock>(); + service.icrc2_approve.mockResolvedValue({ + Err: { + BadFee: { expected_fee: 666n }, + }, + }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + serviceOverride: service, + }); + + const call = async () => await ledger.icrc2Approve(approveRequest); + + expect(call).rejects.toThrowError(BadFeeError); + }); + + it("should raise AllowanceChangedError", async () => { + const service = mock>(); + service.icrc2_approve.mockResolvedValue({ + Err: { + AllowanceChanged: { current_allowance: 444n }, + }, + }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + serviceOverride: service, + }); + + const call = async () => await ledger.icrc2Approve(approveRequest); + + expect(call).rejects.toThrowError(AllowanceChangedError); + }); + + it("should raise CreatedInFutureError", async () => { + const service = mock>(); + service.icrc2_approve.mockResolvedValue({ + Err: { + CreatedInFuture: { ledger_time: BigInt(1234) }, + }, + }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + serviceOverride: service, + }); + + const call = async () => await ledger.icrc2Approve(approveRequest); + + expect(call).rejects.toThrowError(CreatedInFutureError); + }); + + it("should raise TooOldError", async () => { + const service = mock>(); + service.icrc2_approve.mockResolvedValue({ + Err: { + TooOld: null, + }, + }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + serviceOverride: service, + }); + + const call = async () => await ledger.icrc2Approve(approveRequest); + + expect(call).rejects.toThrowError(TooOldError); + }); + + it("should raise ExpiredError", async () => { + const service = mock>(); + service.icrc2_approve.mockResolvedValue({ + Err: { + Expired: { ledger_time: BigInt(1234) }, + }, + }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + serviceOverride: service, + }); + + const call = async () => await ledger.icrc2Approve(approveRequest); + + expect(call).rejects.toThrowError(ExpiredError); + }); + + it("should raise InsufficientFundsError", async () => { + const service = mock>(); + service.icrc2_approve.mockResolvedValue({ + Err: { + InsufficientFunds: { balance: 333888n }, + }, + }); + + const ledger = LedgerCanister.create({ + certifiedServiceOverride: service, + serviceOverride: service, + }); + + const call = async () => await ledger.icrc2Approve(approveRequest); + + expect(call).rejects.toThrowError(InsufficientFundsError); + }); + }); }); diff --git a/packages/ledger-icp/src/ledger.canister.ts b/packages/ledger-icp/src/ledger.canister.ts index ef74e9193..bc412677d 100644 --- a/packages/ledger-icp/src/ledger.canister.ts +++ b/packages/ledger-icp/src/ledger.canister.ts @@ -1,15 +1,21 @@ import type { Principal } from "@dfinity/principal"; import { Canister, createServices, type QueryParams } from "@dfinity/utils"; -import type { _SERVICE as LedgerService, Value } from "../candid/ledger"; +import type { + Icrc1BlockIndex, + _SERVICE as LedgerService, + Value, +} from "../candid/ledger"; import { idlFactory as certifiedIdlFactory } from "../candid/ledger.certified.idl"; import { idlFactory } from "../candid/ledger.idl"; import { toIcrc1TransferRawRequest, + toIcrc2ApproveRawRequest, toTransferRawRequest, } from "./canisters/ledger/ledger.request.converts"; import { MAINNET_LEDGER_CANISTER_ID } from "./constants/canister_ids"; import { mapIcrc1TransferError, + mapIcrc2ApproveError, mapTransferError, } from "./errors/ledger.errors"; import type { BlockHeight } from "./types/common"; @@ -17,6 +23,7 @@ import type { LedgerCanisterOptions } from "./types/ledger.options"; import type { AccountBalanceParams } from "./types/ledger.params"; import type { Icrc1TransferRequest, + Icrc2ApproveRequest, TransferRequest, } from "./types/ledger_converters"; import { paramToAccountIdentifier } from "./utils/params.utils"; @@ -128,4 +135,27 @@ export class LedgerCanister extends Canister { } return response.Ok; }; + + /** + * 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 + * + * @param {Icrc2ApproveRequest} params - The parameters to approve. + * @throws {ApproveError} If the approval fails. + * @returns {Promise} The block index of the approved transaction. + */ + icrc2Approve = async ( + params: Icrc2ApproveRequest, + ): Promise => { + const { icrc2_approve } = this.caller({ certified: true }); + + const response = await icrc2_approve(toIcrc2ApproveRawRequest(params)); + + if ("Err" in response) { + throw mapIcrc2ApproveError(response.Err); + } + + return response.Ok; + }; } diff --git a/packages/ledger-icp/src/mocks/ledger.mock.ts b/packages/ledger-icp/src/mocks/ledger.mock.ts index f35202044..fb824d7aa 100644 --- a/packages/ledger-icp/src/mocks/ledger.mock.ts +++ b/packages/ledger-icp/src/mocks/ledger.mock.ts @@ -1,5 +1,11 @@ +import { Principal } from "@dfinity/principal"; import { AccountIdentifier } from "../account_identifier"; export const mockAccountIdentifier = AccountIdentifier.fromHex( "3e8bbceef8b9338e56a1b561a127326e6614894ab9b0739df4cc3664d40a5958", ); + +export const mockPrincipalText = + "xlmdg-vkosz-ceopx-7wtgu-g3xmd-koiyc-awqaq-7modz-zf6r6-364rh-oqe"; + +export const mockPrincipal = Principal.fromText(mockPrincipalText); diff --git a/packages/ledger-icp/src/types/ledger_converters.ts b/packages/ledger-icp/src/types/ledger_converters.ts index ade1ade77..2f87c3177 100644 --- a/packages/ledger-icp/src/types/ledger_converters.ts +++ b/packages/ledger-icp/src/types/ledger_converters.ts @@ -36,3 +36,21 @@ export type Icrc1TransferRequest = { // https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/README.md#transaction_deduplication createdAt?: Icrc1Timestamp; }; + +/** + * Params for an icrc2_approve. + * + * @param {Account} spender The account of the spender. + * @param {Tokens} amount The amount of tokens to approve. + * @param {Subaccount?} from_subaccount The subaccount to transfer tokens from. + * @param {Uint8Array|number?} icrc1Memo Approve memo. + * @param {Timestamp?} created_at_time nanoseconds since unix epoc to trigger deduplication and avoid other issues + * @param {Tokens?} fee The fee of the transfer when it's not the default fee. + * @param {Tokens?} expected_allowance The optional allowance expected. If the expected_allowance field is set, the ledger MUST ensure that the current allowance for the spender from the caller's account is equal to the given value and return the AllowanceChanged error otherwise. + * @param {Timestamp?} expires_at When the approval expires. If the field is set, it's greater than the current ledger time. + */ +export type Icrc2ApproveRequest = Omit & { + expected_allowance?: Icrc1Tokens; + expires_at?: Icrc1Timestamp; + spender: Account; +};