Skip to content

Commit

Permalink
bugfix: LIVE-12085 keep swap history when hard sync subAccounts (#6752)
Browse files Browse the repository at this point in the history
* bugfix: LIVE-12085 keep swap histo

* chore: changeset

* chore: tests

* Update libs/coin-modules/coin-evm/src/__tests__/unit/logic.unit.test.ts

Co-authored-by: 0xkvn <[email protected]>

* chore: refactor and tests

* chore: lint

* chore: adding history tests to synchro

* chore: lint

* Update libs/coin-modules/coin-evm/src/__tests__/unit/synchronization.unit.test.ts

Co-authored-by: 0xkvn <[email protected]>

* Update libs/coin-modules/coin-evm/src/logic.ts

Co-authored-by: 0xkvn <[email protected]>

* chore: lint

---------

Co-authored-by: 0xkvn <[email protected]>
  • Loading branch information
CremaFR and lambertkevin committed Apr 30, 2024
1 parent e05bf77 commit 434262d
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/odd-pumas-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/coin-evm": patch
---

bugfix, keep swap history for token when deep cleaning
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,27 @@ mockGetConfig.mockImplementation((): any => {
};
});

export const swapHistory = [
{
status: "pending",
provider: "moonpay",
operationId: "js:2:ethereum:0xkvn:+ethereum%2Ferc20%2Fusd__coin-OUT",
swapId: "swap1",
receiverAccountId: "js:2:ethereum:0xkvn:",
fromAmount: new BigNumber("200000"),
toAmount: new BigNumber("129430000"),
},
];

export const tokenCurrencies = [
Object.freeze(getTokenById("ethereum/erc20/usd__coin")),
Object.freeze(getTokenById("ethereum/erc20/usd_tether__erc20_")),
];

export const tokenAccount = makeTokenAccount("0xkvn", tokenCurrencies[0]);
export const tokenAccount = {
...makeTokenAccount("0xkvn", tokenCurrencies[0]),
swapHistory,
};
export const account = Object.freeze({
...makeAccount("0xkvn", currency, [tokenAccount]),
syncHash: logic.getSyncHash(currency),
Expand Down
74 changes: 74 additions & 0 deletions libs/coin-modules/coin-evm/src/__tests__/unit/logic.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as RPC_API from "../../api/node/rpc.common";
import { getCoinConfig } from "../../config";
import {
attachOperations,
createSwapHistoryMap,
eip1559TransactionHasFees,
getAdditionalLayer2Fees,
getDefaultFeeUnit,
Expand Down Expand Up @@ -446,6 +447,79 @@ describe("EVM Family", () => {
});
});

describe("createSwapHistoryMap", () => {
it("returns an empty map if initialAccount is undefined", () => {
const swapHistory = createSwapHistoryMap(undefined);
expect(swapHistory.size).toBe(0);
});
it("returns an empty map if there are no subAccounts", () => {
const account = makeAccount("0xCrema", getCryptoCurrencyById("ethereum"), []);
const swapHistory = createSwapHistoryMap(account);
expect(swapHistory.size).toBe(0);
});

it("maps TokenAccounts to their swapHistory", () => {
const tokenAccount1 = {
...makeTokenAccount("0xCrema1", getTokenById("ethereum/erc20/usd__coin")),
swapHistory: [
{
status: "pending",
provider: "moonpay",
operationId: "js:2:ethereum:0xkvn:+ethereum%2Ferc20%2Fusd__coin-OUT",
swapId: "swap1",
receiverAccountId: "js:2:ethereum:0xkvn:",
fromAmount: new BigNumber("200000"),
toAmount: new BigNumber("129430000"),
},
],
};
const tokenAccount2 = {
...makeTokenAccount("0xCrema2", getTokenById("ethereum/erc20/weth")),
swapHistory: [
{
status: "pending",
provider: "moonpay",
operationId: "js:2:ethereum:0xkvn:+ethereum%2Ferc20%2Fweth-OUT",
swapId: "swap2",
receiverAccountId: "js:2:ethereum:0xkvn:",
fromAmount: new BigNumber("200000"),
toAmount: new BigNumber("129430000"),
},
],
};

const account = makeAccount("0xCrema", getCryptoCurrencyById("ethereum"), [
tokenAccount1,
tokenAccount2,
]);
const swapHistory = createSwapHistoryMap(account);

expect(swapHistory.size).toBe(2);
expect(swapHistory.get(tokenAccount1.token)).toEqual(tokenAccount1.swapHistory);
expect(swapHistory.get(tokenAccount2.token)).toEqual(tokenAccount2.swapHistory);
});
it("should include correct swapHistory for a token account", () => {
const tokenAccount = {
...makeTokenAccount("0xCrema", getTokenById("ethereum/erc20/usd__coin")),
swapHistory: [
{
status: "pending",
provider: "moonpay",
operationId: "js:2:ethereum:0xkvn:+ethereum%2Ferc20%2Fusd__coin-OUT",
swapId: "6342cd15-5aa9-4c8c-9fb3-0b67e9b0714a",
receiverAccountId: "js:2:ethereum:0xkvn:",
tokenId: "ethereum/erc20/usd__coin",
fromAmount: new BigNumber("200000"),
toAmount: new BigNumber("129430000"),
},
],
};
const account = makeAccount("0xCrema", getCryptoCurrencyById("ethereum"), [tokenAccount]);

const swapHistoryMap = createSwapHistoryMap(account);
expect(swapHistoryMap.get(tokenAccount.token)).toEqual(tokenAccount.swapHistory);
});
});
describe("getSyncHash", () => {
const currency = getCryptoCurrencyById("ethereum");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import BigNumber from "bignumber.js";
import { getEnv } from "@ledgerhq/live-env";
import { decodeAccountId } from "@ledgerhq/coin-framework/account/accountId";
import { AccountShapeInfo } from "@ledgerhq/coin-framework/bridge/jsHelpers";
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
import { TokenAccount } from "@ledgerhq/types-live";
import { makeTokenAccount } from "../fixtures/common.fixtures";
import * as etherscanAPI from "../../api/explorer/etherscan";
import * as synchronization from "../../synchronization";
Expand All @@ -19,10 +21,12 @@ import {
tokenCurrencies,
tokenOperations,
internalOperations,
swapHistory,
} from "../fixtures/synchronization.fixtures";
import { UnknownNode } from "../../errors";
import * as logic from "../../logic";
import { getCoinConfig } from "../../config";
import { createSwapHistoryMap } from "../../logic";

jest.mock("../../api/node/rpc.common");
jest.useFakeTimers().setSystemTime(new Date("2014-04-21"));
Expand Down Expand Up @@ -540,13 +544,16 @@ describe("EVM Family", () => {
});

it("should return the right subAccounts", async () => {
const swapHistoryMap = createSwapHistoryMap(account);
const tokenAccounts = await synchronization.getSubAccounts(
{
...getAccountShapeParameters,
initialAccount: account,
},
account.id,
[tokenOperations[0], tokenOperations[1], tokenOperations[3]],
undefined,
swapHistoryMap,
);

const expectedUsdcAccount = {
Expand All @@ -556,7 +563,7 @@ describe("EVM Family", () => {
operations: [tokenOperations[0], tokenOperations[1]],
operationsCount: 2,
starred: undefined,
swapHistory: [],
swapHistory,
};
const expectedUsdtAccount = {
...makeTokenAccount(account.freshAddress, tokenCurrencies[1]),
Expand All @@ -572,6 +579,7 @@ describe("EVM Family", () => {
});

it("should return filtered subAccounts from blacklistedTokenIds", async () => {
const swapHistoryMap = new Map<TokenCurrency, TokenAccount["swapHistory"]>();
const tokenAccounts = await synchronization.getSubAccounts(
{
...getAccountShapeParameters,
Expand All @@ -580,6 +588,7 @@ describe("EVM Family", () => {
account.id,
[tokenOperations[0], tokenOperations[1], tokenOperations[3]],
[tokenCurrencies[0].id],
swapHistoryMap,
);

const expectedUsdtAccount = {
Expand Down Expand Up @@ -619,6 +628,7 @@ describe("EVM Family", () => {
account.id,
tokenCurrencies[0],
[tokenOperations[0], tokenOperations[1], tokenOperations[2]],
[],
);

expect(subAccount).toEqual({
Expand Down
20 changes: 19 additions & 1 deletion libs/coin-modules/coin-evm/src/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {
MessageProperties,
Operation,
SubAccount,
TokenAccount,
} from "@ledgerhq/types-live";
import murmurhash from "imurmurhash";
import { log } from "@ledgerhq/logs";
import { getEnv } from "@ledgerhq/live-env";
import { isNFTActive } from "@ledgerhq/coin-framework/nft/support";
import { CryptoCurrency, Unit } from "@ledgerhq/types-cryptoassets";
import { CryptoCurrency, TokenCurrency, Unit } from "@ledgerhq/types-cryptoassets";
import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
import { hashes as tokensHashesByChainId } from "@ledgerhq/cryptoassets/data/evm/index";
Expand Down Expand Up @@ -407,3 +408,20 @@ export const safeEncodeEIP55 = (addr: string): string => {
return addr;
}
};

/**
* Similar to mergeAccount but used to keep previous data we can't fetch on chain
*/
// logic.ts
export const createSwapHistoryMap = (
initialAccount: Account | undefined,
): Map<TokenCurrency, TokenAccount["swapHistory"]> => {
if (!initialAccount?.subAccounts) return new Map();

const swapHistoryMap = new Map<TokenCurrency, TokenAccount["swapHistory"]>();
for (const subAccount of initialAccount.subAccounts) {
swapHistoryMap.set(subAccount.token, subAccount.swapHistory);
}

return swapHistoryMap;
};
15 changes: 9 additions & 6 deletions libs/coin-modules/coin-evm/src/synchronization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import {
mergeOps,
mergeNfts,
} from "@ledgerhq/coin-framework/bridge/jsHelpers";
import { Account, Operation, SubAccount } from "@ledgerhq/types-live";
import { Account, Operation, SubAccount, TokenAccount } from "@ledgerhq/types-live";
import { decodeOperationId } from "@ledgerhq/coin-framework/operation";
import { nftsFromOperations } from "@ledgerhq/coin-framework/nft/helpers";
import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
import { attachOperations, getSyncHash, mergeSubAccounts } from "./logic";
import { attachOperations, getSyncHash, mergeSubAccounts, createSwapHistoryMap } from "./logic";
import { ExplorerApi } from "./api/explorer/types";
import { getExplorerApi } from "./api/explorer";
import { getNodeApi } from "./api/node/index";
Expand Down Expand Up @@ -75,17 +75,17 @@ export const getAccountShape: GetAccountShape = async (infos, { blacklistedToken
throw e;
}
})();

const swapHistoryMap = createSwapHistoryMap(initialAccount);
const newSubAccounts = await getSubAccounts(
infos,
accountId,
lastTokenOperations,
blacklistedTokenIds,
swapHistoryMap,
);
const subAccounts = shouldSyncFromScratch
? newSubAccounts
: mergeSubAccounts(initialAccount, newSubAccounts); // Merging potential new subAccouns while preserving the references

// Trying to confirm pending operations that we are sure of
// because they were made in the live
// Useful for integrations without explorers
Expand Down Expand Up @@ -144,6 +144,7 @@ export const getSubAccounts = async (
accountId: string,
lastTokenOperations: Operation[],
blacklistedTokenIds: string[] = [],
swapHistoryMap: Map<TokenCurrency, TokenAccount["swapHistory"]>,
): Promise<Partial<SubAccount>[]> => {
const { currency } = infos;

Expand All @@ -167,7 +168,8 @@ export const getSubAccounts = async (
// Fetching all TokenAccounts possible and providing already filtered operations
const subAccountsPromises: Promise<Partial<SubAccount>>[] = [];
for (const [token, ops] of erc20OperationsByToken.entries()) {
subAccountsPromises.push(getSubAccountShape(currency, accountId, token, ops));
const swapHistory = swapHistoryMap.get(token) || [];
subAccountsPromises.push(getSubAccountShape(currency, accountId, token, ops, swapHistory));
}

return Promise.all(subAccountsPromises);
Expand All @@ -181,6 +183,7 @@ export const getSubAccountShape = async (
parentId: string,
token: TokenCurrency,
operations: Operation[],
swapHistory: TokenAccount["swapHistory"],
): Promise<Partial<SubAccount>> => {
const nodeApi = getNodeApi(currency);
const { xpubOrAddress: address } = decodeAccountId(parentId);
Expand All @@ -199,7 +202,7 @@ export const getSubAccountShape = async (
operationsCount: operations.length,
pendingOperations: [],
balanceHistoryCache: emptyHistoryCache,
swapHistory: [],
swapHistory,
};
};

Expand Down

0 comments on commit 434262d

Please sign in to comment.