Skip to content

Commit

Permalink
[LIVE-14648] Feature - Prevent ERC-1155 calls for Blockscout explorer…
Browse files Browse the repository at this point in the history
… & authorize node-only sync (#8899)

* Add `none` explorer option to sync with only node

* Prevent unsupported ERC1155 calls for blockscout

* changeset
  • Loading branch information
lambertkevin authored Jan 17, 2025
1 parent 6de2466 commit c62cec9
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/clean-moose-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/coin-evm": minor
---

Prevent request on blockscout for ERC1155 NFTs as unsupported yet and add a `none` option for explorer's config to allow for node-only synchronization (will make token accounts disappear though)
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ export const initMswHandlers = (currencyConfig: EvmConfigInfo) => {
return HttpResponse.json(response);
}),
);
} else {
} else if (currencyConfig.explorer.type !== "none") {
handlers.push(
http.get(currencyConfig.explorer.uri, async ({ request }) => {
const uri = new URL(request.url).searchParams;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { AssertionError, fail } from "assert";
import BigNumber from "bignumber.js";
import { getEnv } from "@ledgerhq/live-env";
import { TokenAccount } from "@ledgerhq/types-live";
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
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 { UnknownExplorer, UnknownNode } from "../../errors";
import * as synchronization from "../../synchronization";
import * as noneExplorer from "../../api/explorer/none";
import * as nodeApi from "../../api/node/rpc.common";
import { createSwapHistoryMap } from "../../logic";
import {
account,
coinOperations,
Expand All @@ -23,10 +26,8 @@ import {
internalOperations,
swapHistory,
} from "../fixtures/synchronization.fixtures";
import { UnknownNode } from "../../errors";
import * as logic from "../../logic";
import { getCoinConfig } from "../../config";
import { createSwapHistoryMap } from "../../logic";
import * as logic from "../../logic";

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

it("should throw for currency with unsupported explorer", async () => {
mockGetConfig.mockImplementationOnce((): any => {
mockGetConfig.mockImplementation((): any => {
return {
info: {
node: {
type: "external",
uri: "https://my-rpc.com",
},
explorer: {
uri: "http://nope.com",
type: "unsupported" as any,
Expand All @@ -131,12 +136,54 @@ describe("EVM Family", () => {
if (e instanceof AssertionError) {
throw e;
}
expect(e).toBeInstanceOf(UnknownNode);
expect(e).toBeInstanceOf(UnknownExplorer);
}
});

it("shouldn't throw for none explorer config", async () => {
mockGetConfig.mockImplementation((): any => {
return {
info: {
node: {
type: "external",
uri: "https://my-rpc.com",
},
explorer: {
type: "none",
},
},
};
});
const spy = jest.spyOn(noneExplorer?.default, "getLastOperations");

await synchronization.getAccountShape(
{
...getAccountShapeParameters,
currency: {
...currency,
ethereumLikeInfo: {
chainId: 1,
} as any,
},
},
{} as any,
);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveReturnedWith(
Promise.resolve({
lastCoinOperations: [],
lastTokenOperations: [],
lastNftOperations: [],
lastInternalOperations: [],
}),
);
});

describe("With no transactions fetched", () => {
beforeAll(() => {
// @ts-expect-error reseting cache
etherscanAPI?.default.getLastOperations.reset();
jest.spyOn(etherscanAPI, "getLastOperations").mockImplementation(() =>
Promise.resolve({
lastCoinOperations: [],
Expand Down Expand Up @@ -265,6 +312,8 @@ describe("EVM Family", () => {

describe("With transactions fetched", () => {
beforeAll(() => {
// @ts-expect-error reseting cache
etherscanAPI?.default.getLastOperations.reset();
jest
.spyOn(etherscanAPI, "getLastCoinOperations")
.mockImplementation(() =>
Expand Down Expand Up @@ -524,6 +573,84 @@ describe("EVM Family", () => {
expect(accountShape.operations).toEqual([coinOperations[0]]);
});
});

describe("With Blockscout", () => {
beforeAll(() => {
// @ts-expect-error reseting cache
etherscanAPI?.default.getLastOperations.reset();
jest
.spyOn(etherscanAPI, "getLastCoinOperations")
.mockImplementation(() =>
Promise.resolve([{ ...coinOperations[0] }, { ...coinOperations[1] }]),
);
jest
.spyOn(etherscanAPI, "getLastTokenOperations")
.mockImplementation(() =>
Promise.resolve([{ ...tokenOperations[0] }, { ...tokenOperations[1] }]),
);

jest
.spyOn(etherscanAPI, "getLastERC721Operations")
.mockImplementation(() =>
Promise.resolve([
{ ...erc721Operations[0] },
{ ...erc721Operations[1] },
{ ...erc721Operations[2] },
]),
);
jest
.spyOn(etherscanAPI, "getLastInternalOperations")
.mockImplementation(() =>
Promise.resolve([
{ ...internalOperations[0] },
{ ...internalOperations[1] },
{ ...internalOperations[2] },
]),
);
jest
.spyOn(nodeApi, "getTokenBalance")
.mockImplementation(async (a, b, contractAddress) => {
if (contractAddress === tokenCurrencies[0].contractAddress) {
return new BigNumber(10000);
}
throw new Error("Shouldn't be trying to fetch this token balance");
});
});

afterAll(() => {
jest.restoreAllMocks();
});

it("should never call ERC1155 endpoint", async () => {
mockGetConfig.mockImplementation((): any => {
return {
info: {
node: {
type: "external",
uri: "https://my-rpc.com",
},
explorer: {
type: "blockscout",
uri: "https://api.com",
},
},
};
});
console.log(etherscanAPI?.default.getLastOperations);
const spy = jest.spyOn(etherscanAPI, "getLastERC1155Operations");

await synchronization.getAccountShape(
{
...getAccountShapeParameters,
initialAccount: account,
},
{} as any,
);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveReturnedWith(Promise.resolve([]));
});
});
});

describe("getSubAccounts", () => {
Expand Down
5 changes: 5 additions & 0 deletions libs/coin-modules/coin-evm/src/api/explorer/etherscan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ export const getLastERC1155Operations = async (
throw new EtherscanLikeExplorerUsedIncorrectly();
}

// Blockscout has no ERC1155 support yet
if (explorer.type === "blockscout") {
return [];
}

const ops = await fetchWithRetries<EtherscanERC1155Event[]>({
method: "GET",
url: `${explorer.uri}?module=account&action=token1155tx&address=${address}`,
Expand Down
3 changes: 3 additions & 0 deletions libs/coin-modules/coin-evm/src/api/explorer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getCoinConfig } from "../../config";
import etherscanLikeApi from "./etherscan";
import ledgerExplorerApi from "./ledger";
import { ExplorerApi } from "./types";
import noExplorerAPI from "./none";

/**
* Switch to select one of the compatible explorer
Expand All @@ -20,6 +21,8 @@ export const getExplorerApi = (currency: CryptoCurrency): ExplorerApi => {
return etherscanLikeApi;
case "ledger":
return ledgerExplorerApi;
case "none":
return noExplorerAPI;

default:
throw new UnknownExplorer(`Unknown explorer for currency: ${currency.id}`);
Expand Down
19 changes: 19 additions & 0 deletions libs/coin-modules/coin-evm/src/api/explorer/none.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ExplorerApi } from "./types";

/**
* Returns all operation types from an address
*/
export const getLastOperations: ExplorerApi["getLastOperations"] = async () => {
return {
lastCoinOperations: [],
lastTokenOperations: [],
lastNftOperations: [],
lastInternalOperations: [],
};
};

const noExplorerAPI: ExplorerApi = {
getLastOperations,
};

export default noExplorerAPI;
5 changes: 5 additions & 0 deletions libs/coin-modules/coin-evm/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ type EvmConfig = {
| {
type: "ledger";
explorerId: LedgerExplorerId;
}
| {
type: "none";
uri?: never;
explorerId?: never;
};
gasTracker?: {
type: "ledger";
Expand Down

0 comments on commit c62cec9

Please sign in to comment.