Skip to content

Commit

Permalink
[Alpaca] Add pagination to listOperations (#8673)
Browse files Browse the repository at this point in the history
* feat: add pagination to listOperations

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: add changeset and fix lint

Signed-off-by: Stéphane Prohaszka <[email protected]>

* fix: discrepancy issue

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: feedbacks

Signed-off-by: Stéphane Prohaszka <[email protected]>

* fix: issue with id to use for pagination

Signed-off-by: Stéphane Prohaszka <[email protected]>

---------

Signed-off-by: Stéphane Prohaszka <[email protected]>
  • Loading branch information
sprohaszka-ledger authored Dec 13, 2024
1 parent 4941eec commit 9d8e34e
Show file tree
Hide file tree
Showing 32 changed files with 577 additions and 338 deletions.
9 changes: 9 additions & 0 deletions .changeset/two-hornets-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@ledgerhq/coin-polkadot": minor
"@ledgerhq/coin-stellar": minor
"@ledgerhq/coin-tezos": minor
"@ledgerhq/coin-xrp": minor
"@ledgerhq/coin-framework": minor
---

Add pagination to listOperations for Alpaca
11 changes: 10 additions & 1 deletion libs/coin-framework/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@ export type Transaction = {
supplement?: unknown;
};

export type Pagination = { limit: number; start?: 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>;
listOperations: (address: string, blockHeight: number) => Promise<Operation[]>;
/**
*
* @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]>;
};
2 changes: 1 addition & 1 deletion libs/coin-modules/coin-polkadot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,6 @@
"msw": "^2.2.13",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.4.5"
}
}
17 changes: 14 additions & 3 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,17 +42,28 @@ describe("Polkadot Api", () => {
describe("listOperations", () => {
it("returns a list regarding address parameter", async () => {
// When
const result = await module.listOperations(address, 21500219);
const [tx, _] = await module.listOperations(address, { limit: 100 });

// Then
expect(result.length).toBeGreaterThanOrEqual(1);
result.forEach(operation => {
expect(tx.length).toBeGreaterThanOrEqual(1);
tx.forEach(operation => {
expect(operation.address).toEqual(address);
const isSenderOrReceipt =
operation.senders.includes(address) || operation.recipients.includes(address);
expect(isSenderOrReceipt).toBeTruthy();
});
}, 20000);

it("returns paginated 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);

// Then
const checkSet = new Set(tx.map(elt => elt.hash));
expect(checkSet.size).toEqual(tx.length);
});
});

describe("lastBlock", () => {
Expand Down
11 changes: 9 additions & 2 deletions libs/coin-modules/coin-polkadot/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { Api, Transaction as ApiTransaction } from "@ledgerhq/coin-framework/api/index";
import type {
Api,
Transaction as ApiTransaction,
Pagination,
} from "@ledgerhq/coin-framework/api/index";
import coinConfig, { type PolkadotConfig } from "../config";
import {
broadcast,
Expand All @@ -23,7 +27,7 @@ export function createApi(config: PolkadotConfig): Api {
estimateFees: estimate,
getBalance,
lastBlock,
listOperations,
listOperations: operations,
};
}

Expand All @@ -42,3 +46,6 @@ async function estimate(addr: string, amount: bigint): Promise<bigint> {
const tx = await craftEstimationTransaction(addr, amount);
return estimateFees(tx);
}

const operations = async (addr: string, { limit, start }: Pagination) =>
listOperations(addr, { limit, startAt: start });
2 changes: 1 addition & 1 deletion libs/coin-modules/coin-polkadot/src/common/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const POLKADOT_SS58_PREFIX = 0;
*/
// TODO Cache this to improve perf
export const isValidAddress = (
address: string,
address: string | undefined,
ss58Format: number = POLKADOT_SS58_PREFIX,
): boolean => {
if (!address) return false;
Expand Down
9 changes: 6 additions & 3 deletions libs/coin-modules/coin-polkadot/src/logic/listOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ export type Operation = {
transactionSequenceNumber: number;
};

export async function listOperations(addr: string, startAt?: number): Promise<Operation[]> {
export async function listOperations(
addr: string,
{ limit, startAt }: { limit: number; startAt?: number | undefined },
): Promise<[Operation[], number]> {
//The accountId is used to map Operations to Live types.
const fakeAccountId = "";
const operations = await network.getOperations(fakeAccountId, addr, startAt);
const operations = await network.getOperations(fakeAccountId, addr, startAt, limit);

return operations.map(convertToCoreOperation(addr));
return [operations.map(convertToCoreOperation(addr)), operations.slice(-1)[0].blockHeight ?? 0];
}

const convertToCoreOperation = (address: string) => (operation: PolkadotOperation) => {
Expand Down
18 changes: 12 additions & 6 deletions libs/coin-modules/coin-polkadot/src/network/bisontrails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const getValue = (extrinsic: any, type: OperationType): BigNumber => {
const extrinsicToOperation = (
addr: string,
accountId: string,
extrinsic: any,
extrinsic: ExplorerExtrinsic,
): PolkadotOperation | null => {
let type = getOperationType(extrinsic.section, extrinsic.method);

Expand All @@ -219,7 +219,7 @@ const extrinsicToOperation = (
extra: getExtra(type, extrinsic),
senders: [extrinsic.signer],
recipients: [extrinsic.affectedAddress1, extrinsic.affectedAddress2]
.filter(Boolean)
.filter(addr => addr !== undefined)
.filter(isValidAddress),
transactionSequenceNumber: extrinsic.signer === addr ? extrinsic.nonce : undefined,
hasFailed: !extrinsic.isSuccess,
Expand Down Expand Up @@ -295,12 +295,13 @@ const fetchOperationList = async (
accountId: string,
addr: string,
startAt: number,
limit = LIMIT,
offset = 0,
prevOperations: PolkadotOperation[] = [],
): Promise<PolkadotOperation[]> => {
const { data } = await network({
method: "GET",
url: getAccountOperationUrl(addr, offset, startAt),
url: getAccountOperationUrl(addr, offset, startAt, limit),
});
const operations = data.extrinsics.map((extrinsic: any) =>
extrinsicToOperation(addr, accountId, extrinsic),
Expand All @@ -313,7 +314,7 @@ const fetchOperationList = async (
return mergedOp.filter(Boolean).sort((a, b) => b.date - a.date);
}

return await fetchOperationList(accountId, addr, startAt, offset + LIMIT, mergedOp);
return await fetchOperationList(accountId, addr, startAt, limit, offset + LIMIT, mergedOp);
};

/**
Expand All @@ -325,6 +326,11 @@ const fetchOperationList = async (
*
* @return {PolkadotOperation[]}
*/
export const getOperations = async (accountId: string, addr: string, startAt = 0) => {
return await fetchOperationList(accountId, addr, startAt);
export const getOperations = async (
accountId: string,
addr: string,
startAt = 0,
limit = LIMIT,
) => {
return await fetchOperationList(accountId, addr, startAt, limit);
};
24 changes: 19 additions & 5 deletions libs/coin-modules/coin-stellar/src/api/index.integ.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { Api } from "@ledgerhq/coin-framework/api/index";
import { createApi } from ".";

/**
* Testnet scan: https://testnet.lumenscan.io/
*/
describe("Stellar Api", () => {
let module: Api;
const address = "GD6QELUZPSKPRWVXOQ3F6GBF4OBRMCHO5PHREXH4ZRTPJAG7V5MD7JGX";
const address = "GBAUZBDXMVV7HII4JWBGFMLVKVJ6OLQAKOCGXM5E2FM4TAZB6C7JO2L7";

beforeAll(() => {
module = createApi({
Expand All @@ -26,20 +29,31 @@ describe("Stellar Api", () => {
});
});

describe("listOperations", () => {
describe.only("listOperations", () => {
it("returns a list regarding address parameter", async () => {
// When
const result = await module.listOperations(address, 0);
const [tx, _] = await module.listOperations(address, { limit: 100 });

// Then
expect(result.length).toBeGreaterThanOrEqual(1);
result.forEach(operation => {
expect(tx.length).toBeGreaterThanOrEqual(100);
tx.forEach(operation => {
expect(operation.address).toEqual(address);
const isSenderOrReceipt =
operation.senders.includes(address) || operation.recipients.includes(address);
expect(isSenderOrReceipt).toBeTruthy();
});
});

it("returns paginated 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);

// Then
const checkSet = new Set(tx.map(elt => elt.hash));
expect(checkSet.size).toEqual(tx.length);
});
});

describe("lastBlock", () => {
Expand Down
14 changes: 12 additions & 2 deletions libs/coin-modules/coin-stellar/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { Api, Transaction as ApiTransaction } from "@ledgerhq/coin-framework/api/index";
import type {
Api,
Operation,
Pagination,
Transaction as ApiTransaction,
} from "@ledgerhq/coin-framework/api/index";
import coinConfig, { type StellarConfig } from "../config";
import {
broadcast,
Expand All @@ -20,7 +25,7 @@ export function createApi(config: StellarConfig): Api {
estimateFees,
getBalance,
lastBlock,
listOperations,
listOperations: operations,
};
}

Expand Down Expand Up @@ -61,3 +66,8 @@ function compose(tx: string, signature: string, pubkey?: string): string {
}
return combine(tx, signature, pubkey);
}

const operations = async (
address: string,
{ limit, start }: Pagination,
): Promise<[Operation[], number]> => listOperations(address, { limit, cursor: start });
Loading

0 comments on commit 9d8e34e

Please sign in to comment.