diff --git a/explorers-config.md b/explorers-config.md index 77bf1f4f5c..41832c6cdd 100644 --- a/explorers-config.md +++ b/explorers-config.md @@ -16,6 +16,7 @@ | Litecoin | LTC | https://explorers.api.live.ledger.com/blockchain/v3/ltc | N/A | | Peercoin | PPC | https://explorers.api.live.ledger.com/blockchain/v3/ppc | N/A | | PivX | PIVX | https://explorers.api.live.ledger.com/blockchain/v3/pivx | N/A | +| Polygon | MATIC | https://explorers.api.live.ledger.com/blockchain/v3/matic | N/A | | Qtum | QTUM | https://explorers.api.live.ledger.com/blockchain/v3/qtum | N/A | | Stakenet | XSN | https://explorers.api.live.ledger.com/blockchain/v3/xsn | N/A | | Vertcoin | VTC | https://explorers.api.live.ledger.com/blockchain/v3/vtc | N/A | diff --git a/src/__tests__/test-helpers/setup.ts b/src/__tests__/test-helpers/setup.ts index 103710577d..7c272051b3 100644 --- a/src/__tests__/test-helpers/setup.ts +++ b/src/__tests__/test-helpers/setup.ts @@ -21,6 +21,7 @@ setSupportedCurrencies([ "bitcoin", "ethereum", "bsc", + "polygon", "elrond", "ripple", "bitcoin_cash", diff --git a/src/account/helpers.ts b/src/account/helpers.ts index e60e7f3d8b..3d07221349 100644 --- a/src/account/helpers.ts +++ b/src/account/helpers.ts @@ -150,6 +150,7 @@ export function clearAccount(account: T): T { copy.bitcoinResources = initialBitcoinResourcesValue; } delete copy.balanceHistory; + delete copy.nfts; return copy as T; } export function findSubAccountById( diff --git a/src/api/Ethereum.ts b/src/api/Ethereum.ts index 8e9862c7d7..4d4f943b98 100644 --- a/src/api/Ethereum.ts +++ b/src/api/Ethereum.ts @@ -10,6 +10,7 @@ import { blockchainBaseURL } from "./Ledger"; import { FeeEstimationFailed } from "../errors"; import { makeLRUCache } from "../cache"; import { getEnv } from "../env"; +import { isNFTActive } from "../nft/support"; export type Block = { height: BigNumber; @@ -132,10 +133,9 @@ export const apiForCurrency = (currency: CryptoCurrency): API => { let { data } = await network({ method: "GET", url: URL.format({ - pathname: - getEnv("NFT") && currency.ticker === "ETH" - ? `https://explorers.api-01.live.ledger-stg.com/blockchain/v3/eth/addresses/${address}/transactions` - : `${baseURL}/addresses/${address}/transactions`, + pathname: isNFTActive(currency) + ? `https://explorers.api-01.live.ledger-stg.com/blockchain/v3/eth/addresses/${address}/transactions` + : `${baseURL}/addresses/${address}/transactions`, query: { batch_size, noinput: true, diff --git a/src/env.ts b/src/env.ts index 2b0dff96a5..071710b63a 100644 --- a/src/env.ts +++ b/src/env.ts @@ -381,6 +381,11 @@ const envDefinitions = { parser: boolParser, desc: "synchronizing nfts", }, + NFT_CURRENCIES: { + def: "ethereum", + parser: stringParser, + desc: "set the currencies where NFT is active", + }, NFT_ETH_METADATA_SERVICE: { // FIXME LL-8001 def: "https://nft.staging.aws.ledger.fr/v1/ethereum/1/contracts/tokens/infos", diff --git a/src/families/ethereum/nft.test.ts b/src/families/ethereum/nft.test.ts index 10a36763fc..8ae040af39 100644 --- a/src/families/ethereum/nft.test.ts +++ b/src/families/ethereum/nft.test.ts @@ -7,6 +7,8 @@ import { makeBridgeCacheSystem } from "../../bridge/cache"; import { patchAccount } from "../../reconciliation"; import { setEnv } from "../../env"; +jest.setTimeout(120000); + const gaspardAccount: AccountRaw = { id: "js:1:ethereum:0xb98d10d9f6d07ba283bfd21b2dfec050f9ae282a:", seedIdentifier: "0xb98d10d9f6d07ba283bfd21b2dfec050f9ae282a", @@ -26,32 +28,74 @@ const gaspardAccount: AccountRaw = { xpub: "", }; -describe("nft reconciliation", () => { - let account = fromAccountRaw(gaspardAccount); - const localCache = {}; - const cache = makeBridgeCacheSystem({ - saveData(c, d) { - localCache[c.id] = d; - return Promise.resolve(); - }, - getData(c) { - return Promise.resolve(localCache[c.id]); - }, - }); +/* +const gaspardPolygonAccount: AccountRaw = { + id: "js:1:polygon:0xb98d10d9f6d07ba283bfd21b2dfec050f9ae282a:", + seedIdentifier: "0xb98d10d9f6d07ba283bfd21b2dfec050f9ae282a", + name: "G4sp4rd", + derivationMode: "", + index: 0, + freshAddress: "0xb98d10d9f6d07ba283bfd21b2dfec050f9ae282a", + freshAddressPath: "44'/60'/0'/0/0", + freshAddresses: [], + pendingOperations: [], + operations: [], + currencyId: "polygon", + unitMagnitude: 18, + balance: "", + blockHeight: 0, + lastSyncDate: "", + xpub: "", +}; +*/ + +const gaspardBscAccount: AccountRaw = { + id: "js:1:bsc:0xb98d10d9f6d07ba283bfd21b2dfec050f9ae282a:", + seedIdentifier: "0xb98d10d9f6d07ba283bfd21b2dfec050f9ae282a", + name: "G4sp4rd", + derivationMode: "", + index: 0, + freshAddress: "0xb98d10d9f6d07ba283bfd21b2dfec050f9ae282a", + freshAddressPath: "44'/60'/0'/0/0", + freshAddresses: [], + pendingOperations: [], + operations: [], + currencyId: "bsc", + unitMagnitude: 18, + balance: "", + blockHeight: 0, + lastSyncDate: "", + xpub: "", +}; + +async function sync(account, withNFT = true) { const bridge = getAccountBridge(account); - async function sync(account) { - const blacklistedTokenIds = []; - setEnv("NFT", true); - const r = await bridge - .sync(account, { - paginationConfig: {}, - blacklistedTokenIds, - }) - .pipe(reduce((a, f: (arg0: Account) => Account) => f(a), account)) - .toPromise(); - setEnv("NFT", false); - return r; - } + const blacklistedTokenIds = []; + setEnv("NFT", withNFT); + const r = await bridge + .sync(account, { + paginationConfig: {}, + blacklistedTokenIds, + }) + .pipe(reduce((a, f: (arg0: Account) => Account) => f(a), account)) + .toPromise(); + setEnv("NFT", false); + return r; +} + +const localCache = {}; +const cache = makeBridgeCacheSystem({ + saveData(c, d) { + localCache[c.id] = d; + return Promise.resolve(); + }, + getData(c) { + return Promise.resolve(localCache[c.id]); + }, +}); + +describe("gaspard NFT on ethereum", () => { + let account = fromAccountRaw(gaspardAccount); test("first sync & have nfts", async () => { await cache.prepareCurrency(account.currency); @@ -87,4 +131,43 @@ describe("nft reconciliation", () => { const newAccount = patchAccount(copy, toAccountRaw(account)); expect(newAccount.nfts).toEqual(account.nfts); }); + + test("start account with .nfts and disable the NFT flag should make it disappear", async () => { + expect(account.nfts).not.toBeFalsy(); + const resync = await sync(account, false); + expect(resync.nfts).toBeFalsy(); + }); + + test("start account without .nfts and enable the NFT flag should make it appear", async () => { + const first = await sync(account, false); + expect(first.nfts).toBeFalsy(); + const second = await sync(first, true); + expect(second.nfts).not.toBeFalsy(); + expect(second.nfts).toEqual(account.nfts); + }); +}); + +/* +// this is never ending here... have to disable this test... (backend issue) +describe("gaspard NFT on polygon", () => { + let account = fromAccountRaw(gaspardPolygonAccount); + test("first sync", async () => { + await cache.prepareCurrency(account.currency); + account = await sync(account); + }); + test(".nfts shouldn't be visible", () => { + expect(account.nfts).toBeFalsy(); + }); +}); +*/ + +describe("gaspard NFT on bsc", () => { + let account = fromAccountRaw(gaspardBscAccount); + test("first sync", async () => { + await cache.prepareCurrency(account.currency); + account = await sync(account); + }); + test(".nfts shouldn't be visible", () => { + expect(account.nfts).toBeFalsy(); + }); }); diff --git a/src/families/ethereum/signOperation.ts b/src/families/ethereum/signOperation.ts index 623a500ab8..e98ba5e5c3 100644 --- a/src/families/ethereum/signOperation.ts +++ b/src/families/ethereum/signOperation.ts @@ -13,7 +13,7 @@ import { getGasLimit, buildEthereumTx } from "./transaction"; import { apiForCurrency } from "../../api/Ethereum"; import { withDevice } from "../../hw/deviceAccess"; import { modes } from "./modules"; -import { getEnv } from "../../env"; +import { isNFTActive } from "../../nft"; export const signOperation = ({ account, deviceId, @@ -61,7 +61,7 @@ export const signOperation = ({ "0x" + (tx.value.toString("hex") || "0") ); const eth = new Eth(transport); - if (getEnv("NFT")) { + if (isNFTActive(account.currency)) { eth.setLoadConfig({ // FIXME drop this after LL-8001 nftExplorerBaseURL: diff --git a/src/families/ethereum/synchronisation.ts b/src/families/ethereum/synchronisation.ts index ee21fbff57..34f9d8a6a2 100644 --- a/src/families/ethereum/synchronisation.ts +++ b/src/families/ethereum/synchronisation.ts @@ -19,9 +19,8 @@ import type { Operation, TokenAccount, Account, NFT } from "../../types"; import { API, apiForCurrency, Tx } from "../../api/Ethereum"; import { digestTokenAccounts, prepareTokenAccounts } from "./modules"; import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets"; -import { encodeNftId, nftsFromOperations } from "../../nft"; +import { encodeNftId, isNFTActive, nftsFromOperations } from "../../nft"; import { NFT_VERSION } from "./versions"; -import { getEnv } from "../../env"; export const getAccountShape: GetAccountShape = async ( infoInput, @@ -41,24 +40,24 @@ export const getAccountShape: GetAccountShape = async ( const initialStableOperations = initialAccount ? stableOperations(initialAccount) : []; + const shouldNFTBeActive = isNFTActive(currency); // fetch transactions, incrementally if possible const mostRecentStableOperation = initialStableOperations[0]; // when new tokens are added / blacklist changes, we need to sync again because we need to go through all operations again const syncHash = JSON.stringify(blacklistedTokenIds || []) + "_" + + shouldNFTBeActive + + "_" + listTokensForCryptoCurrency(currency, { withDelisted: true, }).length; - const outdatedBlacklist = initialAccount?.syncHash !== syncHash; - const firstNftSync = - getEnv("NFT") && typeof initialAccount?.nfts === "undefined"; + const outdatedSyncHash = initialAccount?.syncHash !== syncHash; const pullFromBlockHash = initialAccount && areAllOperationsLoaded(initialAccount) && mostRecentStableOperation && - !outdatedBlacklist && - !firstNftSync && + !outdatedSyncHash && NFT_VERSION ? mostRecentStableOperation.blockHash : undefined; @@ -173,7 +172,7 @@ export const getAccountShape: GetAccountShape = async ( })); const operations = mergeOps(initialStableOperations, newOps); - const nfts = getEnv("NFT") + const nfts = isNFTActive(currency) ? mergeNfts(initialAccount?.nfts, await getNfts(flatNftOps)) : undefined; @@ -334,91 +333,23 @@ const txToOps = }); // Creating NFTOps from transfer events related to ERC721 only - const erc721Operations = !erc721_transfer_events - ? [] - : flatMap(erc721_transfer_events, (event) => { - const sender = safeEncodeEIP55(event.sender); - const receiver = safeEncodeEIP55(event.receiver); - const contract = safeEncodeEIP55(event.contract); - const tokenId = event.token_id; - const nftId = encodeNftId(id, event.contract, tokenId); - const sending = addr === sender; - const receiving = addr === receiver; - - if (!sending && !receiving) { - return []; - } - - const all: Operation[] = []; - - if (sending) { - const type = "NFT_OUT"; - all.push({ - id: `${nftId}-${hash}-${type}`, - senders: [sender], - recipients: [receiver], - contract, - fee, - standard: "ERC721", - tokenId, - value: new BigNumber(1), - hash, - type, - blockHeight, - blockHash, - date, - transactionSequenceNumber, - accountId: id, - extra: {}, - }); - } - - if (receiving) { - const type = "NFT_IN"; - all.push({ - id: `${nftId}-${hash}-${type}`, - senders: [sender], - recipients: [receiver], - contract, - fee, - standard: "ERC721", - tokenId, - value: new BigNumber(1), - hash, - type, - blockHeight, - blockHash, - date, - transactionSequenceNumber, - accountId: id, - extra: {}, - }); - } - - return all; - }); - - // Creating NFTOps from transfer events related to ERC1155 only - const erc1155Operations = !erc1155_transfer_events - ? [] - : flatMap(erc1155_transfer_events, (event) => { - const sender = safeEncodeEIP55(event.sender); - const receiver = safeEncodeEIP55(event.receiver); - const contract = safeEncodeEIP55(event.contract); - const operator = safeEncodeEIP55(event.operator); - const sending = addr === sender; - const receiving = addr === receiver; - - if (!sending && !receiving) { - return []; - } + const erc721Operations = + !erc721_transfer_events || !isNFTActive(currency) + ? [] + : flatMap(erc721_transfer_events, (event) => { + const sender = safeEncodeEIP55(event.sender); + const receiver = safeEncodeEIP55(event.receiver); + const contract = safeEncodeEIP55(event.contract); + const tokenId = event.token_id; + const nftId = encodeNftId(id, event.contract, tokenId); + const sending = addr === sender; + const receiving = addr === receiver; - const all: Operation[] = []; + if (!sending && !receiving) { + return []; + } - event.transfers.forEach((transfer) => { - const tokenId = transfer.id; - const value = new BigNumber(transfer.value); - const nftId = encodeNftId(id, event.contract, tokenId); + const all: Operation[] = []; if (sending) { const type = "NFT_OUT"; @@ -428,10 +359,9 @@ const txToOps = recipients: [receiver], contract, fee, - operator, - standard: "ERC1155", + standard: "ERC721", tokenId, - value, + value: new BigNumber(1), hash, type, blockHeight, @@ -451,10 +381,9 @@ const txToOps = recipients: [receiver], contract, fee, - operator, - standard: "ERC1155", + standard: "ERC721", tokenId, - value, + value: new BigNumber(1), hash, type, blockHeight, @@ -465,15 +394,88 @@ const txToOps = extra: {}, }); } + + return all; }); - return all; - }); + // Creating NFTOps from transfer events related to ERC1155 only + const erc1155Operations = + !erc1155_transfer_events || !isNFTActive(currency) + ? [] + : flatMap(erc1155_transfer_events, (event) => { + const sender = safeEncodeEIP55(event.sender); + const receiver = safeEncodeEIP55(event.receiver); + const contract = safeEncodeEIP55(event.contract); + const operator = safeEncodeEIP55(event.operator); + const sending = addr === sender; + const receiving = addr === receiver; + + if (!sending && !receiving) { + return []; + } + + const all: Operation[] = []; + + event.transfers.forEach((transfer) => { + const tokenId = transfer.id; + const value = new BigNumber(transfer.value); + const nftId = encodeNftId(id, event.contract, tokenId); + + if (sending) { + const type = "NFT_OUT"; + all.push({ + id: `${nftId}-${hash}-${type}`, + senders: [sender], + recipients: [receiver], + contract, + fee, + operator, + standard: "ERC1155", + tokenId, + value, + hash, + type, + blockHeight, + blockHash, + date, + transactionSequenceNumber, + accountId: id, + extra: {}, + }); + } + + if (receiving) { + const type = "NFT_IN"; + all.push({ + id: `${nftId}-${hash}-${type}`, + senders: [sender], + recipients: [receiver], + contract, + fee, + operator, + standard: "ERC1155", + tokenId, + value, + hash, + type, + blockHeight, + blockHash, + date, + transactionSequenceNumber, + accountId: id, + extra: {}, + }); + } + }); + + return all; + }); const nftOperations = erc721Operations .concat(erc1155Operations) /** @warning is this necessary ? Do we need the operations to be chronologically organised for LLD/LLM ? */ .sort((a, b) => b.date.getTime() - a.date.getTime()); + const ops: Operation[] = []; if (sending) { diff --git a/src/nft/helpers.ts b/src/nft/helpers.ts index 53dd4d8a62..07fa1c88d2 100644 --- a/src/nft/helpers.ts +++ b/src/nft/helpers.ts @@ -1,6 +1,6 @@ import eip55 from "eip55"; import BigNumber from "bignumber.js"; -import { NFT, Operation, Transaction } from "../types"; +import { NFT, Operation } from "../types"; type Collection = NFT["collection"]; @@ -76,11 +76,3 @@ export const nftsByCollections = ( export const getNftKey = (contract: string, tokenId: string): string => { return `${contract}-${tokenId}`; }; - -export const isNftTransaction = (transaction: Transaction): boolean => { - if (transaction.family === "ethereum") { - return ["erc721.transfer", "erc1155.transfer"].includes(transaction.mode); - } - - return false; -}; diff --git a/src/nft/index.ts b/src/nft/index.ts index 06eb590c11..c312c9e37d 100644 --- a/src/nft/index.ts +++ b/src/nft/index.ts @@ -1,3 +1,4 @@ export * from "./nftId"; export * from "./helpers"; +export * from "./support"; export * from "./NftMetadataProvider"; diff --git a/src/nft/support.ts b/src/nft/support.ts new file mode 100644 index 0000000000..b5d81fb827 --- /dev/null +++ b/src/nft/support.ts @@ -0,0 +1,16 @@ +import { Transaction, CryptoCurrency } from "../types"; +import { getEnv } from "../env"; + +export const isNftTransaction = (transaction: Transaction): boolean => { + if (transaction.family === "ethereum") { + return ["erc721.transfer", "erc1155.transfer"].includes(transaction.mode); + } + + return false; +}; + +export function isNFTActive(currency: CryptoCurrency): boolean { + return ( + getEnv("NFT") && getEnv("NFT_CURRENCIES").split(",").includes(currency.id) + ); +}