diff --git a/.changeset/wise-hornets-hunt.md b/.changeset/wise-hornets-hunt.md new file mode 100644 index 000000000000..f67e9c603df6 --- /dev/null +++ b/.changeset/wise-hornets-hunt.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/coin-tezos": patch +--- + +Fix addresses on operations diff --git a/libs/coin-modules/coin-tezos/package.json b/libs/coin-modules/coin-tezos/package.json index 3e7cf7f9ff97..4f7a6ef25757 100644 --- a/libs/coin-modules/coin-tezos/package.json +++ b/libs/coin-modules/coin-tezos/package.json @@ -108,6 +108,7 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "test": "jest", + "test-watch": "jest --watch", "test-integ": "jest --config=jest.integ.config.js", "unimported": "unimported" }, diff --git a/libs/coin-modules/coin-tezos/src/logic/listOperations.test.ts b/libs/coin-modules/coin-tezos/src/logic/listOperations.test.ts new file mode 100644 index 000000000000..d86755ae5378 --- /dev/null +++ b/libs/coin-modules/coin-tezos/src/logic/listOperations.test.ts @@ -0,0 +1,101 @@ +import { listOperations } from "./listOperations"; +import { APIDelegationType, APITransactionType } from "../network/types"; + +const mockNetworkGetTransactions = jest.fn(); +jest.mock("../network", () => ({ + tzkt: { + getAccountOperations: async () => { + return mockNetworkGetTransactions(); + }, + }, +})); + +describe("listOperations", () => { + afterEach(() => { + mockNetworkGetTransactions.mockClear(); + }); + + it("should return no operations", async () => { + // Given + mockNetworkGetTransactions.mockResolvedValue([]); + // When + const [results, token] = await listOperations("any address", {}); + // Then + expect(results).toEqual([]); + expect(token).toEqual(""); + }); + + const someDestinationAddress = "tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D"; + const someSenderAddress = "tz2CVMDVA16dD9A7kpWym2ptGDhs5zUhwWXr"; + const delegate: APIDelegationType = { + type: "delegation", + id: 111, + level: 2702551, + block: "BMJ1ZQ6", + timestamp: "2022-09-12T01:36:59Z", + amount: 724846, + sender: { + address: someSenderAddress, + }, + counter: 65214462, + prevDelegate: { + address: someDestinationAddress, + }, + newDelegate: null, + }; + + const undelegate: APIDelegationType = { + ...delegate, + id: 222, + prevDelegate: null, + newDelegate: { address: someDestinationAddress }, + }; + + const transfer: APITransactionType = { + ...delegate, + id: 333, + initiator: null, + type: "transaction", + target: { address: someDestinationAddress }, + }; + + it.each([undelegate, delegate, transfer])( + "should return operation with proper recipient list", + async operation => { + // Given + mockNetworkGetTransactions.mockResolvedValue([operation]); + // When + const [results, token] = await listOperations("any address", {}); + // Then + expect(results.length).toEqual(1); + expect(results[0].recipients).toEqual([someDestinationAddress]); + expect(token).toEqual(JSON.stringify(operation.id)); + }, + ); + + it.each([ + { ...undelegate, newDelegate: null, prevDelegate: null }, + { ...transfer, target: null }, + ])("should return empty recipient list when no target can be found", async operation => { + // Given + mockNetworkGetTransactions.mockResolvedValue([operation]); + // When + const [results, token] = await listOperations("any address", {}); + // Then + expect(results.length).toEqual(1); + expect(results[0].recipients).toEqual([]); + expect(token).toEqual(JSON.stringify(operation.id)); + }); + + it("should return empty sender list when no sender can be found", async () => { + // Given + const operation = { ...undelegate, sender: null }; + mockNetworkGetTransactions.mockResolvedValue([operation]); + // When + const [results, token] = await listOperations("any address", {}); + // Then + expect(results.length).toEqual(1); + expect(results[0].senders).toEqual([]); + expect(token).toEqual(JSON.stringify(operation.id)); + }); +}); diff --git a/libs/coin-modules/coin-tezos/src/logic/listOperations.ts b/libs/coin-modules/coin-tezos/src/logic/listOperations.ts index 9220d872c31d..3b104309c9c3 100644 --- a/libs/coin-modules/coin-tezos/src/logic/listOperations.ts +++ b/libs/coin-modules/coin-tezos/src/logic/listOperations.ts @@ -1,4 +1,5 @@ import { tzkt } from "../network"; +import { log } from "@ledgerhq/logs"; import { type APIDelegationType, type APITransactionType, @@ -33,25 +34,41 @@ export async function listOperations( } const operations = await tzkt.getAccountOperations(address, options); const lastOperation = operations.slice(-1)[0]; - const nextId = lastOperation ? JSON.stringify(lastOperation?.id) : ""; + const nextToken = lastOperation ? JSON.stringify(lastOperation?.id) : ""; return [ operations .filter(op => isAPITransactionType(op) || isAPIDelegationType(op)) .reduce((acc, op) => acc.concat(convertOperation(address, op)), [] as Operation[]), - nextId, + nextToken, ]; } +// note that "initiator" of APITransactionType is never used in the conversion function convertOperation( address: string, operation: APITransactionType | APIDelegationType, ): Operation { const { amount, hash, storageFee, sender, timestamp, type, counter } = operation; - let targetAddress = ""; - if (isAPITransactionType(operation) && operation.target) { - targetAddress = operation.target.address; + + let targetAddress = undefined; + if (isAPITransactionType(operation)) { + targetAddress = operation?.target?.address; + } else if (isAPIDelegationType(operation)) { + // delegate and undelegate has the type, but hold the address in different fields + targetAddress = operation?.newDelegate?.address || operation?.prevDelegate?.address; + } + + const recipients = []; + if (!targetAddress) { + log("coin:tezos", "(logic/operations): No target address found for operation", operation); + } else { + recipients.push(targetAddress); } + + const senders = sender?.address ? [sender.address] : []; + return { + // hash id defined nullable in the tzkt API, but I wonder when it would be null ? hash: hash ?? "", address, type: type, @@ -63,8 +80,8 @@ function convertOperation( height: operation.level, time: new Date(operation.timestamp), }, - senders: [sender?.address ?? ""], - recipients: [targetAddress], + senders: senders, + recipients: recipients, date: new Date(timestamp), transactionSequenceNumber: counter, };