Skip to content

Commit

Permalink
feat(coin:xrp): apply api change on operations (#8987)
Browse files Browse the repository at this point in the history
* fix(coin:xrp): api iterates over all pages
  • Loading branch information
jprudent authored Jan 31, 2025
1 parent cd6afcd commit 5969aaa
Show file tree
Hide file tree
Showing 21 changed files with 460 additions and 279 deletions.
18 changes: 8 additions & 10 deletions libs/coin-framework/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,19 @@ export type Transaction = {
supplement?: unknown;
};

export type Pagination = { limit: number; start?: number };
// TODO rename start to minHeight
// and add a `token: string` field to the pagination if we really need to support pagination
// (which is not the case for now)
// for now start is used as a minHeight from which we want to fetch ALL operations
// limit is unused for now
// see design document at https://ledgerhq.atlassian.net/wiki/spaces/BE/pages/5446205788/coin-modules+lama-adapter+APIs+refinements
export type Pagination = { minHeight: number };
export type Api = {
broadcast: (tx: string) => Promise<string>;
combine: (tx: string, signature: string, pubkey?: string) => string;
craftTransaction: (address: string, transaction: Transaction, pubkey?: string) => Promise<string>;
estimateFees: (addr: string, amount: bigint) => Promise<bigint>;
getBalance: (address: string) => Promise<bigint>;
lastBlock: () => Promise<BlockInfo>;
/**
*
* @param address
* @param pagination The max number of operation to receive and the "id" or "index" to start from (see returns value).
* @returns Operations found and the next "id" or "index" to use for pagination (i.e. `start` property).\
* If `0` is returns, no pagination needed.
* This "id" or "index" value, thus it has functional meaning, is different for each blockchain.
*/
listOperations: (address: string, pagination: Pagination) => Promise<[Operation[], number]>;
listOperations: (address: string, pagination: Pagination) => Promise<[Operation[], string]>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ import { getTransactions } from "../../network/indexer";
*/
export async function listOperations(
address: string,
{ limit, start }: Pagination,
): Promise<[Operation[], number]> {
const transactions = await getTransactions(address, { from: start || 0, size: limit });

return [transactions.map(convertToCoreOperation(address)), transactions.length];
page: Pagination,
): Promise<[Operation[], string]> {
const transactions = await getTransactions(address, { from: page.minHeight });
return [transactions.map(convertToCoreOperation(address)), ""];
}

const convertToCoreOperation = (address: string) => (operation: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getEnv } from "@ledgerhq/live-env";

export const getTransactions = async (
address: string,
params: { from: number; size: number },
params: { from: number; size?: number },
): Promise<AccountTxResponse["transactions"]> => {
const { data } = await network<AccountTxResponse>({
// NOTE: add INDEXER_BOILERPLATE to libs/env/src/env.ts
Expand Down
8 changes: 3 additions & 5 deletions libs/coin-modules/coin-polkadot/src/api/index.integ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("Polkadot Api", () => {
describe("listOperations", () => {
it("returns a list regarding address parameter", async () => {
// When
const [tx, _] = await module.listOperations(address, { limit: 100 });
const [tx, _] = await module.listOperations(address, { minHeight: 0 });

// Then
expect(tx.length).toBeGreaterThanOrEqual(1);
Expand All @@ -54,11 +54,9 @@ describe("Polkadot Api", () => {
});
}, 20000);

it("returns paginated operations", async () => {
it("returns all operations", async () => {
// When
const [tx, idx] = await module.listOperations(address, { limit: 100 });
const [tx2, _] = await module.listOperations(address, { limit: 100, start: idx });
tx.push(...tx2);
const [tx, _] = await module.listOperations(address, { minHeight: 0 });

// Then
const checkSet = new Set(tx.map(elt => elt.hash));
Expand Down
10 changes: 8 additions & 2 deletions libs/coin-modules/coin-polkadot/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
Api,
Transaction as ApiTransaction,
Operation,
Pagination,
} from "@ledgerhq/coin-framework/api/index";
import coinConfig, { type PolkadotConfig } from "../config";
Expand Down Expand Up @@ -47,5 +48,10 @@ async function estimate(addr: string, amount: bigint): Promise<bigint> {
return estimateFees(tx);
}

const operations = async (addr: string, { limit, start }: Pagination) =>
listOperations(addr, { limit, startAt: start });
async function operations(
address: string,
{ minHeight }: Pagination,
): Promise<[Operation[], string]> {
const [ops, nextHeight] = await listOperations(address, { limit: 0, startAt: minHeight });
return [ops, JSON.stringify(nextHeight)];
}
8 changes: 3 additions & 5 deletions libs/coin-modules/coin-stellar/src/api/index.integ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("Stellar Api", () => {
describe.only("listOperations", () => {
it("returns a list regarding address parameter", async () => {
// When
const [tx, _] = await module.listOperations(address, { limit: 100 });
const [tx, _] = await module.listOperations(address, { minHeight: 0 });

// Then
expect(tx.length).toBeGreaterThanOrEqual(100);
Expand All @@ -44,11 +44,9 @@ describe("Stellar Api", () => {
});
});

it("returns paginated operations", async () => {
it("returns all operations", async () => {
// When
const [tx, idx] = await module.listOperations(address, { limit: 200 });
const [tx2, _] = await module.listOperations(address, { limit: 200, start: idx });
tx.push(...tx2);
const [tx, _] = await module.listOperations(address, { minHeight: 0 });

// Then
const checkSet = new Set(tx.map(elt => elt.hash));
Expand Down
9 changes: 6 additions & 3 deletions libs/coin-modules/coin-stellar/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ function compose(tx: string, signature: string, pubkey?: string): string {
return combine(tx, signature, pubkey);
}

const operations = async (
async function operations(
address: string,
{ limit, start }: Pagination,
): Promise<[Operation[], number]> => listOperations(address, { limit, cursor: start });
_pagination: Pagination,
): Promise<[Operation[], string]> {
// TODO will be fixed with https://github.com/LedgerHQ/ledger-live/pull/8898/files
return listOperations(address, { limit: 200 });
}
8 changes: 4 additions & 4 deletions libs/coin-modules/coin-stellar/src/logic/listOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,21 @@ export type Operation = {

export async function listOperations(
address: string,
{ limit, cursor }: { limit: number; cursor?: number | undefined },
): Promise<[Operation[], number]> {
{ limit, cursor }: { limit?: number; cursor?: string },
): Promise<[Operation[], string]> {
// Fake accountId
const accountId = "";
const operations = await fetchOperations({
accountId,
addr: address,
order: "asc",
limit,
cursor: cursor?.toString(),
cursor: cursor,
});

return [
operations.map(convertToCoreOperation(address)),
parseInt(operations.slice(-1)[0].extra.pagingToken ?? "0"),
operations.slice(-1)[0].extra.pagingToken ?? "",
];
}

Expand Down
8 changes: 3 additions & 5 deletions libs/coin-modules/coin-tezos/src/api/index.integ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe("Tezos Api", () => {
describe.only("listOperations", () => {
it("returns a list regarding address parameter", async () => {
// When
const [tx, _] = await module.listOperations(address, { limit: 100 });
const [tx, _] = await module.listOperations(address, { minHeight: 0 });

// Then
expect(tx.length).toBeGreaterThanOrEqual(1);
Expand All @@ -59,11 +59,9 @@ describe("Tezos Api", () => {
});
});

it("returns paginated operations", async () => {
it("returns all operations", async () => {
// When
const [tx, idx] = await module.listOperations(address, { limit: 100 });
const [tx2, _] = await module.listOperations(address, { limit: 100, start: idx });
tx.push(...tx2);
const [tx, _] = await module.listOperations(address, { minHeight: 0 });

// Then
// Find a way to create a unique id. In Tezos, the same hash may represent different operations in case of delegation.
Expand Down
6 changes: 4 additions & 2 deletions libs/coin-modules/coin-tezos/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,7 @@ async function estimate(addr: string, amount: bigint): Promise<bigint> {
return estimatedFees.estimatedFees;
}

const operations = (address: string, { limit, start }: Pagination) =>
listOperations(address, { limit, lastId: start });
function operations(address: string, _pagination: Pagination) {
//TODO implement properly with https://github.com/LedgerHQ/ledger-live/pull/8875
return listOperations(address, {});
}
14 changes: 10 additions & 4 deletions libs/coin-modules/coin-tezos/src/logic/listOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ export type Operation = {

export async function listOperations(
address: string,
{ lastId, limit }: { lastId?: number; limit?: number },
): Promise<[Operation[], number]> {
const operations = await tzkt.getAccountOperations(address, { lastId, limit });
{ token, limit }: { limit?: number; token?: string },
): Promise<[Operation[], string]> {
let options: { lastId?: number; limit?: number } = { limit: limit };
if (token) {
options = { ...options, lastId: JSON.parse(token) };
}
const operations = await tzkt.getAccountOperations(address, options);
const lastOperation = operations.slice(-1)[0];
const nextId = lastOperation ? JSON.stringify(lastOperation?.id) : "";
return [
operations
.filter(op => isAPITransactionType(op) || isAPIDelegationType(op))
.reduce((acc, op) => acc.concat(convertOperation(address, op)), [] as Operation[]),
operations.slice(-1)[0].id,
nextId,
];
}

Expand Down
1 change: 1 addition & 0 deletions libs/coin-modules/coin-xrp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"@ledgerhq/errors": "workspace:^",
"@ledgerhq/live-network": "workspace:^",
"@ledgerhq/types-live": "workspace:^",
"@ledgerhq/logs": "workspace:^",
"bignumber.js": "^9.1.2",
"invariant": "^2.2.4",
"ripple-address-codec": "^5.0.0",
Expand Down
18 changes: 12 additions & 6 deletions libs/coin-modules/coin-xrp/src/api/index.integ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { sign } from "ripple-keypairs";
describe("Xrp Api", () => {
let module: Api;
const address = "rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb";
const bigAddress = "rUxSkt6hQpWxXQwTNRUCYYRQ7BC2yRA3F8"; // An account with more that 4000 txs
const emptyAddress = "rKtXXTVno77jhu6tto1MAXjepyuaKaLcqB"; // Account with no transaction (at the time of this writing)
const xrpPubKey = process.env["PUB_KEY"]!;
const xrpSecretKey = process.env["SECRET_KEY"]!;
Expand All @@ -30,7 +31,7 @@ describe("Xrp Api", () => {
describe("listOperations", () => {
it("returns a list regarding address parameter", async () => {
// When
const [tx, _] = await module.listOperations(address, { limit: 200 });
const [tx, _] = await module.listOperations(address, { minHeight: 200 });

// Then
expect(tx.length).toBe(200);
Expand All @@ -42,15 +43,20 @@ describe("Xrp Api", () => {
});
});

it("returns paginated operations", async () => {
it("returns all operations", async () => {
// When
const [tx, idx] = await module.listOperations(address, { limit: 200 });
const [tx2, _] = await module.listOperations(address, { limit: 200, start: idx });
tx.push(...tx2);

const [tx, _] = await module.listOperations(bigAddress, { minHeight: 0 });
// Then
const checkSet = new Set(tx.map(elt => elt.hash));
expect(checkSet.size).toEqual(tx.length);
// the first transaction is returned
expect(tx[0].block.height).toEqual(73126713);
expect(tx[0].hash.toUpperCase).toEqual(
"0FC3792449E5B1E431D45E3606017D10EC1FECC8EDF988A98E36B8FE0C33ACAE",
);
// 200 is the default XRP explorer hard limit,
// so here we are checking that this limit is bypassed
expect(tx.length).toBeGreaterThan(200);
});
});

Expand Down
Loading

0 comments on commit 5969aaa

Please sign in to comment.