Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/beige-bats-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ledgerhq/coin-cardano": patch
"ledger-live-desktop": patch
---

sort cardano default validator by less saturated pool
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
import * as StakingFunctions from "@ledgerhq/live-common/families/cardano/staking";
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import {
concatUserAndLedgerPoolIds,
fetchAndSortPools,
putUserPoolAtFirstPositionInPools,
} from "./ValidatorField";
import { StakePool } from "@ledgerhq/live-common/families/cardano/staking";

function stackPool(id: string): StakePool {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return { poolId: id } as StakePool;
}
import { concatUserAndLedgerPoolIds } from "./ValidatorField";

describe("Testing ValidatorField functions", () => {
it.each([[[]], [["1"]], [["1", "2"]]])(
Expand All @@ -31,55 +19,4 @@ describe("Testing ValidatorField functions", () => {
ledgerPoolIds.forEach(id => expect(ids).toContain(id));
},
);

it.each([
[[stackPool("1")]],
[[stackPool("1"), stackPool("2")]],
[[stackPool("2"), stackPool("1")]],
])(
"should always put user last used pool at the first index when user pool exists",
(pools: StakePool[]) => {
const userLastUsedPool = "1";

const result = putUserPoolAtFirstPositionInPools(pools, userLastUsedPool);
expect(result.length).toEqual(pools.length);
expect(result[0].poolId).toEqual(userLastUsedPool);
pools.forEach(pool =>
expect(result.some(sortedPool => sortedPool.poolId === pool.poolId)).toEqual(true),
);
},
);

it.each([
[[]],
[[stackPool("1")]],
[[stackPool("1"), stackPool("2")]],
[[stackPool("2"), stackPool("1")]],
])(
"should return the same pools when user last used pool was not found",
(pools: StakePool[]) => {
const userLastUsedPool = "3";
const result = putUserPoolAtFirstPositionInPools(pools, userLastUsedPool);
expect(result.length).toEqual(pools.length);
expect(result.every(resultPool => resultPool.poolId !== userLastUsedPool)).toEqual(true);
expect(
pools.every(pool => result.some(resultPool => resultPool.poolId === pool.poolId)),
).toEqual(true);
},
);

it("should fetch and sort pools correctly for an user", async () => {
const userLastUsedPool = "1";
const pools = [stackPool("2"), stackPool("3"), stackPool("1")];
jest
.spyOn(StakingFunctions, "fetchPoolDetails")
.mockReturnValue(Promise.resolve({ pools: pools }));

const result = await fetchAndSortPools({} as CryptoCurrency, ["1", "2", "3"], userLastUsedPool);
expect(result).toHaveLength(3);
expect(result[0].poolId).toEqual(userLastUsedPool);
expect(
result.every(resultPool => pools.some(pool => pool.poolId === resultPool.poolId)),
).toEqual(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { useCardanoFamilyPools } from "@ledgerhq/live-common/families/cardano/react";
import {
DEFAULT_SELECTED_POOL_ID,
LEDGER_POOL_IDS,
StakePool,
fetchPoolDetails,
} from "@ledgerhq/live-common/families/cardano/staking";
import { CardanoDelegation } from "@ledgerhq/live-common/families/cardano/types";
import { TransactionStatus } from "@ledgerhq/live-common/generated/types";
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import { Account } from "@ledgerhq/types-live";
import { TFunction } from "i18next";
import React, { useCallback, useEffect, useState } from "react";
Expand Down Expand Up @@ -38,29 +36,6 @@ export function concatUserAndLedgerPoolIds(
return userLastUsedPool ? [userLastUsedPool, ...ledgerPoolIds] : [...ledgerPoolIds];
}

export function putUserPoolAtFirstPositionInPools(
pools: StakePool[],
firstPoolId: string,
): StakePool[] {
const index = pools.findIndex(pool => pool.poolId === firstPoolId);
if (index === -1) {
return pools;
}

const pool = { ...pools[index] };
return [pool, ...pools.filter((_, i) => i !== index)];
}

export async function fetchAndSortPools(
currency: CryptoCurrency,
poolIds: string[],
userLastUsedPoolId: string,
) {
const response = await fetchPoolDetails(currency, poolIds);
const sortedPools = putUserPoolAtFirstPositionInPools(response.pools, userLastUsedPoolId);
return sortedPools;
}

const ValidatorField = ({ account, delegation, onChangeValidator, selectedPoolId }: Props) => {
const unit = useAccountUnit(account);
const [showAll, setShowAll] = useState(false);
Expand All @@ -74,13 +49,11 @@ const ValidatorField = ({ account, delegation, onChangeValidator, selectedPoolId

useEffect(() => {
setUserAndLedgerPoolsLoading(true);
fetchAndSortPools(account.currency, userAndLedgerPoolIds, DEFAULT_SELECTED_POOL_ID).then(
(sortedPools: StakePool[]) => {
setUserAndLedgerPools(sortedPools);
onChangeValidator(sortedPools[0]);
setUserAndLedgerPoolsLoading(false);
},
);
fetchPoolDetails(account.currency, userAndLedgerPoolIds).then(poolDetails => {
setUserAndLedgerPools(poolDetails.pools);
onChangeValidator(poolDetails.pools[0]);
setUserAndLedgerPoolsLoading(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand Down
161 changes: 161 additions & 0 deletions libs/coin-modules/coin-cardano/src/api/getPools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { fetchPoolDetails } from "./getPools";
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import network from "@ledgerhq/live-network/network";

jest.mock("@ledgerhq/live-network/network");

const poolIds = ["pool1", "pool2", "pool3"];
const currency = {
id: "cardano",
family: "cardano",
coinType: 1815,
name: "Cardano",
managerAppName: "Cardano",
ticker: "ADA",
scheme: "cardano",
units: [
{ name: "ada", code: "ADA", magnitude: 6 },
{ name: "Lovelace", code: "Lovelace", magnitude: 0 },
],
} as CryptoCurrency;

describe("fetchPoolDetails with sorted data", () => {
it("success with 200", async () => {
(network as jest.Mock).mockImplementation(() => ({
data: {
pools: [
{
poolId: "pool1",
name: "Ledger by Figment 1",
ticker: "LBF4",
liveStake: "12345",
},
{
poolId: "pool2",
name: "Ledger by Figment 2",
ticker: "LBF2",
liveStake: "123",
},
{
poolId: "pool3",
name: "Ledger by Figment 3",
ticker: "LBF1",
liveStake: "1234",
},
],
},
}));

const result = await fetchPoolDetails(currency, poolIds);

expect(result.pools).toStrictEqual([
{
poolId: "pool2",
name: "Ledger by Figment 2",
ticker: "LBF2",
liveStake: "123",
},
{
poolId: "pool3",
name: "Ledger by Figment 3",
ticker: "LBF1",
liveStake: "1234",
},
{
poolId: "pool1",
name: "Ledger by Figment 1",
ticker: "LBF4",
liveStake: "12345",
},
]);
});

it("returns empty pools array when API returns no pools", async () => {
(network as jest.Mock).mockImplementation(() => ({
data: { pools: [] },
}));

const result = await fetchPoolDetails(currency, poolIds);

expect(result.pools).toStrictEqual([]);
});

it("sorts pools with equal liveStake values in stable order (preserves original order)", async () => {
const poolsWithEqualStake = {
pools: [
{ poolId: "first", name: "First", ticker: "FST", liveStake: "1000" },
{ poolId: "second", name: "Second", ticker: "SND", liveStake: "1000" },
{ poolId: "third", name: "Third", ticker: "TRD", liveStake: "1000" },
],
};
(network as jest.Mock).mockImplementation(() => ({
data: poolsWithEqualStake,
}));

const result = await fetchPoolDetails(currency, ["first", "second", "third"]);

expect(result.pools).toStrictEqual([
{ poolId: "first", name: "First", ticker: "FST", liveStake: "1000" },
{ poolId: "second", name: "Second", ticker: "SND", liveStake: "1000" },
{ poolId: "third", name: "Third", ticker: "TRD", liveStake: "1000" },
]);
});

it("sorts pools with very large liveStake values correctly", async () => {
const poolsWithLargeStake = {
pools: [
{
poolId: "huge",
name: "Huge Pool",
ticker: "HUG",
liveStake: "999999999999999999999999",
},
{
poolId: "small",
name: "Small Pool",
ticker: "SML",
liveStake: "1000000000000000000",
},
{
poolId: "medium",
name: "Medium Pool",
ticker: "MED",
liveStake: "500000000000000000000000",
},
],
};
(network as jest.Mock).mockImplementation(() => ({
data: poolsWithLargeStake,
}));

const result = await fetchPoolDetails(currency, ["huge", "small", "medium"]);

expect(result.pools).toStrictEqual([
{
poolId: "small",
name: "Small Pool",
ticker: "SML",
liveStake: "1000000000000000000",
},
{
poolId: "medium",
name: "Medium Pool",
ticker: "MED",
liveStake: "500000000000000000000000",
},
{
poolId: "huge",
name: "Huge Pool",
ticker: "HUG",
liveStake: "999999999999999999999999",
},
]);
});

it("propagates error when network call fails", async () => {
const networkError = new Error("Network request failed");
(network as jest.Mock).mockRejectedValue(networkError);

await expect(fetchPoolDetails(currency, poolIds)).rejects.toThrow("Network request failed");
});
});
16 changes: 15 additions & 1 deletion libs/coin-modules/coin-cardano/src/api/getPools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import network from "@ledgerhq/live-network/network";
import BigNumber from "bignumber.js";
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import { CARDANO_API_ENDPOINT, CARDANO_TESTNET_API_ENDPOINT } from "../constants";
import { isTestnet } from "../logic";
Expand Down Expand Up @@ -31,5 +32,18 @@ export async function fetchPoolDetails(
: `${CARDANO_API_ENDPOINT}/v1/pool/detail`,
params: { poolIds },
});
return data;

const sortedPools = [...data.pools].sort((a, b) => {
const stakeA = new BigNumber(a.liveStake);
const stakeB = new BigNumber(b.liveStake);
if (stakeA.isLessThan(stakeB)) {
return -1;
}
if (stakeA.isGreaterThan(stakeB)) {
return 1;
}
return 0;
});

return { ...data, pools: sortedPools };
}
Loading