Skip to content

Commit 49e71fa

Browse files
authored
fix: exclude marketplace fees from private listings (#1714)
1 parent a156373 commit 49e71fa

5 files changed

Lines changed: 124 additions & 9 deletions

File tree

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ export const SHARED_STOREFRONT_ADDRESSES = [
2828
].map((address) => address.toLowerCase());
2929
export const SHARED_STOREFRONT_LAZY_MINT_ADAPTER_CROSS_CHAIN_ADDRESS =
3030
"0xa604060890923ff400e8c6f5290461a83aedacec";
31+
32+
export const OPENSEA_FEE_RECIPIENT =
33+
"0x0000a26b00c1f0df003000390027140000faa719";

src/sdk.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
INVERSE_BASIS_POINT,
2828
ENGLISH_AUCTION_ZONE_MAINNETS,
2929
ENGLISH_AUCTION_ZONE_TESTNETS,
30+
OPENSEA_FEE_RECIPIENT,
3031
} from "./constants";
3132
import {
3233
constructPrivateListingCounterOrder,
@@ -44,6 +45,7 @@ import {
4445
EventData,
4546
EventType,
4647
Chain,
48+
Fee,
4749
OpenSeaAPIConfig,
4850
OpenSeaCollection,
4951
OrderSide,
@@ -260,18 +262,25 @@ export class OpenSeaSDK {
260262
startAmount,
261263
endAmount,
262264
excludeOptionalCreatorFees,
265+
isPrivateListing = false,
263266
}: {
264267
collection: OpenSeaCollection;
265268
seller?: string;
266269
paymentTokenAddress: string;
267270
startAmount: bigint;
268271
endAmount?: bigint;
269272
excludeOptionalCreatorFees?: boolean;
273+
isPrivateListing?: boolean;
270274
}): Promise<ConsiderationInputItem[]> {
271275
let collectionFees = collection.fees;
272276
if (excludeOptionalCreatorFees) {
273277
collectionFees = collectionFees.filter((fee) => fee.required);
274278
}
279+
if (isPrivateListing) {
280+
collectionFees = collectionFees.filter((fee) =>
281+
this.isNotMarketplaceFee(fee),
282+
);
283+
}
275284
const collectionFeesBasisPoints = totalBasisPointsForFees(collectionFees);
276285
const sellerBasisPoints = INVERSE_BASIS_POINT - collectionFeesBasisPoints;
277286

@@ -302,6 +311,10 @@ export class OpenSeaSDK {
302311
return considerationItems;
303312
}
304313

314+
private isNotMarketplaceFee(fee: Fee): boolean {
315+
return fee.recipient.toLowerCase() !== OPENSEA_FEE_RECIPIENT.toLowerCase();
316+
}
317+
305318
private getNFTItems(
306319
nfts: NFT[],
307320
quantities: bigint[] = [],
@@ -507,6 +520,7 @@ export class OpenSeaSDK {
507520
startAmount: basePrice,
508521
endAmount: endPrice,
509522
excludeOptionalCreatorFees,
523+
isPrivateListing: !!buyerAddress,
510524
});
511525

512526
if (buyerAddress) {

test/integration/postOrder.spec.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@ import {
99
sdk,
1010
sdkPolygon,
1111
walletAddress,
12+
getRandomExpiration,
1213
} from "./setup";
1314
import { ENGLISH_AUCTION_ZONE_MAINNETS } from "../../src/constants";
1415
import { getWETHAddress } from "../../src/utils";
1516
import { OFFER_AMOUNT } from "../utils/constants";
1617
import { expectValidOrder } from "../utils/utils";
1718

18-
const ONE_HOUR = Math.floor(Date.now() / 1000) + 3600;
19-
const expirationTime = ONE_HOUR;
20-
2119
suite("SDK: order posting", () => {
2220
test("Post Offer - Mainnet", async () => {
21+
const expirationTime = getRandomExpiration();
2322
const offer = {
2423
accountAddress: walletAddress,
2524
startAmount: +OFFER_AMOUNT,
@@ -41,6 +40,7 @@ suite("SDK: order posting", () => {
4140
});
4241

4342
test("Post Offer - Polygon", async () => {
43+
const expirationTime = getRandomExpiration();
4444
const offer = {
4545
accountAddress: walletAddress,
4646
startAmount: +OFFER_AMOUNT,
@@ -58,12 +58,13 @@ suite("SDK: order posting", () => {
5858
if (!TOKEN_ADDRESS_MAINNET || !TOKEN_ID_MAINNET) {
5959
this.skip();
6060
}
61+
const expirationTime = getRandomExpiration();
6162
const listing = {
6263
accountAddress: walletAddress,
6364
startAmount: LISTING_AMOUNT,
6465
asset: {
65-
tokenAddress: TOKEN_ADDRESS_MAINNET as string,
66-
tokenId: TOKEN_ID_MAINNET as string,
66+
tokenAddress: TOKEN_ADDRESS_MAINNET,
67+
tokenId: TOKEN_ID_MAINNET,
6768
},
6869
expirationTime,
6970
};
@@ -78,12 +79,13 @@ suite("SDK: order posting", () => {
7879
if (!TOKEN_ADDRESS_MAINNET || !TOKEN_ID_MAINNET) {
7980
this.skip();
8081
}
82+
const expirationTime = getRandomExpiration();
8183
const listing = {
8284
accountAddress: walletAddress,
8385
startAmount: LISTING_AMOUNT,
8486
asset: {
85-
tokenAddress: TOKEN_ADDRESS_MAINNET as string,
86-
tokenId: TOKEN_ID_MAINNET as string,
87+
tokenAddress: TOKEN_ADDRESS_MAINNET,
88+
tokenId: TOKEN_ID_MAINNET,
8789
},
8890
englishAuction: true,
8991
expirationTime,
@@ -105,6 +107,7 @@ suite("SDK: order posting", () => {
105107
});
106108

107109
test.skip("Post Listing - Polygon", async function () {
110+
const expirationTime = getRandomExpiration();
108111
const listing = {
109112
accountAddress: walletAddress,
110113
paymentTokenAddress: getWETHAddress(sdkPolygon.chain),
@@ -122,6 +125,7 @@ suite("SDK: order posting", () => {
122125
test.skip("Post Collection Offer - Mainnet", async () => {
123126
const collection = await sdk.api.getCollection("cool-cats-nft");
124127
const paymentTokenAddress = getWETHAddress(sdk.chain);
128+
const expirationTime = getRandomExpiration();
125129
const postOrderRequest = {
126130
collectionSlug: collection.collection,
127131
accountAddress: walletAddress,
@@ -149,6 +153,7 @@ suite("SDK: order posting", () => {
149153
test.skip("Post Collection Offer - Polygon", async () => {
150154
const collection = await sdkPolygon.api.getCollection("arttoken-1155-4");
151155
const paymentTokenAddress = getWETHAddress(sdkPolygon.chain);
156+
const expirationTime = getRandomExpiration();
152157
const postOrderRequest = {
153158
collectionSlug: collection.collection,
154159
accountAddress: walletAddress,
@@ -180,6 +185,7 @@ suite("SDK: order posting", () => {
180185
test("Post Trait Offer - Ethereum", async () => {
181186
const collection = await sdk.api.getCollection("cool-cats-nft");
182187
const paymentTokenAddress = getWETHAddress(sdk.chain);
188+
const expirationTime = getRandomExpiration();
183189
const postOrderRequest = {
184190
collectionSlug: collection.collection,
185191
accountAddress: walletAddress,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { expect } from "chai";
2+
import { suite, test } from "mocha";
3+
import {
4+
getRandomExpiration,
5+
LISTING_AMOUNT,
6+
TOKEN_ADDRESS_MAINNET,
7+
TOKEN_ID_MAINNET,
8+
sdk,
9+
walletAddress,
10+
} from "./setup";
11+
import { OPENSEA_FEE_RECIPIENT } from "../../src/constants";
12+
import { expectValidOrder } from "../utils/utils";
13+
14+
suite("SDK: Private Listings Integration", () => {
15+
test("Post Private Listing - Mainnet", async function () {
16+
if (!TOKEN_ADDRESS_MAINNET || !TOKEN_ID_MAINNET) {
17+
this.skip();
18+
}
19+
20+
const buyerAddress = "0x0000000000000000000000000000000000000001";
21+
const expirationTime = getRandomExpiration();
22+
23+
const privateListing = {
24+
accountAddress: walletAddress,
25+
startAmount: LISTING_AMOUNT,
26+
asset: {
27+
tokenAddress: TOKEN_ADDRESS_MAINNET,
28+
tokenId: TOKEN_ID_MAINNET,
29+
},
30+
buyerAddress,
31+
expirationTime,
32+
};
33+
34+
const order = await sdk.createListing(privateListing);
35+
expectValidOrder(order);
36+
37+
expect(order.protocolData.parameters.consideration).to.exist;
38+
39+
const hasMarketplaceFee = order.protocolData.parameters.consideration.some(
40+
(item: { recipient?: string }) =>
41+
item.recipient?.toLowerCase() === OPENSEA_FEE_RECIPIENT.toLowerCase(),
42+
);
43+
44+
expect(hasMarketplaceFee).to.be.false;
45+
});
46+
47+
test("Post Regular Listing - Mainnet (for comparison)", async function () {
48+
if (!TOKEN_ADDRESS_MAINNET || !TOKEN_ID_MAINNET) {
49+
this.skip();
50+
}
51+
52+
const expirationTime = getRandomExpiration();
53+
const regularListing = {
54+
accountAddress: walletAddress,
55+
startAmount: LISTING_AMOUNT,
56+
asset: {
57+
tokenAddress: TOKEN_ADDRESS_MAINNET,
58+
tokenId: TOKEN_ID_MAINNET,
59+
},
60+
expirationTime,
61+
};
62+
63+
const order = await sdk.createListing(regularListing);
64+
expectValidOrder(order);
65+
66+
expect(order.protocolData.parameters.consideration).to.exist;
67+
expect(
68+
order.protocolData.parameters.consideration.length,
69+
).to.be.greaterThan(0);
70+
});
71+
});

test/integration/setup.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { randomBytes } from "crypto";
12
import { ethers } from "ethers";
23
import { OpenSeaSDK } from "../../src/sdk";
34
import { Chain } from "../../src/types";
@@ -14,8 +15,9 @@ for (const envVar of ["WALLET_PRIV_KEY"]) {
1415
}
1516
}
1617

17-
export const TOKEN_ADDRESS_MAINNET = process.env.SELL_ORDER_CONTRACT_ADDRESS;
18-
export const TOKEN_ID_MAINNET = process.env.SELL_ORDER_TOKEN_ID;
18+
export const TOKEN_ADDRESS_MAINNET = process.env
19+
.SELL_ORDER_CONTRACT_ADDRESS as string;
20+
export const TOKEN_ID_MAINNET = process.env.SELL_ORDER_TOKEN_ID as string;
1921
export const TOKEN_ADDRESS_POLYGON =
2022
process.env.SELL_ORDER_CONTRACT_ADDRESS_POLYGON;
2123
export const TOKEN_ID_POLYGON = process.env.SELL_ORDER_TOKEN_ID_POLYGON;
@@ -49,3 +51,22 @@ export const sdkPolygon = new OpenSeaSDK(
4951
},
5052
(line) => console.info(`POLYGON: ${line}`),
5153
);
54+
55+
export const getRandomExpiration = (): number => {
56+
const now = Math.floor(Date.now() / 1000);
57+
const fifteenMinutes = 15 * 60;
58+
const oneHour = 60 * 60;
59+
const range = oneHour - fifteenMinutes + 1;
60+
61+
const maxValue = 0xffffffff; // 2^32 - 1
62+
const rejectionThreshold = maxValue - (maxValue % range);
63+
64+
let randomValue: number;
65+
do {
66+
const randomBuffer = randomBytes(4);
67+
randomValue = randomBuffer.readUInt32BE(0);
68+
} while (randomValue >= rejectionThreshold);
69+
70+
const randomSeconds = (randomValue % range) + fifteenMinutes;
71+
return now + randomSeconds;
72+
};

0 commit comments

Comments
 (0)