From b2f142e225889ef17e690159824070555ac0496d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Prudent?= Date: Fri, 7 Feb 2025 10:04:44 +0100 Subject: [PATCH] fixup! max iteration --- .vscode/launch.json | 13 +++++ .../coin-tezos/src/api/index.integ.test.ts | 4 +- .../coin-tezos/src/api/index.test.ts | 36 +++++++++++- libs/coin-modules/coin-tezos/src/api/index.ts | 58 +++++++++---------- .../src/logic/listOperations.test.ts | 10 ++-- .../coin-tezos/src/logic/listOperations.ts | 10 +++- .../coin-tezos/src/network/types.ts | 2 + .../coin-tezos/src/network/tzkt.ts | 9 ++- 8 files changed, 94 insertions(+), 48 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0f3e1d56201a..3fc01875a8d6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,19 @@ "type": "node", "request": "attach", "skipFiles": ["/**"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug Jest Tests", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--runInBand", "coin-tezoz", "api/index.test.ts"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "skipFiles": ["/**"], + "runtimeArgs": ["--inspect-brk"], + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/dist/**/*.js"] } ] } diff --git a/libs/coin-modules/coin-tezos/src/api/index.integ.test.ts b/libs/coin-modules/coin-tezos/src/api/index.integ.test.ts index deca17a272cb..c30cafca75b8 100644 --- a/libs/coin-modules/coin-tezos/src/api/index.integ.test.ts +++ b/libs/coin-modules/coin-tezos/src/api/index.integ.test.ts @@ -57,7 +57,7 @@ describe("Tezos Api", () => { expect(isSenderOrReceipt).toBeTruthy(); expect(operation.block).toBeDefined(); }); - }, 15000); + }); it("returns all operations", async () => { // When @@ -67,7 +67,7 @@ describe("Tezos Api", () => { // Find a way to create a unique id. In Tezos, the same hash may represent different operations in case of delegation. const checkSet = new Set(tx.map(elt => `${elt.hash}${elt.type}${elt.senders[0]}`)); expect(checkSet.size).toEqual(tx.length); - }, 15000); + }); }); describe("lastBlock", () => { diff --git a/libs/coin-modules/coin-tezos/src/api/index.test.ts b/libs/coin-modules/coin-tezos/src/api/index.test.ts index b2f901271205..b5989d03ec53 100644 --- a/libs/coin-modules/coin-tezos/src/api/index.test.ts +++ b/libs/coin-modules/coin-tezos/src/api/index.test.ts @@ -1,9 +1,10 @@ +import { Operation } from "@ledgerhq/coin-framework/lib/api/types"; import { createApi } from "./index"; -const mockGetTransactions = jest.fn(); +const logicGetTransactions = jest.fn(); jest.mock("../logic", () => ({ listOperations: async () => { - return mockGetTransactions(); + return logicGetTransactions(); }, })); @@ -28,8 +29,12 @@ const api = createApi({ }); describe("get operations", () => { + afterEach(() => { + logicGetTransactions.mockClear(); + }); + it("operations", async () => { - mockGetTransactions.mockResolvedValue([[], ""]); + logicGetTransactions.mockResolvedValue([[], ""]); // When const [operations, token] = await api.listOperations("addr", { minHeight: 100 }); @@ -38,4 +43,29 @@ describe("get operations", () => { expect(operations).toEqual([]); expect(token).toEqual(""); }); + + const op: Operation = { + hash: "opHash", + address: "tz1...", + type: "transaction", + value: BigInt(1000), + fee: BigInt(100), + block: { + hash: "blockHash", + height: 123456, + time: new Date(), + }, + senders: ["tz1Sender"], + recipients: ["tz1Recipient"], + date: new Date(), + transactionSequenceNumber: 1, + }; + + it("stops iterating after 10 iterations", async () => { + logicGetTransactions.mockResolvedValue([[op], "888"]); + const [operations, token] = await api.listOperations("addr", { minHeight: 100 }); + expect(logicGetTransactions).toHaveBeenCalledTimes(10); + expect(operations.length).toBe(10); + expect(token).toEqual("888"); + }); }); diff --git a/libs/coin-modules/coin-tezos/src/api/index.ts b/libs/coin-modules/coin-tezos/src/api/index.ts index c533938bd3e3..00ec943896b4 100644 --- a/libs/coin-modules/coin-tezos/src/api/index.ts +++ b/libs/coin-modules/coin-tezos/src/api/index.ts @@ -74,55 +74,49 @@ type PaginationState = { currentIteration: number; readonly minHeight: number; continueIterations: boolean; - nextCursor: string; + nextCursor?: string; accumulator: Operation[]; }; +async function fetchNextPage(address: string, state: PaginationState): Promise { + const [operations, newNextCursor] = await listOperations(address, { + limit: state.pageSize, + token: state.nextCursor, + sort: "Ascending", + minHeight: state.minHeight, + }); + const newCurrentIteration = state.currentIteration + 1; + let continueIteration = newNextCursor !== ""; + if (newCurrentIteration >= state.maxIterations) { + log("coin:tezos", "(api/operations): max iterations reached", state.maxIterations); + continueIteration = false; + } + const accumulated = operations.concat(state.accumulator); + return { + ...state, + continueIterations: continueIteration, + currentIteration: newCurrentIteration, + nextCursor: newNextCursor, + accumulator: accumulated, + }; +} + async function operationsFromHeight( address: string, start: number, ): Promise<[Operation[], string]> { - async function fetchNextPage(state: PaginationState): Promise { - console.log("🐊🐊🐊🐊🐊", { ...state, accumulator: state.accumulator.length }); - const [operations, newNextCursor] = await listOperations(address, { - limit: state.pageSize, - token: state.nextCursor, - sort: "Ascending", - }); - const newCurrentIteration = state.currentIteration + 1; - const inBoundsOperations = operations.filter(op => op.block.height >= state.minHeight); - assert( - (operations.length == 0 && newNextCursor === "") || - (operations.length > 0 && newNextCursor !== ""), - ); - const isTruncated = operations.length !== inBoundsOperations.length; - let continueIteration = !(newNextCursor === "" || isTruncated); - if (newCurrentIteration >= state.maxIterations) { - log("coin:tezos", "(api/operations): max iterations reached", state.maxIterations); - continueIteration = false; - } - const accumulated = inBoundsOperations.concat(state.accumulator); - return { - ...state, - continueIterations: continueIteration, - nextCursor: newNextCursor, - accumulator: accumulated, - }; - } - const firstState: PaginationState = { pageSize: 200, maxIterations: 10, currentIteration: 0, minHeight: start, continueIterations: true, - nextCursor: "0", // this is supposed to be the lowest operation id accumulator: [], }; - let state = await fetchNextPage(firstState); + let state = await fetchNextPage(address, firstState); while (state.continueIterations) { - state = await fetchNextPage(state); + state = await fetchNextPage(address, state); } return [state.accumulator, state.nextCursor || ""]; } diff --git a/libs/coin-modules/coin-tezos/src/logic/listOperations.test.ts b/libs/coin-modules/coin-tezos/src/logic/listOperations.test.ts index e771ffa27127..47b3a9f5d919 100644 --- a/libs/coin-modules/coin-tezos/src/logic/listOperations.test.ts +++ b/libs/coin-modules/coin-tezos/src/logic/listOperations.test.ts @@ -10,12 +10,14 @@ jest.mock("../network", () => ({ }, })); -const options: { sort: "Ascending" | "Descending" } = { sort: "Ascending" }; +const options: { sort: "Ascending" | "Descending"; minHeight: number } = { + sort: "Ascending", + minHeight: 0, +}; describe("listOperations", () => { afterEach(() => { - // mockGetServerInfos.mockClear(); - // mockNetworkGetTransactions.mockClear(); + mockNetworkGetTransactions.mockClear(); }); it("should return no operations", async () => { @@ -106,7 +108,7 @@ describe("listOperations", () => { const op1 = { ...undelegate, level: "1" }; const op2 = { ...undelegate, level: "2" }; mockNetworkGetTransactions.mockResolvedValue([op1, op2]); - const [results, _] = await listOperations("any address", { sort: "Ascending" }); + const [results, _] = await listOperations("any address", options); expect(results.map(op => op.block.height)).toEqual(["2", "1"]); }); }); diff --git a/libs/coin-modules/coin-tezos/src/logic/listOperations.ts b/libs/coin-modules/coin-tezos/src/logic/listOperations.ts index ef7e101a9d02..37cf3ff2332e 100644 --- a/libs/coin-modules/coin-tezos/src/logic/listOperations.ts +++ b/libs/coin-modules/coin-tezos/src/logic/listOperations.ts @@ -34,14 +34,20 @@ export type Operation = { * "Descending" returns newest operation first, "Ascending" returns oldest operation first. * It doesn't control the order of the operations in the result list: * operations are always returned sorted in descending order (newest first). + * @param minHeight retrieve operations from a specific block height until top most (inclusive). * @param token a token to be used for pagination * @returns a list of operations is descending (newest first) order and a token to be used for pagination */ export async function listOperations( address: string, - { token, limit, sort }: { limit?: number; token?: string; sort: "Ascending" | "Descending" }, + { + token, + limit, + sort, + minHeight, + }: { limit?: number; token?: string; sort: "Ascending" | "Descending"; minHeight: number }, ): Promise<[Operation[], string]> { - let options: AccountsGetOperationsOptions = { limit, sort }; + let options: AccountsGetOperationsOptions = { limit, sort, "level.ge": minHeight }; if (token) { options = { ...options, lastId: JSON.parse(token) }; } diff --git a/libs/coin-modules/coin-tezos/src/network/types.ts b/libs/coin-modules/coin-tezos/src/network/types.ts index 9a372fcec59e..5909ae1ab56c 100644 --- a/libs/coin-modules/coin-tezos/src/network/types.ts +++ b/libs/coin-modules/coin-tezos/src/network/types.ts @@ -67,6 +67,8 @@ export type AccountsGetOperationsOptions = { lastId?: number; // used as a pagination cursor to fetch more transactions limit?: number; sort?: "Descending" | "Ascending"; + // the minimum height of the block the operation is in + "level.ge": number; }; export type APIOperation = diff --git a/libs/coin-modules/coin-tezos/src/network/tzkt.ts b/libs/coin-modules/coin-tezos/src/network/tzkt.ts index ab6e74ac1cce..9e117bd770c5 100644 --- a/libs/coin-modules/coin-tezos/src/network/tzkt.ts +++ b/libs/coin-modules/coin-tezos/src/network/tzkt.ts @@ -2,7 +2,7 @@ import URL from "url"; import { log } from "@ledgerhq/logs"; import network from "@ledgerhq/live-network"; import coinConfig from "../config"; -import { APIAccount, APIBlock, APIOperation, AccountsGetOperationsOptions as GetAccountOperationsOptions } from "./types"; +import { APIAccount, APIBlock, APIOperation, AccountsGetOperationsOptions } from "./types"; const getExplorerUrl = () => coinConfig.getCoinConfig().explorer.url; @@ -36,7 +36,7 @@ const api = { // https://api.tzkt.io/#operation/Accounts_GetOperations async getAccountOperations( address: string, - query: GetAccountOperationsOptions, + query: AccountsGetOperationsOptions, ): Promise { // Remove undefined from query Object.entries(query).forEach( @@ -52,7 +52,7 @@ const api = { }, }; - +// TODO this has same purpose as api/listOperations export const fetchAllTransactions = async ( address: string, lastId?: number, @@ -62,9 +62,8 @@ export const fetchAllTransactions = async ( do { const newOps = await api.getAccountOperations(address, { lastId, - // FIXME use object - // FIXME check if this is a number or string, documentation is unclear sort: "Ascending", + "level.ge": 0, }); if (newOps.length === 0) return ops; ops = ops.concat(newOps);