Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Alpaca] Add pagination to listOperations #8673

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading