diff --git a/cli/package.json b/cli/package.json index e4a6e10c29..324b62c388 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "ledger-live", - "version": "21.18.0", + "version": "21.19.1", "description": "ledger-live CLI version", "repository": { "type": "git", @@ -28,16 +28,16 @@ "@ledgerhq/hw-transport-node-ble": "5.7.0" }, "dependencies": { - "@ledgerhq/cryptoassets": "6.17.0", + "@ledgerhq/cryptoassets": "6.18.0", "@ledgerhq/errors": "6.10.0", - "@ledgerhq/hw-app-btc": "6.17.0", + "@ledgerhq/hw-app-btc": "6.17.1", "@ledgerhq/hw-transport-http": "6.15.0", "@ledgerhq/hw-transport-mocker": "6.11.2", "@ledgerhq/hw-transport-node-ble": "^6.15.0", "@ledgerhq/hw-transport-node-hid": "6.11.2", "@ledgerhq/hw-transport-node-speculos": "6.11.2", "@ledgerhq/ledger-core": "6.14.5", - "@ledgerhq/live-common": "^21.18.0", + "@ledgerhq/live-common": "^21.19.1", "@ledgerhq/logs": "6.10.0", "@walletconnect/client": "^1.6.6", "asciichart": "^1.5.25", diff --git a/cli/yarn.lock b/cli/yarn.lock index 315fab670f..02f357b46c 100644 --- a/cli/yarn.lock +++ b/cli/yarn.lock @@ -690,10 +690,10 @@ dependencies: commander "^2.20.0" -"@ledgerhq/cryptoassets@6.17.0", "@ledgerhq/cryptoassets@^6.17.0": - version "6.17.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/cryptoassets/-/cryptoassets-6.17.0.tgz#cc3963b0ed726efcba976b78185d7f72ea9bedbc" - integrity sha512-xvXmS0fCLBOr54/lBF4w4pfWVhmbEcf/Wf45k0Carx4vtSUzMChd/KyAvIuLODNeIBj9lXOK2OqXB0u8wc8p5A== +"@ledgerhq/cryptoassets@6.18.0", "@ledgerhq/cryptoassets@^6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/cryptoassets/-/cryptoassets-6.18.0.tgz#8cf1b581fe17f3018f60abeedee6b97d809dee72" + integrity sha512-OGJlWFMWp26y/ZgUQVFwyPGd9BHVvmdxO8eA7mNg5mLQgcO5KQVchXbkCTqf6P3beFJ9eapBO35f+ngbveuUaw== dependencies: invariant "2" @@ -739,10 +739,10 @@ js-sha512 "^0.8.0" tweetnacl "^1.0.3" -"@ledgerhq/hw-app-btc@6.17.0": - version "6.17.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-6.17.0.tgz#c08090dcd0ad78eb7a2a288735c3bcf8e914a423" - integrity sha512-3aFFL1oqn8wd4xccRlBpdDkmNo0K64wr1jg5leGhES5utRLflhTiJhBL3RlHPn4RD3EoFH9X9TuFzrIpJQlHeg== +"@ledgerhq/hw-app-btc@6.17.1": + version "6.17.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-6.17.1.tgz#4b54cf18c7e88b8baee7dd0fab5a191472f5760d" + integrity sha512-xyfYnKr6b+0DoQ6veiXJIBLGhz2YwDVs3bEMBY2EoZA9CbsvyQbnVxETFHNCRsRp/LWZJyr4Atvj0bT0wZCWQQ== dependencies: "@ledgerhq/hw-transport" "^6.11.2" "@ledgerhq/logs" "^6.10.0" @@ -765,12 +765,12 @@ "@ledgerhq/hw-transport" "^6.11.2" bip32-path "^0.4.2" -"@ledgerhq/hw-app-eth@6.17.0": - version "6.17.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-6.17.0.tgz#9f2f7ee18b0fe553277a08d007a8271ec94014ff" - integrity sha512-msZD5mU/Ck/2ODKCNPSyMDq3v8FBOI3R3QasDDm7WNfH5H8UVB+mobjExPjmlke+t1GsM864V2EU2/YiLGM/lg== +"@ledgerhq/hw-app-eth@6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-6.18.0.tgz#9af0c4d90a5e4805fdb0cd98c26d14f823cc2fda" + integrity sha512-QQGd0uIeY7LDDsg1AoaobdvlSs7ymG0BqxPqEgOH/YnOlixC/BsK4P5BQJSWWP5wJX3qYBwQ04f1AMzzM2FnmA== dependencies: - "@ledgerhq/cryptoassets" "^6.17.0" + "@ledgerhq/cryptoassets" "^6.18.0" "@ledgerhq/errors" "^6.10.0" "@ledgerhq/hw-transport" "^6.11.2" "@ledgerhq/logs" "^6.10.0" @@ -934,20 +934,20 @@ bignumber.js "^9.0.1" json-rpc-2.0 "^0.2.16" -"@ledgerhq/live-common@^21.18.0": - version "21.18.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-21.18.0.tgz#54d2b147cf1b2e5e31a4196418ccf5e9db4726a4" - integrity sha512-Jv0wnyUK6gLyThyQhpiKzohSiAIcrnFLOt0NMkMN2S2dsSU6+pb8Rl/GFM/odTfJ/0giK7Lw4AAlQEEwr3Thrg== +"@ledgerhq/live-common@^21.19.1": + version "21.19.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-21.19.1.tgz#a4b1fd5068b5e79735e91a5d29d44e3e6d673bf5" + integrity sha512-k5YjJUO+LorxkVffF0xsklhuxxoqXNwvVdvajwComMyP6wT73Tq1r6brPm8NFH9d6xS6qDDjknOyypnj3PhVow== dependencies: "@crypto-com/chain-jslib" "0.0.19" "@ledgerhq/compressjs" "1.3.2" - "@ledgerhq/cryptoassets" "6.17.0" + "@ledgerhq/cryptoassets" "6.18.0" "@ledgerhq/devices" "6.11.2" "@ledgerhq/errors" "6.10.0" "@ledgerhq/hw-app-algorand" "6.11.2" - "@ledgerhq/hw-app-btc" "6.17.0" + "@ledgerhq/hw-app-btc" "6.17.1" "@ledgerhq/hw-app-cosmos" "6.11.2" - "@ledgerhq/hw-app-eth" "6.17.0" + "@ledgerhq/hw-app-eth" "6.18.0" "@ledgerhq/hw-app-polkadot" "6.11.2" "@ledgerhq/hw-app-str" "6.11.2" "@ledgerhq/hw-app-tezos" "6.11.2" @@ -1955,13 +1955,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base-x@3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.7.tgz#1c5a7fafe8f66b4114063e8da102799d4e7c408f" - integrity sha512-zAKJGuQPihXW22fkrfOclUUZXM2g92z5GzlSMHxhO6r6Qj+Nm0ccaGNBzDZojzwOMkpjAv4J0fOv1U4go+a4iw== - dependencies: - safe-buffer "^5.0.1" - base-x@3.0.9, base-x@^3.0.2: version "3.0.9" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" @@ -4567,15 +4560,7 @@ ripemd160@2, ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.2: hash-base "^3.0.0" inherits "^2.0.1" -ripple-address-codec@^4.0.0, ripple-address-codec@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ripple-address-codec/-/ripple-address-codec-4.1.1.tgz#48e5d76b00b6b9752b1d376646d5abbcd3c8bd67" - integrity sha512-mcVD8f7+CH6XaBnLkRcmw4KyHMufa0HTJE3TYeaecwleIQASLYVenjQmVJLgmJQcDUS2Ldh/EltqktmiFMFgkg== - dependencies: - base-x "3.0.7" - create-hash "^1.1.2" - -ripple-address-codec@^4.2.0: +ripple-address-codec@^4.0.0, ripple-address-codec@^4.1.1, ripple-address-codec@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/ripple-address-codec/-/ripple-address-codec-4.2.0.tgz#dc3291394ff22f46d8aeac6ef30d51be1416d7c9" integrity sha512-9QhBNDiWjwj7l+WQ7H7klXF/VwxVj2Q0HRhd4vLCueTPoxUtaNQyfvUZFiXJrqxg0heM3/iWxupkq4TwrXgSuQ== @@ -4583,19 +4568,7 @@ ripple-address-codec@^4.2.0: base-x "3.0.9" create-hash "^1.1.2" -ripple-binary-codec@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.1.3.tgz#9dc6cd139fd587ec6fc2ffe72fc1f0ced53ca906" - integrity sha512-NnFNZZ+225BxdDdHtcEn4GiGzup+V0DGAbtKygZIwbqA5116oZBt6uY3g43gYpdDMISsEbM7NewBij8+7jdlvA== - dependencies: - assert "^2.0.0" - big-integer "^1.6.48" - buffer "5.6.0" - create-hash "^1.2.0" - decimal.js "^10.2.0" - ripple-address-codec "^4.1.1" - -ripple-binary-codec@^1.2.0: +ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.2.0.tgz#4058281c045fd6f9f8eb8be9402bbaf2d56aad8b" integrity sha512-XMRCbFXyG+dGp3x7tMs9IwA+FVWPPaGjdHYW2+g4Q/WQJqFp5MRED+jjOBOUafmrW4TUsOn1PEEdbB4ozWbDBw== diff --git a/explorers-config.md b/explorers-config.md index 41832c6cdd..e3059a2036 100644 --- a/explorers-config.md +++ b/explorers-config.md @@ -9,7 +9,7 @@ | Dash | DASH | https://explorers.api.live.ledger.com/blockchain/v3/dash | N/A | | Decred | DCR | https://explorers.api.live.ledger.com/blockchain/v3/dcr | N/A | | DigiByte | DGB | https://explorers.api.live.ledger.com/blockchain/v3/dgb | N/A | -| Dogecoin | DOGE | https://explorers.api.live.ledger.com/blockchain/v2/doge | https://explorers.api.live.ledger.com/blockchain/v3/doge | +| Dogecoin | DOGE | https://explorers.api.live.ledger.com/blockchain/v3/doge | N/A | | Ethereum | ETH | https://explorers.api.live.ledger.com/blockchain/v3/eth | N/A | | Ethereum Classic | ETC | https://explorers.api.live.ledger.com/blockchain/v3/etc | N/A | | Komodo | KMD | https://explorers.api.live.ledger.com/blockchain/v3/kmd | N/A | diff --git a/package.json b/package.json index a81d66811f..05e0087038 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "type": "git", "url": "https://github.com/LedgerHQ/ledger-live-common" }, - "version": "21.19.0", + "version": "21.19.1", "main": "lib/index.js", "types": "lib/index.d.ts", "license": "Apache-2.0", diff --git a/src/__tests__/__snapshots__/all.libcore.ts.snap b/src/__tests__/__snapshots__/all.libcore.ts.snap index 7bb97dde33..6e86dd3488 100644 --- a/src/__tests__/__snapshots__/all.libcore.ts.snap +++ b/src/__tests__/__snapshots__/all.libcore.ts.snap @@ -15177,7 +15177,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_6026", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_6026", "unitMagnitude": 18, "used": true, }, @@ -15218,7 +15218,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_6026", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_6026", "unitMagnitude": 18, "used": true, }, @@ -15698,7 +15698,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_6026", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_6026", "unitMagnitude": 18, "used": true, }, @@ -16033,7 +16033,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_6026", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_6026", "unitMagnitude": 18, "used": true, }, @@ -16130,7 +16130,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_6026", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_6026", "unitMagnitude": 18, "used": true, }, @@ -16255,7 +16255,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_6026", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_6026", "unitMagnitude": 18, "used": true, }, @@ -16352,7 +16352,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_6026", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_6026", "unitMagnitude": 18, "used": true, }, @@ -48285,7 +48285,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_0", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_0", "unitMagnitude": 18, "used": true, }, @@ -48312,7 +48312,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_0", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_0", "unitMagnitude": 18, "used": true, }, @@ -48339,7 +48339,7 @@ Array [ "starred": false, "subAccounts": Array [], "swapHistory": Array [], - "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_0", + "syncHash": "[\\"ethereum/erc20/ampleforth\\",\\"ethereum/erc20/steth\\"]_false_0", "unitMagnitude": 18, "used": true, }, diff --git a/src/api/explorerConfig/index.ts b/src/api/explorerConfig/index.ts index 070ab01160..33c8bb24df 100644 --- a/src/api/explorerConfig/index.ts +++ b/src/api/explorerConfig/index.ts @@ -63,10 +63,6 @@ const initialExplorerConfig: FullConfig = { dogecoin: { id: "doge", stable: { - base: "EXPLORER", - version: "v2", - }, - experimental: { base: "EXPLORER", version: "v3", }, diff --git a/src/bridge/jsHelpers.ts b/src/bridge/jsHelpers.ts index 651dd44541..cbbdc2e179 100644 --- a/src/bridge/jsHelpers.ts +++ b/src/bridge/jsHelpers.ts @@ -134,15 +134,16 @@ export const mergeNfts = ( const nfts = oldNfts?.slice() ?? []; for (let i = 0; i < nfts.length; i++) { const nft = nfts[i]; + // The NFTs are the same, do don't anything - if (!newNftsPerId[nft.id] || isEqual(nft, newNftsPerId[nft.id])) { - // NFT already in, deleting it from the newNfts to keep only the un-added ones at the end - delete newNftsPerId[nft.id]; - continue; + if (!newNftsPerId[nft.id]) { + nfts.splice(i, 1); + i--; + } else if (!isEqual(nft, newNftsPerId[nft.id])) { + // Use the new NFT instead (as a copy cause we're deleting the reference just after) + nfts[i] = Object.assign({}, newNftsPerId[nft.id]); } - // Use the new NFT instead (as a copy cause we're deleting the reference just after) - nfts[i] = Object.assign({}, newNftsPerId[nft.id]); // Delete it from the newNfts to keep only the un-added ones at the end delete newNftsPerId[nft.id]; } diff --git a/src/families/ethereum/bridge/js.ts b/src/families/ethereum/bridge/js.ts index 1caa6300ee..601a772ec7 100644 --- a/src/families/ethereum/bridge/js.ts +++ b/src/families/ethereum/bridge/js.ts @@ -24,11 +24,16 @@ import { estimateGasLimit, } from "../transaction"; import { getAccountShape } from "../synchronisation"; -import { preload, hydrate } from "../modules"; +import { + preload, + hydrate, + prepareTransaction as prepareTransactionModules, +} from "../modules"; import { signOperation } from "../signOperation"; import { modes } from "../modules"; import postSyncPatch from "../postSyncPatch"; import { inferDynamicRange } from "../../../range"; + const receive = makeAccountBridgeReceive(); const broadcast = async ({ @@ -173,6 +178,8 @@ const prepareTransaction = async (a, t: Transaction): Promise => { t.estimatedGasLimit = estimatedGasLimit; } + t = await prepareTransactionModules(a, t); + return t; }; diff --git a/src/families/ethereum/modules/erc1155.ts b/src/families/ethereum/modules/erc1155.ts index afbfafaa02..29b884ebd2 100644 --- a/src/families/ethereum/modules/erc1155.ts +++ b/src/families/ethereum/modules/erc1155.ts @@ -9,6 +9,7 @@ import { import { validateRecipient } from "../transaction"; import type { ModeModule, Transaction } from "../types"; import type { Account } from "../../../types"; +import { prepareTransaction } from "./erc721"; const notOwnedNft = createCustomErrorClass("NotOwnedNft"); const notEnoughNftOwned = createCustomErrorClass("NotEnoughNftOwned"); @@ -75,25 +76,37 @@ const erc1155Transfer: ModeModule = { fields.push({ type: "text", label: "Type", - value: `ERC721.transfer`, + value: `NFT Transfer`, }); fields.push({ type: "text", - label: "Collection", - value: input.transaction.collection ?? "", + label: "To", + value: input.transaction.recipient ?? "", }); fields.push({ type: "text", - label: "Token IDs", - value: input.transaction.tokenIds?.join(",") ?? "", + label: "Collection Name", + value: input.transaction.collectionName || "", }); fields.push({ type: "text", - label: "Quantities", - value: input.transaction.quantities?.join(",") ?? "", + label: "Quantity", + value: input.transaction.quantities?.[0]?.toFixed() ?? "", + }); + + fields.push({ + type: "address", + label: "NFT Address", + address: input.transaction.collection ?? "", + }); + + fields.push({ + type: "text", + label: "NFT ID", + value: input.transaction.tokenIds?.[0] ?? "", }); }, @@ -107,6 +120,8 @@ const erc1155Transfer: ModeModule = { approving: true, // workaround to track the status ENABLING }; }, + + prepareTransaction, }; function serializeTransactionData( diff --git a/src/families/ethereum/modules/erc721.ts b/src/families/ethereum/modules/erc721.ts index 2e1928d332..704257e32e 100644 --- a/src/families/ethereum/modules/erc721.ts +++ b/src/families/ethereum/modules/erc721.ts @@ -9,11 +9,35 @@ import { import { validateRecipient } from "../transaction"; import type { ModeModule, Transaction } from "../types"; import type { Account } from "../../../types"; +import { apiForCurrency } from "../../../api/Ethereum"; const notOwnedNft = createCustomErrorClass("NotOwnedNft"); export type Modes = "erc721.transfer"; +export async function prepareTransaction( + account: Account, + transaction: Transaction +): Promise { + let t = transaction; + const { collection, collectionName, tokenIds } = transaction; + if (collection && tokenIds && typeof collectionName === "undefined") { + const api = apiForCurrency(account.currency); + const [{ status, result }] = await api.getNFTMetadata([ + { + contract: collection, + tokenId: tokenIds[0], + }, + ]); + let collectionName = ""; // default value fallback if issue + if (status === 200) { + collectionName = result?.tokenName || ""; + } + t = { ...t, collectionName }; + } + return Promise.resolve(t); +} + const erc721Transfer: ModeModule = { /** * Tx data is filled during the buildEthereumTx @@ -52,6 +76,8 @@ const erc721Transfer: ModeModule = { } }, + prepareTransaction, + /** * This will only be used by LLM & LLD, not the HW. */ @@ -59,18 +85,30 @@ const erc721Transfer: ModeModule = { fields.push({ type: "text", label: "Type", - value: `ERC721.transfer`, + value: `NFT Transfer`, }); fields.push({ type: "text", - label: "Collection", - value: input.transaction.collection ?? "", + label: "To", + value: input.transaction.recipient ?? "", + }); + + fields.push({ + type: "text", + label: "Collection Name", + value: input.transaction.collectionName || "", + }); + + fields.push({ + type: "address", + label: "NFT Address", + address: input.transaction.collection ?? "", }); fields.push({ type: "text", - label: "Token ID", + label: "NFT ID", value: input.transaction.tokenIds?.[0] ?? "", }); }, diff --git a/src/families/ethereum/modules/index.ts b/src/families/ethereum/modules/index.ts index 27f9d6f6d1..0e738e9f6d 100644 --- a/src/families/ethereum/modules/index.ts +++ b/src/families/ethereum/modules/index.ts @@ -90,6 +90,14 @@ export type ModeModule = { arg1: Transaction, arg2: Operation ) => void; + + /** + * hook to resolve a transaction like the prepareTransaction of the bridge + */ + prepareTransaction?: ( + account: Account, + transaction: Transaction + ) => Promise; }; export const modes: Record = {} as Record< TransactionMode, @@ -137,6 +145,18 @@ export function hydrate(value: unknown, currency: CryptoCurrency): void { } } } +export const prepareTransaction = ( + account: Account, + transaction: Transaction +): Promise => + values(modules) + // @ts-expect-error some module implement it + .map((m) => m.prepareTransaction) + .filter(Boolean) + .reduce( + (p, fn) => p.then((t) => fn(account, t)), + Promise.resolve(transaction) + ); export const prepareTokenAccounts = ( currency: CryptoCurrency, subAccounts: TokenAccount[], diff --git a/src/families/ethereum/nft.test.ts b/src/families/ethereum/nft.test.ts index 8ae040af39..dd88f90bfb 100644 --- a/src/families/ethereum/nft.test.ts +++ b/src/families/ethereum/nft.test.ts @@ -1,11 +1,14 @@ import "../../__tests__/test-helpers/setup"; +import BigNumber from "bignumber.js"; import { reduce } from "rxjs/operators"; -import { fromAccountRaw, toAccountRaw } from "../../account"; -import type { Account, AccountRaw } from "../../types"; +import { fromAccountRaw, toAccountRaw, toNFTRaw } from "../../account"; +import type { Account, AccountRaw, NFT } from "../../types"; import { getAccountBridge } from "../../bridge"; import { makeBridgeCacheSystem } from "../../bridge/cache"; import { patchAccount } from "../../reconciliation"; import { setEnv } from "../../env"; +import { mergeNfts } from "../../bridge/jsHelpers"; +import { encodeNftId } from "../../nft"; jest.setTimeout(120000); @@ -94,6 +97,78 @@ const cache = makeBridgeCacheSystem({ }, }); +describe("nft merging", () => { + const makeNFT = (tokenId: string, contract: string, amount: number): NFT => ({ + id: encodeNftId("test", contract, tokenId), + tokenId, + amount: new BigNumber(amount), + collection: { + contract, + standard: "erc721", + }, + }); + const oldNfts = [ + makeNFT("1", "contract1", 10), + makeNFT("2", "contract1", 1), + makeNFT("3", "contract2", 6), + ]; + + test("should remove first NFT and return new array with same refs", () => { + const nfts = [makeNFT("2", "contract1", 1), makeNFT("3", "contract2", 6)]; + const newNfts = mergeNfts(oldNfts, nfts); + + expect(newNfts.map(toNFTRaw)).toEqual(nfts.map(toNFTRaw)); + expect(oldNfts[1]).toBe(newNfts[0]); + expect(oldNfts[2]).toBe(newNfts[1]); + }); + + test("should remove any NFT and return new array with same refs", () => { + const nfts = [makeNFT("1", "contract1", 10), makeNFT("3", "contract2", 6)]; + const newNfts = mergeNfts(oldNfts, nfts); + + expect(newNfts.map(toNFTRaw)).toEqual(nfts.map(toNFTRaw)); + expect(oldNfts[0]).toBe(newNfts[0]); + expect(oldNfts[2]).toBe(newNfts[1]); + }); + + test("should change NFT amount and return new array with new ref", () => { + const nfts = [ + makeNFT("1", "contract1", 10), + makeNFT("2", "contract1", 5), + makeNFT("3", "contract2", 6), + ]; + const addToNft1 = mergeNfts(oldNfts, nfts); + + expect(addToNft1.map(toNFTRaw)).toEqual(nfts.map(toNFTRaw)); + expect(oldNfts[0]).toBe(addToNft1[0]); + expect(oldNfts[1]).not.toBe(addToNft1[1]); + expect(oldNfts[2]).toBe(addToNft1[2]); + }); + + test("should add NFT and return new array with new ref", () => { + const nfts = [ + makeNFT("1", "contract1", 10), + makeNFT("2", "contract1", 1), + makeNFT("3", "contract2", 6), + makeNFT("4", "contract2", 4), + ]; + const addToNft1 = mergeNfts(oldNfts, nfts); + + expect(addToNft1.map(toNFTRaw)).toEqual( + [ + makeNFT("4", "contract2", 4), + makeNFT("1", "contract1", 10), + makeNFT("2", "contract1", 1), + makeNFT("3", "contract2", 6), + ].map(toNFTRaw) + ); + expect(oldNfts[0]).toBe(addToNft1[1]); + expect(oldNfts[1]).toBe(addToNft1[2]); + expect(oldNfts[2]).toBe(addToNft1[3]); + expect(addToNft1[0]).toBe(nfts[3]); + }); +}); + describe("gaspard NFT on ethereum", () => { let account = fromAccountRaw(gaspardAccount); @@ -122,6 +197,17 @@ describe("gaspard NFT on ethereum", () => { expect(resync.nfts).toEqual(account.nfts); }); + test("remove half NFTs will restore them with half operations", async () => { + const halfOps = Math.ceil(account.operations.length / 2); + const copy = { + ...account, + operations: account.operations.slice(halfOps), + nfts: account.nfts?.slice(Math.ceil((account.nfts?.length || 0) / 2)), + }; + const resync = await sync(copy); + expect(resync.nfts).toEqual(account.nfts); + }); + test("patchAccount restore new NFTs correctly", async () => { const copy = { ...account, diff --git a/src/families/ethereum/synchronisation.ts b/src/families/ethereum/synchronisation.ts index 34f9d8a6a2..9cc417d9d8 100644 --- a/src/families/ethereum/synchronisation.ts +++ b/src/families/ethereum/synchronisation.ts @@ -15,7 +15,7 @@ import { } from "../../account"; import { listTokensForCryptoCurrency } from "../../currencies"; import { encodeAccountId } from "../../account"; -import type { Operation, TokenAccount, Account, NFT } from "../../types"; +import type { Operation, TokenAccount, Account } from "../../types"; import { API, apiForCurrency, Tx } from "../../api/Ethereum"; import { digestTokenAccounts, prepareTokenAccounts } from "./modules"; import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets"; @@ -82,10 +82,8 @@ export const getAccountShape: GetAccountShape = async ( let newOps = flatMap(txs, txToOps({ address, id: accountId, currency })); // extracting out the sub operations by token account const perTokenAccountIdOperations = {}; - // extracting and concat all nft operations for an account - const flatNftOps: Operation[] = []; newOps.forEach((op) => { - const { subOperations, nftOperations } = op; + const { subOperations } = op; if (subOperations?.length) { subOperations.forEach((sop) => { @@ -96,10 +94,6 @@ export const getAccountShape: GetAccountShape = async ( perTokenAccountIdOperations[sop.accountId].push(sop); }); } - - if (nftOperations?.length) { - nftOperations.forEach((nop) => flatNftOps.push(nop)); - } }); const subAccountsExisting = {}; initialAccount?.subAccounts?.forEach((a) => { @@ -173,7 +167,7 @@ export const getAccountShape: GetAccountShape = async ( const operations = mergeOps(initialStableOperations, newOps); const nfts = isNFTActive(currency) - ? mergeNfts(initialAccount?.nfts, await getNfts(flatNftOps)) + ? mergeNfts(initialAccount?.nfts, nftsFromOperations(operations)) : undefined; const accountShape: Partial = { @@ -621,11 +615,6 @@ async function loadERC20Balances(tokenAccounts, address, api) { .filter(Boolean); } -function getNfts(nftOperations: Operation[]): NFT[] { - const nfts: NFT[] = nftsFromOperations(nftOperations); - return nfts.filter((nft) => nft.amount.gt(0)); -} - const SAFE_REORG_THRESHOLD = 80; function stableOperations(a) { diff --git a/src/families/ethereum/transaction.ts b/src/families/ethereum/transaction.ts index f33233e530..7043613b5b 100644 --- a/src/families/ethereum/transaction.ts +++ b/src/families/ethereum/transaction.ts @@ -90,12 +90,14 @@ export const formatTransaction = ( const header = (() => { switch (t.mode) { case "erc721.transfer": - return `${t.mode.toUpperCase()} Collection: ${t.collection} TokenId: ${ - t.tokenIds?.[0] - }`; + return `${t.mode.toUpperCase()} Collection: ${t.collection} (${ + t.collectionName || "" + }) TokenId: ${t.tokenIds?.[0]}`; case "erc1155.transfer": return ( - `${t.mode.toUpperCase()} Collection: ${t.collection}` + + `${t.mode.toUpperCase()} Collection: ${t.collection} (${ + t.collectionName || "" + })` + t.tokenIds ?.map((tokenId, index) => { return `\n - TokenId: ${tokenId} Quantity: ${ @@ -153,6 +155,7 @@ export const fromTransactionRaw = (tr: TransactionRaw): Transaction => { feesStrategy: tr.feesStrategy, tokenIds: tr.tokenIds, collection: tr.collection, + collectionName: tr.collectionName, quantities: tr.quantities?.map((q) => new BigNumber(q)), }; }; @@ -180,6 +183,7 @@ export const toTransactionRaw = (t: Transaction): TransactionRaw => { feesStrategy: t.feesStrategy, tokenIds: t.tokenIds, collection: t.collection, + collectionName: t.collectionName, quantities: t.quantities?.map((q) => q.toString()), }; }; diff --git a/src/families/ethereum/types.ts b/src/families/ethereum/types.ts index d9b8412679..3d358c0c31 100644 --- a/src/families/ethereum/types.ts +++ b/src/families/ethereum/types.ts @@ -38,6 +38,7 @@ export type Transaction = TransactionCommon & { networkInfo: NetworkInfo | null | undefined; allowZeroAmount?: boolean; collection?: string; + collectionName?: string; tokenIds?: string[]; quantities?: BigNumber[]; }; @@ -54,6 +55,7 @@ export type TransactionRaw = TransactionCommonRaw & { allowZeroAmount?: boolean; tokenIds?: string[]; collection?: string; + collectionName?: string; quantities?: string[]; }; export type TypedMessage = { diff --git a/src/nft/helpers.ts b/src/nft/helpers.ts index 07fa1c88d2..9908dcfe4f 100644 --- a/src/nft/helpers.ts +++ b/src/nft/helpers.ts @@ -24,7 +24,7 @@ export const nftsFromOperations = (ops: Operation[]): NFT[] => { const nftKey = contract + nftOp.tokenId!; const { tokenId, standard, id } = nftOp; - const nft = (acc[nftKey] ?? { + const nft = (acc[nftKey] || { id, tokenId: tokenId!, amount: new BigNumber(0), diff --git a/src/reconciliation.ts b/src/reconciliation.ts index bf851488b3..d7895114a1 100644 --- a/src/reconciliation.ts +++ b/src/reconciliation.ts @@ -384,7 +384,10 @@ export function patchAccount( } const nfts = updatedRaw?.nfts?.map(fromNFTRaw); - if (updatedRaw.nfts && !isEqual(account.nfts, nfts)) { + if (!updatedRaw.nfts && account.nfts) { + delete next.nfts; + changed = true; + } else if (!isEqual(account.nfts, nfts)) { next.nfts = nfts; changed = true; }