Skip to content

Commit

Permalink
Fix partial fills (#490)
Browse files Browse the repository at this point in the history
* fix partial fills

* bump version
  • Loading branch information
ryanio authored Mar 18, 2024
1 parent 354d612 commit ddd5f09
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 5 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
13 changes: 9 additions & 4 deletions src/seaport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -855,6 +856,8 @@ export class Seaport {

const currentBlockTimestamp = currentBlock!.timestamp;

scaleOrderStatusToMaxUnits(order, orderStatus);

const { totalFilled, totalSize } = orderStatus;

const sanitizedOrder = validateAndSanitizeFromOrderStatus(
Expand Down Expand Up @@ -910,8 +913,7 @@ export class Seaport {
order: sanitizedOrder,
unitsToFill,
totalFilled,
totalSize:
totalSize === 0n ? getMaximumSizeForOrder(sanitizedOrder) : totalSize,
totalSize,
offerCriteria,
considerationCriteria,
tips: tipConsiderationItems,
Expand Down Expand Up @@ -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:
Expand Down
19 changes: 19 additions & 0 deletions src/utils/fulfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
OrderParameters,
OrderStatus,
OrderUseCase,
OrderWithCounter,
} from "../types";
import { getApprovalActions } from "./approval";
import {
Expand Down Expand Up @@ -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;
};
171 changes: 171 additions & 0 deletions test/fulfill-orders.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
133 changes: 133 additions & 0 deletions test/partial-fulfill.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit ddd5f09

Please sign in to comment.