From 0fd316d7946784d480bee69f03d13e91425f3feb Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 13 Nov 2023 11:53:26 +0100 Subject: [PATCH 1/2] docs: fix typo in ledger-icrc README (#464) # Motivation Fix a typo in README which is the root cause that leads to the issue faced in #462 question. --- packages/ledger-icrc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ledger-icrc/README.md b/packages/ledger-icrc/README.md index df53ec378..c5a66bb3b 100644 --- a/packages/ledger-icrc/README.md +++ b/packages/ledger-icrc/README.md @@ -45,7 +45,7 @@ const { metadata } = IcrcLedgerCanister.create({ canisterId: MY_LEDGER_CANISTER_ID, }); -const data = await metadata(); +const data = await metadata({}); ``` ## Features From 8a6f5d9761b63a2b1acbfbd42b22b675a58405d3 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 14 Nov 2023 14:02:00 +0100 Subject: [PATCH 2/2] feat: ICP index canister getTransactions (#466) # Motivation Add support to fetch transactions with the ICP Index canister. # Changes - introduce `get_account_identifier_transactions` --- CHANGELOG.md | 2 +- packages/ledger-icp/README.md | 25 ++- .../ledger-icp/src/errors/index.errors.ts | 1 + .../ledger-icp/src/index.canister.spec.ts | 152 +++++++++++++++++- packages/ledger-icp/src/index.canister.ts | 46 +++++- packages/ledger-icp/src/index.ts | 1 + packages/ledger-icp/src/types/index.params.ts | 8 + 7 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 packages/ledger-icp/src/errors/index.errors.ts create mode 100644 packages/ledger-icp/src/types/index.params.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a01b1c96..9f8817c88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ## Features -- add support for ICP Index canister to library `@dfinity/ledger-icp`. New `IndexCanister` functions: `accountBalance`. +- add support for ICP Index canister to library `@dfinity/ledger-icp`. New `IndexCanister` functions: `accountBalance` and `getTransactions`. - expose few types - notably `BlockHeight` - for library `@dfinity/ledger-icp`. - support new fields from swap canister response types: `min_direct_participation_icp_e8s`, `max_direct_participation_icp_e8s` and `neurons_fund_participation`. - support new fields in the `CreateServiceNervousSystem` proposal action `maximum_direct_participation_icp`, `minimum_direct_participation_icp` and `neurons_fund_participation`. diff --git a/packages/ledger-icp/README.md b/packages/ledger-icp/README.md index 039c3db7e..1fb619857 100644 --- a/packages/ledger-icp/README.md +++ b/packages/ledger-icp/README.md @@ -241,12 +241,13 @@ Returns the index of the block containing the tx if it was successful. ### :factory: IndexCanister -[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/index.canister.ts#L9) +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/index.canister.ts#L19) #### Methods - [create](#gear-create) - [accountBalance](#gear-accountbalance) +- [getTransactions](#gear-gettransactions) ##### :gear: create @@ -254,7 +255,7 @@ Returns the index of the block containing the tx if it was successful. | -------- | --------------------------------------------------------------------------------------------- | | `create` | `({ canisterId: optionsCanisterId, ...options }: CanisterOptions<_SERVICE>) => IndexCanister` | -[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/index.canister.ts#L10) +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/index.canister.ts#L20) ##### :gear: accountBalance @@ -270,7 +271,25 @@ 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/index.canister.ts#L35) +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/index.canister.ts#L45) + +##### :gear: getTransactions + +Returns the transactions and balance of an ICP account. + +| Method | Type | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `getTransactions` | `({ certified, accountIdentifier, start, maxResults: max_results, }: GetTransactionsParams) => Promise` | + +Parameters: + +- `params`: The parameters to get the transactions. +- `params.certified`: query or update call. +- `params.accountIdentifier`: The account identifier provided either as hex string or as an AccountIdentifier. +- `params.start`: If set then the results will start from the next most recent transaction id after start (start won't be included). If not provided, then the results will start from the most recent transaction id. +- `params.maxResults`: Maximum number of transactions to fetch. + +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icp/src/index.canister.ts#L64) diff --git a/packages/ledger-icp/src/errors/index.errors.ts b/packages/ledger-icp/src/errors/index.errors.ts new file mode 100644 index 000000000..24fe693f5 --- /dev/null +++ b/packages/ledger-icp/src/errors/index.errors.ts @@ -0,0 +1 @@ +export class IndexError extends Error {} diff --git a/packages/ledger-icp/src/index.canister.spec.ts b/packages/ledger-icp/src/index.canister.spec.ts index 7db4b501c..bb586e61c 100644 --- a/packages/ledger-icp/src/index.canister.spec.ts +++ b/packages/ledger-icp/src/index.canister.spec.ts @@ -1,6 +1,10 @@ import { ActorSubclass } from "@dfinity/agent"; import { mock } from "jest-mock-extended"; -import { _SERVICE as IndexService } from "../candid/index"; +import { + GetAccountIdentifierTransactionsError, + GetAccountIdentifierTransactionsResponse, + _SERVICE as IndexService, +} from "../candid/index"; import { IndexCanister } from "./index.canister"; import { mockAccountIdentifier } from "./mocks/ledger.mock"; @@ -71,4 +75,150 @@ describe("IndexCanister", () => { ).toThrowError(); }); }); + + describe("getTransactions", () => { + const transactionsMock = { + Ok: { + balance: 1234n, + transactions: [{ id: 1n }, { id: 2n }], + oldest_tx_id: [], + } as GetAccountIdentifierTransactionsResponse, + }; + + it("returns transactions with query call", async () => { + const service = mock>(); + service.get_account_identifier_transactions.mockResolvedValue( + transactionsMock, + ); + const index = IndexCanister.create({ + serviceOverride: service, + }); + + const transactions = await index.getTransactions({ + accountIdentifier: mockAccountIdentifier, + certified: false, + maxResults: 10n, + }); + + expect(transactions).toEqual(transactionsMock.Ok); + expect(service.get_account_identifier_transactions).toBeCalledWith({ + account_identifier: mockAccountIdentifier.toHex(), + max_results: 10n, + start: [], + }); + }); + + it("returns transactions with update call", async () => { + const service = mock>(); + service.get_account_identifier_transactions.mockResolvedValue( + transactionsMock, + ); + const index = IndexCanister.create({ + certifiedServiceOverride: service, + }); + + const transactions = await index.getTransactions({ + accountIdentifier: mockAccountIdentifier, + certified: true, + maxResults: 10n, + }); + + expect(transactions).toEqual(transactionsMock.Ok); + expect(service.get_account_identifier_transactions).toBeCalledWith({ + account_identifier: mockAccountIdentifier.toHex(), + max_results: 10n, + start: [], + }); + }); + + it("returns transactions with account identifier as hex", async () => { + const service = mock>(); + service.get_account_identifier_transactions.mockResolvedValue( + transactionsMock, + ); + const index = IndexCanister.create({ + serviceOverride: service, + }); + + const transactions = await index.getTransactions({ + accountIdentifier: mockAccountIdentifier.toHex(), + certified: false, + maxResults: 10n, + }); + + expect(transactions).toEqual(transactionsMock.Ok); + expect(service.get_account_identifier_transactions).toBeCalledWith({ + account_identifier: mockAccountIdentifier.toHex(), + max_results: 10n, + start: [], + }); + }); + + it("query transactions from start", async () => { + const service = mock>(); + service.get_account_identifier_transactions.mockResolvedValue( + transactionsMock, + ); + const index = IndexCanister.create({ + serviceOverride: service, + }); + + const transactions = await index.getTransactions({ + accountIdentifier: mockAccountIdentifier.toHex(), + certified: false, + maxResults: 10n, + start: 3n, + }); + + expect(transactions).toEqual(transactionsMock.Ok); + expect(service.get_account_identifier_transactions).toBeCalledWith({ + account_identifier: mockAccountIdentifier.toHex(), + max_results: 10n, + start: [3n], + }); + }); + + it("throws errors", async () => { + const transactionsErrorMock = { + Err: { + message: "Test error", + } as GetAccountIdentifierTransactionsError, + }; + + const service = mock>(); + service.get_account_identifier_transactions.mockResolvedValue( + transactionsErrorMock, + ); + const index = IndexCanister.create({ + serviceOverride: service, + }); + + expect(() => + index.getTransactions({ + accountIdentifier: mockAccountIdentifier.toHex(), + certified: false, + maxResults: 10n, + }), + ).rejects.toThrowError(); + }); + + it("should bubble errors", () => { + const service = mock>(); + service.get_account_identifier_transactions.mockImplementation(() => { + throw new Error(); + }); + + const index = IndexCanister.create({ + serviceOverride: service, + }); + + expect(() => + index.getTransactions({ + accountIdentifier: mockAccountIdentifier.toHex(), + certified: false, + maxResults: 10n, + }), + ).rejects.toThrowError(); + }); + }); }); diff --git a/packages/ledger-icp/src/index.canister.ts b/packages/ledger-icp/src/index.canister.ts index e9b68c7bb..e055aba24 100644 --- a/packages/ledger-icp/src/index.canister.ts +++ b/packages/ledger-icp/src/index.canister.ts @@ -1,8 +1,18 @@ -import { Canister, createServices, type CanisterOptions } from "@dfinity/utils"; -import type { _SERVICE as IndexService } from "../candid/index"; +import { + Canister, + createServices, + toNullable, + type CanisterOptions, +} from "@dfinity/utils"; +import type { + GetAccountIdentifierTransactionsResponse, + _SERVICE as IndexService, +} from "../candid/index"; import { idlFactory as certifiedIdlFactory } from "../candid/ledger.certified.idl"; import { idlFactory } from "../candid/ledger.idl"; import { MAINNET_INDEX_CANISTER_ID } from "./constants/canister_ids"; +import { IndexError } from "./errors/index.errors"; +import type { GetTransactionsParams } from "./types/index.params"; import type { AccountBalanceParams } from "./types/ledger.params"; import { paramToAccountIdentifier } from "./utils/params.utils"; @@ -39,4 +49,36 @@ export class IndexCanister extends Canister { this.caller({ certified }).get_account_identifier_balance( paramToAccountIdentifier(accountIdentifier).toHex(), ); + + /** + * Returns the transactions and balance of an ICP account. + * + * @param {GetTransactionsParams} params The parameters to get the transactions. + * @param {boolean} params.certified query or update call. + * @param {AccountIdentifierParam} params.accountIdentifier The account identifier provided either as hex string or as an AccountIdentifier. + * @param {bigint} params.start If set then the results will start from the next most recent transaction id after start (start won't be included). If not provided, then the results will start from the most recent transaction id. + * @param {bigint} params.maxResults Maximum number of transactions to fetch. + * @returns {Promise} The transactions, balance and the transaction id of the oldest transaction the account has. + * @throws {@link IndexError} + */ + getTransactions = async ({ + certified, + accountIdentifier, + start, + maxResults: max_results, + }: GetTransactionsParams): Promise => { + const response = await this.caller({ + certified, + }).get_account_identifier_transactions({ + account_identifier: paramToAccountIdentifier(accountIdentifier).toHex(), + start: toNullable(start), + max_results, + }); + + if ("Err" in response) { + throw new IndexError(response.Err.message); + } + + return response.Ok; + }; } diff --git a/packages/ledger-icp/src/index.ts b/packages/ledger-icp/src/index.ts index 4a2be52d3..090c7e7e0 100644 --- a/packages/ledger-icp/src/index.ts +++ b/packages/ledger-icp/src/index.ts @@ -1,3 +1,4 @@ +export type * from "../candid/index"; export { AccountIdentifier, SubAccount } from "./account_identifier"; export * from "./errors/ledger.errors"; export { IndexCanister } from "./index.canister"; diff --git a/packages/ledger-icp/src/types/index.params.ts b/packages/ledger-icp/src/types/index.params.ts new file mode 100644 index 000000000..3540c9502 --- /dev/null +++ b/packages/ledger-icp/src/types/index.params.ts @@ -0,0 +1,8 @@ +import type { QueryParams } from "@dfinity/utils"; +import type { AccountIdentifierParam } from "./ledger.params"; + +export type GetTransactionsParams = { + maxResults: bigint; + start?: bigint; + accountIdentifier: AccountIdentifierParam; +} & QueryParams;