Skip to content

Commit 125b065

Browse files
authored
Fix: Support zero-payment private listings (#1834)
Fixes SDK crash when fulfilling zero-payment private listings (e.g., rewards claims). Problem: - constructPrivateListingCounterOrder crashes when paymentItems is empty - fulfillPrivateOrder crashes accessing counterOrder.parameters.offer[0] Solution (mirrors backend behavior in os2-core Seaport.kt): - Allow empty payment items in constructPrivateListingCounterOrder - Return counter order with offer: [] for zero-payment listings - Add computePrivateListingValue() to calculate ETH value from original order - Use computed value instead of counter order's offer[0].startAmount This enables fulfillment of zero-payment private listings used for rewards claims via REWARDS_PRIVATE_LISTING_CLAIM_WALLET. Closes #1832
1 parent a640110 commit 125b065

File tree

4 files changed

+359
-20
lines changed

4 files changed

+359
-20
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opensea-js",
3-
"version": "8.0.8",
3+
"version": "8.0.9",
44
"description": "TypeScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data",
55
"license": "MIT",
66
"author": "OpenSea Developers",

src/orders/privateListings.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ItemType } from "@opensea/seaport-js/lib/constants";
12
import {
23
ConsiderationInputItem,
34
CreateInputItem,
@@ -7,6 +8,28 @@ import {
78
} from "@opensea/seaport-js/lib/types";
89
import { isCurrencyItem } from "@opensea/seaport-js/lib/utils/item";
910
import { generateRandomSalt } from "@opensea/seaport-js/lib/utils/order";
11+
import { ZeroAddress } from "ethers";
12+
13+
/**
14+
* Compute the native currency (ETH) value required to fulfill a private listing.
15+
* Sums all native currency consideration items not going to the taker.
16+
* @param order The private listing order
17+
* @param takerAddress The address of the private listing recipient
18+
* @returns The total native currency value as a bigint
19+
*/
20+
export const computePrivateListingValue = (
21+
order: OrderWithCounter,
22+
takerAddress: string,
23+
): bigint => {
24+
return order.parameters.consideration
25+
.filter(
26+
(item) =>
27+
item.recipient.toLowerCase() !== takerAddress.toLowerCase() &&
28+
item.token.toLowerCase() === ZeroAddress.toLowerCase() &&
29+
item.itemType === ItemType.NATIVE,
30+
)
31+
.reduce((sum, item) => sum + BigInt(item.startAmount), 0n);
32+
};
1033

1134
export const getPrivateListingConsiderations = (
1235
offer: CreateInputItem[],
@@ -28,15 +51,18 @@ export const constructPrivateListingCounterOrder = (
2851
item.recipient.toLowerCase() !== privateSaleRecipient.toLowerCase(),
2952
);
3053

31-
if (!paymentItems.every((item) => isCurrencyItem(item))) {
32-
throw new Error(
33-
"The consideration for the private listing did not contain only currency items",
34-
);
35-
}
36-
if (
37-
!paymentItems.every((item) => item.itemType === paymentItems[0].itemType)
38-
) {
39-
throw new Error("Not all currency items were the same for private order");
54+
// Only validate payment items if there are any (zero-payment private listings are valid)
55+
if (paymentItems.length > 0) {
56+
if (!paymentItems.every((item) => isCurrencyItem(item))) {
57+
throw new Error(
58+
"The consideration for the private listing did not contain only currency items",
59+
);
60+
}
61+
if (
62+
!paymentItems.every((item) => item.itemType === paymentItems[0].itemType)
63+
) {
64+
throw new Error("Not all currency items were the same for private order");
65+
}
4066
}
4167

4268
const { aggregatedStartAmount, aggregatedEndAmount } = paymentItems.reduce(
@@ -54,15 +80,19 @@ export const constructPrivateListingCounterOrder = (
5480
parameters: {
5581
...order.parameters,
5682
offerer: privateSaleRecipient,
57-
offer: [
58-
{
59-
itemType: paymentItems[0].itemType,
60-
token: paymentItems[0].token,
61-
identifierOrCriteria: paymentItems[0].identifierOrCriteria,
62-
startAmount: aggregatedStartAmount.toString(),
63-
endAmount: aggregatedEndAmount.toString(),
64-
},
65-
],
83+
// Empty offer for zero-payment private listings, single aggregated item otherwise
84+
offer:
85+
paymentItems.length > 0
86+
? [
87+
{
88+
itemType: paymentItems[0].itemType,
89+
token: paymentItems[0].token,
90+
identifierOrCriteria: paymentItems[0].identifierOrCriteria,
91+
startAmount: aggregatedStartAmount.toString(),
92+
endAmount: aggregatedEndAmount.toString(),
93+
},
94+
]
95+
: [],
6696
// The consideration here is empty as the original private listing order supplies
6797
// the taker address to receive the desired items.
6898
consideration: [],

src/sdk/fulfillment.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SDKContext } from "./context";
55
import { OrdersManager } from "./orders";
66
import { Listing, Offer, Order } from "../api/types";
77
import {
8+
computePrivateListingValue,
89
constructPrivateListingCounterOrder,
910
getPrivateListingFulfillments,
1011
} from "../orders/privateListings";
@@ -60,6 +61,14 @@ export class FulfillmentManager {
6061
order.taker.address,
6162
);
6263
const fulfillments = getPrivateListingFulfillments(order.protocolData);
64+
65+
// Compute ETH value from original order's consideration items
66+
// This handles both standard private listings and zero-payment listings (e.g., rewards)
67+
const value = computePrivateListingValue(
68+
order.protocolData,
69+
order.taker.address,
70+
);
71+
6372
const seaport = getSeaportInstance(
6473
order.protocolAddress,
6574
this.context.seaport,
@@ -70,7 +79,7 @@ export class FulfillmentManager {
7079
fulfillments,
7180
overrides: {
7281
...overrides,
73-
value: counterOrder.parameters.offer[0].startAmount,
82+
value,
7483
},
7584
accountAddress,
7685
domain,

0 commit comments

Comments
 (0)