diff --git a/package.json b/package.json index d02cbe0b..0016eef7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@opensea/seaport-js", - "version": "3.0.2", + "version": "3.0.3", "description": "[Seaport](https://github.com/ProjectOpenSea/seaport) is a new marketplace protocol for safely and efficiently buying and selling NFTs. This is a TypeScript library intended to make interfacing with the contract reasonable and easy.", "license": "MIT", "author": "OpenSea Developers", diff --git a/src/seaport.ts b/src/seaport.ts index 7f2f9aa1..bec40063 100644 --- a/src/seaport.ts +++ b/src/seaport.ts @@ -49,10 +49,11 @@ import { fulfillBasicOrder, FulfillOrdersMetadata, fulfillStandardOrder, + scaleOrderStatusToMaxUnits, shouldUseBasicFulfill, validateAndSanitizeFromOrderStatus, } from "./utils/fulfill"; -import { getMaximumSizeForOrder, isCurrencyItem } from "./utils/item"; +import { isCurrencyItem } from "./utils/item"; import { areAllCurrenciesSame, deductFees, @@ -855,6 +856,8 @@ export class Seaport { const currentBlockTimestamp = currentBlock!.timestamp; + scaleOrderStatusToMaxUnits(order, orderStatus); + const { totalFilled, totalSize } = orderStatus; const sanitizedOrder = validateAndSanitizeFromOrderStatus( @@ -910,8 +913,7 @@ export class Seaport { order: sanitizedOrder, unitsToFill, totalFilled, - totalSize: - totalSize === 0n ? getMaximumSizeForOrder(sanitizedOrder) : totalSize, + totalSize, offerCriteria, considerationCriteria, tips: tipConsiderationItems, @@ -1036,7 +1038,10 @@ export class Seaport { (orderDetails, index) => ({ order: orderDetails.order, unitsToFill: orderDetails.unitsToFill, - orderStatus: orderStatuses[index], + orderStatus: scaleOrderStatusToMaxUnits( + orderDetails.order, + orderStatuses[index], + ), offerCriteria: orderDetails.offerCriteria ?? [], considerationCriteria: orderDetails.considerationCriteria ?? [], tips: diff --git a/src/utils/fulfill.ts b/src/utils/fulfill.ts index 4106ff73..c241b592 100644 --- a/src/utils/fulfill.ts +++ b/src/utils/fulfill.ts @@ -21,6 +21,7 @@ import type { OrderParameters, OrderStatus, OrderUseCase, + OrderWithCounter, } from "../types"; import { getApprovalActions } from "./approval"; import { @@ -863,3 +864,21 @@ export const getAdvancedOrderNumeratorDenominator = ( return { numerator, denominator }; }; + +export const scaleOrderStatusToMaxUnits = ( + order: OrderWithCounter, + orderStatus: OrderStatus, +) => { + const maxUnits = getMaximumSizeForOrder(order); + if (orderStatus.totalSize === 0n) { + // Seaport returns 0 for totalSize if the order has not been fulfilled before. + orderStatus.totalSize = maxUnits; + } else { + // Scale the total filled and total size to the max units, + // so we can properly calculate the units to fill. + orderStatus.totalFilled = + (orderStatus.totalFilled * maxUnits) / orderStatus.totalSize; + orderStatus.totalSize = maxUnits; + } + return orderStatus; +}; diff --git a/test/fulfill-orders.spec.ts b/test/fulfill-orders.spec.ts index 3bd53056..eb4d2c90 100644 --- a/test/fulfill-orders.spec.ts +++ b/test/fulfill-orders.spec.ts @@ -778,6 +778,177 @@ describeWithFixture( }); }); + describe("[Buy now] I want to buy three ERC1155 listings twice", () => { + beforeEach(async () => { + const { testErc1155 } = fixture; + + // These will be used in 3 separate orders + await testErc1155.mint(await offerer.getAddress(), nftId, 100); + await testErc1155.mint(await offerer.getAddress(), nftId, 100); + await secondTestErc1155.mint( + await secondOfferer.getAddress(), + nftId, + 100, + ); + + firstStandardCreateOrderInput = { + allowPartialFills: true, + offer: [ + { + itemType: ItemType.ERC1155, + token: await testErc1155.getAddress(), + amount: "100", + identifier: nftId, + }, + ], + consideration: [ + { + amount: parseEther("10").toString(), + recipient: await offerer.getAddress(), + }, + ], + // 2.5% fee + fees: [{ recipient: await zone.getAddress(), basisPoints: 250 }], + }; + + secondStandardCreateOrderInput = { + allowPartialFills: true, + offer: [ + { + itemType: ItemType.ERC1155, + token: await testErc1155.getAddress(), + amount: "100", + identifier: nftId, + }, + ], + consideration: [ + { + amount: parseEther("10").toString(), + recipient: await offerer.getAddress(), + }, + ], + // 2.5% fee + fees: [{ recipient: await zone.getAddress(), basisPoints: 250 }], + }; + + thirdStandardCreateOrderInput = { + allowPartialFills: true, + offer: [ + { + itemType: ItemType.ERC1155, + token: await secondTestErc1155.getAddress(), + amount: "100", + identifier: nftId, + }, + ], + consideration: [ + { + amount: parseEther("10").toString(), + recipient: await secondOfferer.getAddress(), + }, + ], + // 2.5% fee + fees: [{ recipient: await zone.getAddress(), basisPoints: 250 }], + }; + }); + + describe("with ETH", () => { + it("3 ERC1155 <=> ETH", async () => { + const { seaport, testErc1155 } = fixture; + + const firstOrderUseCase = await seaport.createOrder( + firstStandardCreateOrderInput, + ); + + const firstOrder = await firstOrderUseCase.executeAllActions(); + + const secondOrderUseCase = await seaport.createOrder( + secondStandardCreateOrderInput, + ); + + const secondOrder = await secondOrderUseCase.executeAllActions(); + + const thirdOrderUseCase = await seaport.createOrder( + thirdStandardCreateOrderInput, + await secondOfferer.getAddress(), + ); + + const thirdOrder = await thirdOrderUseCase.executeAllActions(); + + const { actions } = await seaport.fulfillOrders({ + fulfillOrderDetails: [ + { order: firstOrder, unitsToFill: 50 }, + { order: secondOrder, unitsToFill: 50 }, + { order: thirdOrder, unitsToFill: 50 }, + ], + accountAddress: await fulfiller.getAddress(), + domain: OPENSEA_DOMAIN, + }); + + expect(actions.length).to.eq(1); + + const action = actions[0]; + + expect(action.type).eq("exchange"); + + expect( + (await action.transactionMethods.buildTransaction()).data?.slice( + -8, + ), + ).to.eq(OPENSEA_DOMAIN_TAG); + + const transaction = await action.transactionMethods.transact(); + expect(transaction.data.slice(-8)).to.eq(OPENSEA_DOMAIN_TAG); + + const balances = await Promise.all([ + testErc1155.balanceOf(await fulfiller.getAddress(), nftId), + secondTestErc1155.balanceOf(await fulfiller.getAddress(), nftId), + ]); + + expect(balances[0]).to.equal(100n); + expect(balances[1]).to.equal(50n); + + expect(fulfillAvailableOrdersSpy.calledOnce); + + // Fulfill the order again for another 7 units + const { actions: actions2 } = await seaport.fulfillOrders({ + fulfillOrderDetails: [ + { order: firstOrder, unitsToFill: 7 }, + { order: secondOrder, unitsToFill: 7 }, + { order: thirdOrder, unitsToFill: 7 }, + ], + accountAddress: await fulfiller.getAddress(), + domain: OPENSEA_DOMAIN, + }); + + expect(actions2.length).to.eq(1); + + const action2 = actions2[0]; + + expect(action2.type).eq("exchange"); + + expect( + (await action2.transactionMethods.buildTransaction()).data?.slice( + -8, + ), + ).to.eq(OPENSEA_DOMAIN_TAG); + + const transaction2 = await action2.transactionMethods.transact(); + expect(transaction2.data.slice(-8)).to.eq(OPENSEA_DOMAIN_TAG); + + const balances2 = await Promise.all([ + testErc1155.balanceOf(await fulfiller.getAddress(), nftId), + secondTestErc1155.balanceOf(await fulfiller.getAddress(), nftId), + ]); + + expect(balances2[0]).to.equal(114n); + expect(balances2[1]).to.equal(BigInt(57n)); + + expect(fulfillAvailableOrdersSpy.calledTwice); + }); + }); + }); + describe("[Accept offer] I want to accept three ERC1155 offers", () => { beforeEach(async () => { const { testErc1155, testErc20 } = fixture; diff --git a/test/partial-fulfill.spec.ts b/test/partial-fulfill.spec.ts index 1d538fa6..8517914f 100644 --- a/test/partial-fulfill.spec.ts +++ b/test/partial-fulfill.spec.ts @@ -771,6 +771,139 @@ describeWithFixture( }); }); + describe("[Buy now] I want to partially buy an ERC1155 twice", () => { + beforeEach(async () => { + const { testErc1155 } = fixture; + + // Mint 100 ERC1155s to offerer + await testErc1155.mint(await offerer.getAddress(), nftId, 100); + + standardCreateOrderInput = { + allowPartialFills: true, + + offer: [ + { + itemType: ItemType.ERC1155, + token: await testErc1155.getAddress(), + amount: "100", + identifier: nftId, + }, + ], + consideration: [ + { + amount: parseEther("100").toString(), + recipient: await offerer.getAddress(), + }, + ], + // 2.5% fee + fees: [{ recipient: await zone.getAddress(), basisPoints: 250 }], + }; + }); + + it("ERC1155 <=> ETH", async () => { + const { seaport, testErc1155 } = fixture; + + const { executeAllActions } = await seaport.createOrder( + standardCreateOrderInput, + ); + + const order = await executeAllActions(); + + expect(order.parameters.orderType).eq(OrderType.PARTIAL_OPEN); + + const orderStatus = await seaport.getOrderStatus( + seaport.getOrderHash(order.parameters), + ); + + const ownerToTokenToIdentifierBalances = + await getBalancesForFulfillOrder( + order, + await fulfiller.getAddress(), + ); + + const { actions } = await seaport.fulfillOrder({ + order, + unitsToFill: 50, + accountAddress: await fulfiller.getAddress(), + domain: OPENSEA_DOMAIN, + }); + + expect(actions.length).to.eq(1); + + const action = actions[0]; + + expect(action).to.deep.equal({ + type: "exchange", + transactionMethods: action.transactionMethods, + }); + + const transaction = await action.transactionMethods.transact(); + expect(transaction.data.slice(-8)).to.eq(OPENSEA_DOMAIN_TAG); + + const receipt = await transaction.wait(); + + const offererErc1155Balance = await testErc1155.balanceOf( + await offerer.getAddress(), + nftId, + ); + + const fulfillerErc1155Balance = await testErc1155.balanceOf( + await fulfiller.getAddress(), + nftId, + ); + + expect(offererErc1155Balance).eq(50n); + expect(fulfillerErc1155Balance).eq(50n); + + await verifyBalancesAfterFulfill({ + ownerToTokenToIdentifierBalances, + order, + unitsToFill: 50, + orderStatus, + fulfillerAddress: await fulfiller.getAddress(), + + fulfillReceipt: receipt!, + }); + + expect(fulfillStandardOrderSpy.calledOnce); + + // Fulfill the order again for another 7 units + const { actions: actions2 } = await seaport.fulfillOrder({ + order, + unitsToFill: 7, + accountAddress: await fulfiller.getAddress(), + domain: OPENSEA_DOMAIN, + }); + + expect(actions2.length).to.eq(1); + + const action2 = actions2[0]; + + expect(action2).to.deep.equal({ + type: "exchange", + transactionMethods: action2.transactionMethods, + }); + + const transaction2 = await action2.transactionMethods.transact(); + expect(transaction2.data.slice(-8)).to.eq(OPENSEA_DOMAIN_TAG); + + const offererErc1155Balance2 = await testErc1155.balanceOf( + await offerer.getAddress(), + nftId, + ); + + const fulfillerErc1155Balance2 = await testErc1155.balanceOf( + await fulfiller.getAddress(), + nftId, + ); + + expect(offererErc1155Balance2).eq(43n); + expect(fulfillerErc1155Balance2).eq(57n); + + expect(fulfillStandardOrderSpy.calledTwice); + }); + }); + describe("[Accept offer] I want to accept a partial offer for my ERC1155", () => { beforeEach(async () => { const { testErc20, testErc1155 } = fixture;