Skip to content

Commit 0696f6a

Browse files
authored
Add english auctions (#1160)
* add english auctions * move decodeTokenIds tests to misc.spec.ts * improve error * update docs, remove more unused fields
1 parent 893866a commit 0696f6a

8 files changed

Lines changed: 119 additions & 128 deletions

File tree

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ See [Listening to Events](#listening-to-events) to respond to the setup transact
201201

202202
English Auctions are auctions that start at a small amount (we recommend even doing 0!) and increase with every bid. At expiration time, the item sells to the highest bidder.
203203

204-
To create an English Auction, create a listing that waits for the highest bid by setting `waitForHighestBid` to `true`:
204+
To create an English Auction set `englishAuction` to `true`:
205205

206206
```typescript
207207
// Create an auction to receive Wrapped Ether (WETH). See note below.
@@ -218,7 +218,7 @@ const order = await openseaSDK.createSellOrder({
218218
startAmount,
219219
expirationTime,
220220
paymentTokenAddress,
221-
waitForHighestBid: true,
221+
englishAuction: true,
222222
});
223223
```
224224

@@ -256,14 +256,12 @@ The available API filters for the orders endpoint is documented in the `OrdersQu
256256
```TypeScript
257257
/**
258258
* Attrs used by orderbook to make queries easier
259-
* More to come soon!
260259
*/
261260
side: "bid" | "ask", // "bid" for buy orders, "ask" for sell orders
262261
protocol?: "seaport"; // Protocol of the order (more options may be added in future)
263262
maker?: string, // Address of the order's creator
264263
taker?: string, // The null address if anyone is allowed to take the order
265264
owner?: string, // Address of owner of the order's item
266-
sale_kind?: SaleKind, // 0 for fixed-price, 1 for Dutch auctions
267265
assetContractAddress?: string, // Contract address for order's item
268266
paymentTokenAddress?: string; // Contract address for order's payment token
269267
tokenId?: number | string,

developerDocs/getting-started.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ See [Listening to Events](#listening-to-events) to respond to the setup transact
131131

132132
English Auctions are auctions that start at a small amount (we recommend even doing 0!) and increase with every bid. At expiration time, the item sells to the highest bidder.
133133

134-
To create an English Auction, create a listing that waits for the highest bid by setting `waitForHighestBid` to `true`:
134+
To create an English Auction set `englishAuction` to `true`:
135135

136136
```typescript
137137
// Create an auction to receive Wrapped Ether (WETH). See note below.
@@ -148,7 +148,7 @@ const auction = await openseaSDK.createSellOrder({
148148
startAmount,
149149
expirationTime,
150150
paymentTokenAddress,
151-
waitForHighestBid: true,
151+
englishAuction: true,
152152
});
153153
```
154154

@@ -186,14 +186,12 @@ The available API filters for the orders endpoint is documented in the `OrdersQu
186186
```TypeScript
187187
/**
188188
* Attrs used by orderbook to make queries easier
189-
* More to come soon!
190189
*/
191190
side: "bid" | "ask", // "bid" for buy orders, "ask" for sell orders
192191
protocol?: "seaport"; // Protocol of the order (more options may be added in future)
193192
maker?: string, // Address of the order's creator
194193
taker?: string, // The null address if anyone is allowed to take the order
195194
owner?: string, // Address of owner of the order's item
196-
sale_kind?: SaleKind, // 0 for fixed-price, 1 for Dutch auctions
197195
assetContractAddress?: string, // Contract address for order's item
198196
paymentTokenAddress?: string; // Contract address for order's payment token
199197
tokenId?: number | string,

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const API_BASE_TESTNET = "https://testnets-api.opensea.io";
88
export const API_V1_PATH = `/api/v1`;
99

1010
export const DEFAULT_ZONE = ethers.constants.AddressZero;
11+
export const ENGLISH_AUCTION_ZONE =
12+
"0x110b2b128a9ed1be5ef3232d8e4e41640df5c2cd";
1113

1214
// Ignore eslint no-unused-modules for below to keep backward compatibility
1315
// in case a downstream user was already using these imports directly.

src/sdk.ts

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import {
1919
import { parseEther } from "ethers/lib/utils";
2020
import { OpenSeaAPI } from "./api/api";
2121
import { Offer, NFT } from "./api/types";
22-
import { INVERSE_BASIS_POINT, DEFAULT_ZONE } from "./constants";
22+
import {
23+
INVERSE_BASIS_POINT,
24+
DEFAULT_ZONE,
25+
ENGLISH_AUCTION_ZONE,
26+
} from "./constants";
2327
import {
2428
constructPrivateListingCounterOrder,
2529
getPrivateListingConsiderations,
@@ -429,6 +433,7 @@ export class OpenSeaSDK {
429433
* @param options.expirationTime Expiration time for the order, in UTC seconds.
430434
* @param options.paymentTokenAddress ERC20 address for the payment token in the order. If unspecified, defaults to ETH
431435
* @param options.buyerAddress Optional address that's allowed to purchase this item. If specified, no other address will be able to take the order, unless its value is the null address.
436+
* @param options.englishAuction If true, the order will be listed as an English auction.
432437
* @returns The {@link OrderV2} that was created.
433438
*
434439
* @throws Error if the asset does not contain a token id.
@@ -448,6 +453,7 @@ export class OpenSeaSDK {
448453
expirationTime,
449454
paymentTokenAddress = ethers.constants.AddressZero,
450455
buyerAddress,
456+
englishAuction,
451457
}: {
452458
asset: Asset;
453459
accountAddress: string;
@@ -460,6 +466,7 @@ export class OpenSeaSDK {
460466
expirationTime?: number;
461467
paymentTokenAddress?: string;
462468
buyerAddress?: string;
469+
englishAuction?: boolean;
463470
}): Promise<OrderV2> {
464471
await this._requireAccountIsAvailable(accountAddress);
465472

@@ -477,6 +484,12 @@ export class OpenSeaSDK {
477484
[BigNumber.from(quantity ?? 1)],
478485
);
479486

487+
if (englishAuction && paymentTokenAddress == ethers.constants.AddressZero) {
488+
throw new Error(
489+
`English auctions must use wrapped ETH or an ERC-20 token.`,
490+
);
491+
}
492+
480493
const { basePrice, endPrice } = await this._getPriceParameters(
481494
OrderSide.Sell,
482495
paymentTokenAddress,
@@ -514,11 +527,11 @@ export class OpenSeaSDK {
514527
endTime:
515528
expirationTime?.toString() ??
516529
getMaxOrderExpirationTimestamp().toString(),
517-
zone: DEFAULT_ZONE,
530+
zone: englishAuction ? ENGLISH_AUCTION_ZONE : DEFAULT_ZONE,
518531
domain,
519532
salt: BigNumber.from(salt ?? 0).toString(),
520-
restrictedByZone: false,
521-
allowPartialFills: true,
533+
restrictedByZone: englishAuction ? true : false,
534+
allowPartialFills: englishAuction ? false : true,
522535
},
523536
accountAddress,
524537
);
@@ -988,17 +1001,13 @@ export class OpenSeaSDK {
9881001
* @param expirationTime When the auction expires, or 0 if never.
9891002
* @param startAmount The base value for the order, in the token's main units (e.g. ETH instead of wei)
9901003
* @param endAmount The end value for the order, in the token's main units (e.g. ETH instead of wei). If unspecified, the order's `extra` attribute will be 0
991-
* @param waitingForBestCounterOrder
992-
* @param englishAuctionReservePrice
9931004
*/
9941005
private async _getPriceParameters(
9951006
orderSide: OrderSide,
9961007
tokenAddress: string,
9971008
expirationTime: BigNumberish,
9981009
startAmount: BigNumberish,
9991010
endAmount?: BigNumberish,
1000-
waitingForBestCounterOrder = false,
1001-
englishAuctionReservePrice?: BigNumberish,
10021011
) {
10031012
const isEther = tokenAddress === ethers.constants.AddressZero;
10041013
let paymentToken: OpenSeaFungibleToken | undefined;
@@ -1025,9 +1034,6 @@ export class OpenSeaSDK {
10251034
const basePrice = startAmountWei;
10261035
const endPrice = endAmountWei;
10271036
const extra = priceDiffWei;
1028-
const reservePrice = englishAuctionReservePrice
1029-
? ethers.utils.parseUnits(startAmount.toString(), decimals)
1030-
: undefined;
10311037

10321038
// Validation
10331039
if (startAmount == null || startAmountWei.lt(0)) {
@@ -1051,11 +1057,6 @@ export class OpenSeaSDK {
10511057
);
10521058
}
10531059
}
1054-
if (isEther && waitingForBestCounterOrder) {
1055-
throw new Error(
1056-
`English auctions must use wrapped ETH or an ERC-20 token.`,
1057-
);
1058-
}
10591060
if (isEther && orderSide === OrderSide.Buy) {
10601061
throw new Error(`Offers must use wrapped ETH or an ERC-20 token.`);
10611062
}
@@ -1069,18 +1070,7 @@ export class OpenSeaSDK {
10691070
"Expiration time must be set if order will change in price.",
10701071
);
10711072
}
1072-
const reservePriceIsDefinedAndNonZero =
1073-
reservePrice && !reservePrice.isZero();
1074-
if (reservePriceIsDefinedAndNonZero && !waitingForBestCounterOrder) {
1075-
throw new Error("Reserve prices may only be set on English auctions.");
1076-
}
1077-
if (reservePriceIsDefinedAndNonZero && reservePrice?.lt(startAmountWei)) {
1078-
throw new Error(
1079-
"Reserve price must be greater than or equal to the start amount.",
1080-
);
1081-
}
1082-
1083-
return { basePrice, extra, paymentToken, reservePrice, endPrice };
1073+
return { basePrice, extra, paymentToken, endPrice };
10841074
}
10851075

10861076
private _dispatch(event: EventType, data: EventData) {

src/types.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,6 @@ export enum FeeMethod {
164164
SplitFee = 1,
165165
}
166166

167-
/**
168-
* Type of sale.
169-
*/
170-
export enum SaleKind {
171-
FixedPrice = 0,
172-
DutchAuction = 1,
173-
}
174-
175167
/**
176168
* Types of asset contracts
177169
* Given by the asset_contract_type in the OpenSea API
@@ -675,13 +667,6 @@ export type ExchangeMetadata =
675667
| ExchangeMetadataForAsset
676668
| ExchangeMetadataForBundle;
677669

678-
export enum HowToCall {
679-
Call = 0,
680-
DelegateCall = 1,
681-
StaticCall = 2,
682-
Create = 3,
683-
}
684-
685670
export interface UnsignedOrder {
686671
hash?: string;
687672
exchange: string;
@@ -706,15 +691,8 @@ export interface UnsignedOrder {
706691

707692
feeMethod: FeeMethod;
708693
side: OrderSide;
709-
saleKind: SaleKind;
710-
howToCall: HowToCall;
711694
quantity: BigNumber;
712695

713-
// OpenSea-specific
714-
makerReferrerFee: BigNumber;
715-
waitingForBestCounterOrder: boolean;
716-
englishAuctionReservePrice?: BigNumber;
717-
718696
metadata: ExchangeMetadata;
719697
}
720698

@@ -741,7 +719,6 @@ export interface Order extends UnsignedOrder, Partial<ECSignature> {
741719
markedInvalid?: boolean;
742720
asset?: OpenSeaAsset;
743721
assetBundle?: OpenSeaAssetBundle;
744-
nonce?: number;
745722
}
746723

747724
/**
@@ -761,9 +738,7 @@ export interface OrderJSON extends Partial<ECSignature> {
761738
feeRecipient: string;
762739
feeMethod: number;
763740
side: number;
764-
saleKind: number;
765741
target: string;
766-
howToCall: number;
767742
calldata: string;
768743
replacementPattern: string;
769744
staticTarget: string;
@@ -775,15 +750,11 @@ export interface OrderJSON extends Partial<ECSignature> {
775750
expirationTime: number | string;
776751
salt: string;
777752

778-
makerReferrerFee: string;
779753
quantity: string;
780-
englishAuctionReservePrice: string | undefined;
781754

782755
// createdTime is undefined when order hasn't been posted yet
783756
createdTime?: number | string;
784757
metadata: ExchangeMetadata;
785-
786-
nonce?: number;
787758
}
788759

789760
/**

test/integration/postOrder.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
sdkPolygon,
1111
walletAddress,
1212
} from "./setup";
13+
import { ENGLISH_AUCTION_ZONE } from "../../src/constants";
1314
import { getWETHAddress } from "../../src/utils";
1415
import { OFFER_AMOUNT } from "../utils/constants";
1516
import { expectValidOrder } from "../utils/utils";
@@ -57,6 +58,35 @@ suite("SDK: order posting", () => {
5758
expectValidOrder(order);
5859
});
5960

61+
test("Post Auction Sell Order - Mainnet", async function () {
62+
if (!TOKEN_ADDRESS_MAINNET || !TOKEN_ID_MAINNET) {
63+
this.skip();
64+
}
65+
const sellOrder = {
66+
accountAddress: walletAddress,
67+
startAmount: LISTING_AMOUNT,
68+
asset: {
69+
tokenAddress: TOKEN_ADDRESS_MAINNET as string,
70+
tokenId: TOKEN_ID_MAINNET as string,
71+
},
72+
englishAuction: true,
73+
};
74+
try {
75+
const order = await sdk.createSellOrder(sellOrder);
76+
expectValidOrder(order);
77+
expect(order.protocolData.parameters.zone.toLowerCase()).to.equal(
78+
ENGLISH_AUCTION_ZONE,
79+
);
80+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
81+
} catch (error: any) {
82+
expect(
83+
error.message.includes(
84+
"There is already a live auction for this item. You can only have one auction live at any time.",
85+
),
86+
);
87+
}
88+
});
89+
6090
test("Post Sell Order - Polygon", async function () {
6191
if (!TOKEN_ADDRESS_POLYGON || !TOKEN_ID_POLYGON) {
6292
this.skip();

0 commit comments

Comments
 (0)