diff --git a/src/families/ethereum/nft.merging.unit.test.ts b/src/families/ethereum/nft.unit.test.ts similarity index 52% rename from src/families/ethereum/nft.merging.unit.test.ts rename to src/families/ethereum/nft.unit.test.ts index 50bed022f8..ed61974653 100644 --- a/src/families/ethereum/nft.merging.unit.test.ts +++ b/src/families/ethereum/nft.unit.test.ts @@ -1,9 +1,10 @@ import "../../__tests__/test-helpers/setup"; import BigNumber from "bignumber.js"; -import { toNFTRaw } from "../../account"; -import type { ProtoNFT } from "../../types"; +import { encodeAccountId, toNFTRaw } from "../../account"; +import { Operation } from "../../types"; +import { ProtoNFT } from "../../types/nft"; import { mergeNfts } from "../../bridge/jsHelpers"; -import { encodeNftId } from "../../nft"; +import { encodeNftId, nftsFromOperations } from "../../nft"; describe("nft merging", () => { const makeNFT = ( @@ -79,3 +80,62 @@ describe("nft merging", () => { expect(addToNft1[0]).toBe(nfts[3]); }); }); + +describe("OpenSea lazy minting bs", () => { + test("should have a correct on-chain nft amount even with OpenSea lazy minting", () => { + const makeNftOperation = (type: Operation["type"], value): Operation => { + if (!["NFT_IN", "NFT_OUT"].includes(type)) { + return {} as Operation; + } + + const id = encodeAccountId({ + type: "type", + currencyId: "polygon", + xpubOrAddress: "0xbob", + derivationMode: "", + version: "1", + }); + const sender = type === "NFT_IN" ? "0xbob" : "0xkvn"; + const receiver = type === "NFT_IN" ? "0xkvn" : "0xbob"; + const contract = "0x0000000000000000000000000000000000000000"; + const fee = new BigNumber(0); + const tokenId = "42069"; + const hash = "FaKeHasH"; + + return { + id, + hash, + senders: [sender], + recipients: [receiver], + contract, + fee, + standard: "ERC1155", + tokenId, + value: new BigNumber(value), + type, + accountId: id, + } as Operation; + }; + + // scenario with bob lazy minting 10 NFTs + const ops = [ + makeNftOperation("NFT_OUT", 5), // lazy mint sending 5 NFT + makeNftOperation("NFT_IN", 1), // receiving 1 of them back + makeNftOperation("NFT_IN", 2), // receiving 2 of them back + makeNftOperation("NFT_OUT", 2), // lazy mint sending 5 NFT (transformed by OpenSea in 2 txs) 1/2 (off-chain) + makeNftOperation("NFT_OUT", 3), // lazy mint sending 5 NFT (transformed by OpenSea in 2 txs) 2/2 (on-chain) + makeNftOperation("NFT_IN", 1), // receiving 1 back + ]; + + // What happened for bob: + // + // -5 off-chain -> 0 on-chain (5 off-chain) + // +1 on-chain -> 1 on-chain (5 off-chain) + // +2 on-chain -> 3 on-chain (5 off-chain) + // -2 off-chain & -3 on-chain -> 0 on-chain (3 off-chain) + // +1 on-chain -> 1 on-chain (and 3 off-chain) + + const nfts = nftsFromOperations(ops); + expect(nfts[0].amount.toNumber()).toBe(1); + }); +}); diff --git a/src/nft/helpers.ts b/src/nft/helpers.ts index 2996151b62..3484bf2f51 100644 --- a/src/nft/helpers.ts +++ b/src/nft/helpers.ts @@ -42,7 +42,21 @@ export const nftsFromOperations = (ops: Operation[]): ProtoNFT[] => { if (nftOp.type === "NFT_IN") { nft.amount = nft.amount.plus(nftOp.value); } else if (nftOp.type === "NFT_OUT") { - nft.amount = nft.amount.minus(nftOp.value); + const newAmount = nft.amount.minus(nftOp.value); + + // In case of OpenSea lazy minting feature (minting an NFT off-chain) + // OpenSea will fire a false ERC1155 event saying that you sent an NFT + // from your account that you never received first. + // + // E.g.: I'm creating 10 ERC1155 on OpenSea. It's not going to create anything on-chain. + // Then I (bob) decide to transfer 5 to someone (kvn). OpenSea is going to mint the NFT in its + // collection (`OpenSea Shared Storefront` / `OpenSea Collections`) and transfer it to kvn. + // But the event fired by the Smart Contract is going to be `bob transfered 5 NFT to kvn` which is false. + // It would then result in bob have -5 NFTs since he never received them first. + // If kvn send 2 back to bob, based on the events we would think that bob has -3 NFTs. + // + // To mitigate that we put a minimum value of 0 when an account is transferring some NFTs. + nft.amount = newAmount.isNegative() ? new BigNumber(0) : newAmount; } acc[id] = nft;