Skip to content

Commit

Permalink
collection offers (#868)
Browse files Browse the repository at this point in the history
* add collection offer posting
  • Loading branch information
cterech authored Mar 9, 2023
1 parent 24d8fd6 commit ac61e81
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 26 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages

name: Node.js Package

on:
release:
types: [created]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16.11.0
- run: npm ci
- run: npm test
- run: npm run build
- run: yarn build

publish-npm:
needs: build
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16.11.0
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"coverage-report": "nyc report --reporter=text-lcov | coveralls",
"docs-build": "typedoc --out docs src/index.ts",
"eslint:check": "eslint . --max-warnings 0 --ext .js,.ts",
"integration_tests": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' TS_NODE_TRANSPILE_ONLY=true nyc mocha -r ts-node/register src/__integration_tests__/**/*.ts --timeout 15000",
"lint:check": "concurrently \"yarn check-types\" \"yarn prettier:check\" \"yarn eslint:check\"",
"prepare": "husky install && npm run build",
"test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' TS_NODE_TRANSPILE_ONLY=true nyc mocha -r ts-node/register src/__tests__/**/*.ts --timeout 15000",
Expand Down
16 changes: 16 additions & 0 deletions src/__integration_tests__/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Integration Tests

These tests were built to test the order posting functionality of the SDK. Signing and posting order requires a bit more setup than the other tests, so we detail that here.

### Environment variables:

- `API_KEY`: your OpenSea mainnet API key.
- `WALLET_ADDRESS`: the wallet address to send your offer from.
- `WALLET_PRIV_KEY`: the private key to your wallet. This is required to sign the order.
- `ALCHEMY_API_KEY`: your Alchemy API key.

### How to run:

```
API_KEY="..." WALLET_ADDRESS="..." WALLET_PRIV_KEY="..." ALCHEMY_API_KEY="..." npm run integration_tests
```
49 changes: 49 additions & 0 deletions src/__integration_tests__/postOrder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ethers } from "ethers";
import { suite, test } from "mocha";
import Web3 from "web3";
import {
MAINNET_API_KEY,
WALLET_ADDRESS,
WALLET_PRIV_KEY,
ALCHEMY_API_KEY,
WETH_ADDRESS,
} from "../__tests__/constants";
import { OpenSeaSDK } from "../index";
import { Network } from "../types";

const webProvider = new Web3.providers.HttpProvider(
`https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`
);

const rpcProvider = new ethers.providers.JsonRpcProvider(
`https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`
);

const wallet = new ethers.Wallet(
WALLET_PRIV_KEY ? WALLET_PRIV_KEY : "",
rpcProvider
);

const sdk = new OpenSeaSDK(
webProvider,
{
networkName: Network.Main,
apiKey: MAINNET_API_KEY,
},
(line) => console.info(`MAINNET: ${line}`),
wallet
);

suite("SDK: order posting", () => {
test("Post collection offer", async () => {
const collection = await sdk.api.getCollection("cool-cats-nft");
const postOrderRequest = {
collectionSlug: collection.slug,
accountAddress: WALLET_ADDRESS ? WALLET_ADDRESS : "",
amount: "0.004",
quantity: 1,
paymentTokenAddress: WETH_ADDRESS,
};
await sdk.createCollectionOffer(postOrderRequest);
});
});
4 changes: 4 additions & 0 deletions src/__tests__/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { OpenSeaAPI } from "../api";
import { Network } from "../types";

export const MAINNET_API_KEY = process.env.API_KEY;
export const WALLET_ADDRESS = process.env.WALLET_ADDRESS;
export const WALLET_PRIV_KEY = process.env.WALLET_PRIV_KEY;
export const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;

export const mainApi = new OpenSeaAPI(
{
Expand All @@ -24,3 +27,4 @@ export const BAYC_CONTRACT_ADDRESS =
"0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d";
export const BAYC_TOKEN_ID = "1";
export const BAYC_TOKEN_IDS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
export const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
62 changes: 62 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import {
ORDERBOOK_PATH,
} from "./constants";
import {
BuildOfferResponse,
FulfillmentDataResponse,
GetCollectionResponse,
OrderAPIOptions,
OrderSide,
OrdersPostQueryResponse,
OrdersQueryOptions,
OrdersQueryResponse,
OrderV2,
PostOfferResponse,
ProtocolData,
QueryCursors,
} from "./orders/types";
Expand All @@ -26,6 +29,11 @@ import {
getFulfillmentDataPath,
getFulfillListingPayload,
getFulfillOfferPayload,
getBuildOfferPath,
getBuildCollectionOfferPayload,
getCollectionPath,
getPostCollectionOfferPath,
getPostCollectionOfferPayload,
} from "./orders/utils";
import {
Network,
Expand All @@ -34,6 +42,7 @@ import {
OpenSeaAssetBundle,
OpenSeaAssetBundleQuery,
OpenSeaAssetQuery,
OpenSeaCollection,
OpenSeaFungibleToken,
OpenSeaFungibleTokenQuery,
} from "./types";
Expand All @@ -42,6 +51,7 @@ import {
assetFromJSON,
delay,
tokenFromJSON,
collectionFromJSON,
} from "./utils/utils";

export class OpenSeaAPI {
Expand Down Expand Up @@ -196,6 +206,49 @@ export class OpenSeaAPI {
return deserializeOrder(response.order);
}

/**
* Build an offer
*/
public async buildOffer(
offererAddress: string,
quantity: number,
collectionSlug: string
): Promise<BuildOfferResponse> {
const payload = getBuildCollectionOfferPayload(
offererAddress,
quantity,
collectionSlug
);
const response = await this.post<BuildOfferResponse>(
getBuildOfferPath(),
payload
);
return response;
}

/**
* Post collection offer
*/
public async postCollectionOffer(
order: ProtocolData,
slug: string,
retries = 0
): Promise<PostOfferResponse | null> {
const payload = getPostCollectionOfferPayload(slug, order);
console.log("Post Order Payload");
console.log(JSON.stringify(payload, null, 4));
try {
return await this.post<PostOfferResponse>(
getPostCollectionOfferPath(),
payload
);
} catch (error) {
_throwOrContinue(error, retries);
await delay(1000);
return this.postCollectionOffer(order, slug, retries - 1);
}
}

/**
* Create a whitelist entry for an asset to prevent others from buying.
* Buyers will have to have verified at least one of the emails
Expand Down Expand Up @@ -294,6 +347,15 @@ export class OpenSeaAPI {
};
}

/**
* Fetch a collection through the API
*/
public async getCollection(slug: string): Promise<OpenSeaCollection> {
const path = getCollectionPath(slug);
const response = await this.get<GetCollectionResponse>(path);
return collectionFromJSON(response.collection);
}

/**
* Fetch list of fungible tokens from the API matching parameters
* @param query Query to use for getting orders. A subset of parameters on the `OpenSeaAssetJSON` type is supported
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const UNISWAP_FACTORY_ADDRESS_RINKEBY =
"0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36";
export const DEFAULT_WRAPPED_NFT_LIQUIDATION_UNISWAP_SLIPPAGE_IN_BASIS_POINTS = 1000;
export const MIN_EXPIRATION_MINUTES = 15;
export const MAX_EXPIRATION_MONTHS = 3;
export const MAX_EXPIRATION_MONTHS = 1;
export const ORDER_MATCHING_LATENCY_SECONDS = 60 * 60 * 24 * 7;
const ORDERBOOK_VERSION = 1 as number;
export const API_BASE_MAINNET = "https://api.opensea.io";
Expand Down
40 changes: 39 additions & 1 deletion src/orders/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { OrderWithCounter } from "@opensea/seaport-js/lib/types";
import {
ConsiderationItem,
OrderWithCounter,
} from "@opensea/seaport-js/lib/types";
import { OpenSeaAccount, OpenSeaAssetBundle } from "../types";

// Protocol data
Expand Down Expand Up @@ -58,6 +61,41 @@ type Transaction = {
input_data: object;
};

export type BuildOfferResponse = {
partialParameters: PartialParameters;
};

export type GetCollectionResponse = {
collection: object;
};

export type PostOfferResponse = {
order_hash: string;
chain: string;
criteria: Criteria;
protocol_data: ProtocolData;
protocol_address: string;
};

type Criteria = {
collection: CollectionCriteria;
contract?: ContractCriteria;
};

type CollectionCriteria = {
slug: string;
};

type ContractCriteria = {
address: string;
};

type PartialParameters = {
consideration: ConsiderationItem[];
zone: string;
zoneHash: string;
};

// API query types
type OpenOrderOrderingOption = "created_date" | "eth_price";
type OrderByDirection = "asc" | "desc";
Expand Down
41 changes: 41 additions & 0 deletions src/orders/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OrderSide,
OrderV2,
SerializedOrderV2,
ProtocolData,
} from "./types";

const NETWORK_TO_CHAIN = {
Expand All @@ -24,6 +25,46 @@ export const getOrdersAPIPath = (
return `/v2/orders/${chain}/${protocol}/${sidePath}`;
};

export const getCollectionPath = (slug: string) => {
return `/api/v1/collection/${slug}`;
};

export const getBuildOfferPath = () => {
return `/v2/offers/build`;
};

export const getPostCollectionOfferPath = () => {
return `/v2/offers`;
};

export const getPostCollectionOfferPayload = (
collectionSlug: string,
protocol_data: ProtocolData
) => {
return {
criteria: {
collection: { slug: collectionSlug },
},
protocol_data,
};
};

export const getBuildCollectionOfferPayload = (
offererAddress: string,
quantity: number,
collectionSlug: string
) => {
return {
offerer: offererAddress,
quantity,
criteria: {
collection: {
slug: collectionSlug,
},
},
};
};

export const getFulfillmentDataPath = (side: OrderSide) => {
const sidePath = side === "ask" ? "listings" : "offers";
return `/v2/${sidePath}/fulfillment_data`;
Expand Down
Loading

0 comments on commit ac61e81

Please sign in to comment.