diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index cb36cbc2b..ae9e1719a 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -65,9 +65,12 @@ jobs: env: OPENSEA_API_KEY: ${{ secrets.OPENSEA_API_KEY }} ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + ALCHEMY_API_KEY_POLYGON: ${{ secrets.ALCHEMY_API_KEY_POLYGON }} WALLET_PRIV_KEY: ${{ secrets.WALLET_PRIV_KEY }} SELL_ORDER_CONTRACT_ADDRESS: ${{ secrets.SELL_ORDER_CONTRACT_ADDRESS }} SELL_ORDER_TOKEN_ID: ${{ secrets.SELL_ORDER_TOKEN_ID }} + SELL_ORDER_CONTRACT_ADDRESS_POLYGON: ${{ secrets.SELL_ORDER_CONTRACT_ADDRESS_POLYGON }} + SELL_ORDER_TOKEN_ID_POLYGON: ${{ secrets.SELL_ORDER_TOKEN_ID_POLYGON }} run: npm run test:integration test-earliest-node-engine-support: diff --git a/package-lock.json b/package-lock.json index cfad5c6f2..c7ea33b3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opensea-js", - "version": "6.0.5", + "version": "6.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "opensea-js", - "version": "6.0.5", + "version": "6.1.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1e259d91f..a48e289e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opensea-js", - "version": "6.1.0", + "version": "6.1.1", "description": "JavaScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data!", "license": "MIT", "author": "OpenSea Developers", @@ -33,7 +33,7 @@ "prettier:check:package.json": "prettier-package-json --list-different", "prettier:fix": "prettier --write .", "test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' TS_NODE_TRANSPILE_ONLY=true nyc mocha -r ts-node/register test/**/*.spec.ts --exclude test/integration/**/*.ts --timeout 15000", - "test:integration": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' TS_NODE_TRANSPILE_ONLY=true nyc mocha -r ts-node/register -r dotenv/config test/integration/**/*.spec.ts --timeout 15000" + "test:integration": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' TS_NODE_TRANSPILE_ONLY=true nyc mocha -r ts-node/register -r dotenv/config test/integration/**/*.spec.ts --timeout 25000" }, "types": "lib/index.d.ts", "dependencies": { diff --git a/src/api/types.ts b/src/api/types.ts index 728eeec47..c886f70ac 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -45,7 +45,7 @@ export type GetNFTResponse = { nft: NFT; }; -type NFT = { +export type NFT = { identifier: string; collection: string; contract: string; diff --git a/src/sdk.ts b/src/sdk.ts index fd18000eb..8153c8ab7 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -17,7 +17,7 @@ import { } from "ethers"; import { parseEther } from "ethers/lib/utils"; import { OpenSeaAPI } from "./api/api"; -import { PostOfferResponse } from "./api/types"; +import { PostOfferResponse, NFT } from "./api/types"; import { INVERSE_BASIS_POINT, DEFAULT_ZONE } from "./constants"; import { constructPrivateListingCounterOrder, @@ -42,6 +42,7 @@ import { OpenSeaCollection, OrderSide, TokenStandard, + OpenSeaFungibleToken, } from "./types"; import { delay, @@ -301,6 +302,23 @@ export class OpenSeaSDK { })); } + private getNFTItems( + nfts: NFT[], + quantities: BigNumber[] = [] + ): CreateInputItem[] { + return nfts.map((nft, index) => ({ + itemType: getAssetItemType( + nft.token_standard.toUpperCase() as TokenStandard + ), + token: + getAddressAfterRemappingSharedStorefrontAddressToLazyMintAdapterAddress( + nft.contract + ), + identifier: nft.identifier ?? undefined, + amount: quantities[index].toString() ?? "1", + })); + } + /** * Create a buy order to make an offer on an asset. * @param options Options for creating the buy order @@ -343,9 +361,13 @@ export class OpenSeaSDK { } paymentTokenAddress = paymentTokenAddress ?? getWETHAddress(this.chain); - const openseaAsset = await this.api.getAsset(asset); - const considerationAssetItems = this.getAssetItems( - [openseaAsset], + const { nft } = await this.api.getNFT( + this.chain, + asset.tokenAddress, + asset.tokenId + ); + const considerationAssetItems = this.getNFTItems( + [nft], [BigNumber.from(quantity ?? 1)] ); @@ -356,12 +378,13 @@ export class OpenSeaSDK { startAmount ); - const { openseaSellerFees, collectionSellerFees: collectionSellerFees } = - await this.getFees({ - collection: openseaAsset.collection, - paymentTokenAddress, - startAmount: basePrice, - }); + const collection = await this.api.getCollection(nft.collection); + + const { openseaSellerFees, collectionSellerFees } = await this.getFees({ + collection, + paymentTokenAddress, + startAmount: basePrice, + }); const considerationFeeItems = [ ...openseaSellerFees, ...collectionSellerFees, @@ -440,16 +463,14 @@ export class OpenSeaSDK { if (!asset.tokenId) { throw new Error("Asset must have a tokenId"); } - //TODO: Make this function multichain compatible - if (this.chain != Chain.Mainnet && this.chain != Chain.Goerli) { - throw new Error( - `Creating orders on ${this.chain} not yet supported by the SDK.` - ); - } - const openseaAsset = await this.api.getAsset(asset); - const offerAssetItems = this.getAssetItems( - [openseaAsset], + const { nft } = await this.api.getNFT( + this.chain, + asset.tokenAddress, + asset.tokenId + ); + const offerAssetItems = this.getNFTItems( + [nft], [BigNumber.from(quantity ?? 1)] ); @@ -461,16 +482,15 @@ export class OpenSeaSDK { endAmount ?? undefined ); - const { - sellerFee, - openseaSellerFees, - collectionSellerFees: collectionSellerFees, - } = await this.getFees({ - collection: openseaAsset.collection, - paymentTokenAddress, - startAmount: basePrice, - endAmount: endPrice, - }); + const collection = await this.api.getCollection(nft.collection); + + const { sellerFee, openseaSellerFees, collectionSellerFees } = + await this.getFees({ + collection, + paymentTokenAddress, + startAmount: basePrice, + endAmount: endPrice, + }); const considerationFeeItems = [ sellerFee, ...openseaSellerFees, @@ -550,13 +570,12 @@ export class OpenSeaSDK { expirationTime ?? getMaxOrderExpirationTimestamp(), amount ); - const { openseaSellerFees, collectionSellerFees: collectionSellerFees } = - await this.getFees({ - collection, - paymentTokenAddress, - startAmount: basePrice, - endAmount: basePrice, - }); + const { openseaSellerFees, collectionSellerFees } = await this.getFees({ + collection, + paymentTokenAddress, + startAmount: basePrice, + endAmount: basePrice, + }); const considerationItems = [ convertedConsiderationItem, @@ -938,7 +957,7 @@ export class OpenSeaSDK { englishAuctionReservePrice?: BigNumberish ) { const isEther = tokenAddress === ethers.constants.AddressZero; - let paymentToken; + let paymentToken: OpenSeaFungibleToken | undefined; if (!isEther) { const { tokens } = await this.api.getPaymentTokens({ address: tokenAddress.toLowerCase(), @@ -971,7 +990,22 @@ export class OpenSeaSDK { throw new Error(`Starting price must be a number >= 0`); } if (!isEther && !paymentToken) { - throw new Error(`No ERC-20 token found for ${tokenAddress}`); + try { + if ( + tokenAddress.toLowerCase() == getWETHAddress(this.chain).toLowerCase() + ) { + paymentToken = { + name: "Wrapped Ether", + symbol: "WETH", + decimals: 18, + address: tokenAddress, + }; + } + } catch (error) { + throw new Error( + `No ERC-20 token found for ${tokenAddress}, only WETH is currently supported for chains other than Mainnet Ethereum` + ); + } } if (isEther && waitingForBestCounterOrder) { throw new Error( diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 8d71de98d..1601a639c 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -332,6 +332,23 @@ export const getWETHAddress = (chain: Chain) => { return "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6"; case Chain.Sepolia: return "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"; + case Chain.Klaytn: + return "0xfd844c2fca5e595004b17615f891620d1cb9bbb2"; + case Chain.Baobab: + return "0x9330dd6713c8328a8d82b14e3f60a0f0b4cc7bfb"; + case Chain.Avalanche: + return "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"; + case Chain.Fuji: + return "0xd00ae08403B9bbb9124bB305C09058E32C39A48c"; + case Chain.BNB: + return "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"; + case Chain.BNBTestnet: + return "0xae13d989dac2f0debff460ac112a837c89baa7cd"; + // OP Chains have weth at the same address + case Chain.Optimism: + case Chain.Zora: + case Chain.ZoraTestnet: + return "0x4200000000000000000000000000000000000006"; default: throw new Error(`WETH is not supported on ${chain}`); } diff --git a/test/integration/README.md b/test/integration/README.md index 108452cfc..b675bb8e1 100644 --- a/test/integration/README.md +++ b/test/integration/README.md @@ -7,20 +7,25 @@ These tests were built to test the order posting functionality of the SDK. Signi Environment variables for integration tests are set using `.env`. This file is not in the source code for the repository so you will need to create a file with the following fields: ```bash -OPENSEA_API_KEY = "" # OpenSea API Key -ALCHEMY_API_KEY = "" # Alchemy API Key -WALLET_PRIV_KEY = "0x" # Wallet private key +OPENSEA_API_KEY="" # OpenSea API Key +ALCHEMY_API_KEY="" # Alchemy API Key for ETH Mainnet +ALCHEMY_API_KEY_POLYGON="" # Alchemy API Key for Polygon +WALLET_PRIV_KEY="0x" # Wallet private key # The following needs to be an NFT owned by the wallet address derived from WALLET_PRIV_KEY -SELL_ORDER_CONTRACT_ADDRESS = "0x" -SELL_ORDER_TOKEN_ID = "123" +## Mainnet +SELL_ORDER_CONTRACT_ADDRESS="0x" +SELL_ORDER_TOKEN_ID="123" +## Polygon +SELL_ORDER_CONTRACT_ADDRESS_POLYGON="0x" +SELL_ORDER_TOKEN_ID_POLYGON="123" ``` Optional: ```bash -OFFER_AMOUNT = "0.004" # Defaults to 0.004 -LISTING_AMOUNT = "40" # Defaults to 40 +OFFER_AMOUNT="0.004" # Defaults to 0.004 +LISTING_AMOUNT="40" # Defaults to 40 ``` #### WETH Tests diff --git a/test/integration/postOrder.spec.ts b/test/integration/postOrder.spec.ts index f01e6c0d2..5e5265eca 100644 --- a/test/integration/postOrder.spec.ts +++ b/test/integration/postOrder.spec.ts @@ -2,9 +2,12 @@ import { expect } from "chai"; import { suite, test } from "mocha"; import { LISTING_AMOUNT, - TOKEN_ADDRESS, - TOKEN_ID, + TOKEN_ADDRESS_MAINNET, + TOKEN_ADDRESS_POLYGON, + TOKEN_ID_MAINNET, + TOKEN_ID_POLYGON, sdk, + sdkPolygon, walletAddress, } from "./setup"; import { getWETHAddress } from "../../src/utils"; @@ -12,7 +15,7 @@ import { OFFER_AMOUNT } from "../utils/constants"; import { expectValidOrder } from "../utils/utils"; suite("SDK: order posting", () => { - test("Post Buy Order", async () => { + test("Post Buy Order - Mainnet", async () => { const buyOrder = { accountAddress: walletAddress, startAmount: +OFFER_AMOUNT, @@ -21,32 +24,56 @@ suite("SDK: order posting", () => { tokenId: "2288", }, }; - const order = await sdk.createBuyOrder(buyOrder); + expectValidOrder(order); + }); + test("Post Buy Order - Polygon", async () => { + const buyOrder = { + accountAddress: walletAddress, + startAmount: +OFFER_AMOUNT, + asset: { + tokenAddress: "0x1a92f7381b9f03921564a437210bb9396471050c", + tokenId: "2288", + }, + }; + const order = await sdk.createBuyOrder(buyOrder); expectValidOrder(order); }); - test("Post Sell Order", async function () { - if (!TOKEN_ADDRESS || !TOKEN_ID) { + test("Post Sell Order - Mainnet", async function () { + if (!TOKEN_ADDRESS_MAINNET || !TOKEN_ID_MAINNET) { this.skip(); } - const sellOrder = { accountAddress: walletAddress, startAmount: LISTING_AMOUNT, asset: { - tokenAddress: TOKEN_ADDRESS, - tokenId: TOKEN_ID, + tokenAddress: TOKEN_ADDRESS_MAINNET as string, + tokenId: TOKEN_ID_MAINNET as string, }, }; - const order = await sdk.createSellOrder(sellOrder); + expectValidOrder(order); + }); + test("Post Sell Order - Polygon", async function () { + if (!TOKEN_ADDRESS_POLYGON || !TOKEN_ID_POLYGON) { + this.skip(); + } + const sellOrder = { + accountAddress: walletAddress, + startAmount: LISTING_AMOUNT, + asset: { + tokenAddress: TOKEN_ADDRESS_POLYGON, + tokenId: TOKEN_ID_POLYGON, + }, + }; + const order = await sdkPolygon.createSellOrder(sellOrder); expectValidOrder(order); }); - test("Post collection offer", async () => { + test("Post Collection Offer - Mainnet", async () => { const collection = await sdk.api.getCollection("cool-cats-nft"); const paymentTokenAddress = getWETHAddress(sdk.chain); const postOrderRequest = { @@ -56,10 +83,23 @@ suite("SDK: order posting", () => { quantity: 1, paymentTokenAddress, }; - const offerResponse = await sdk.createCollectionOffer(postOrderRequest); + expect(offerResponse).to.exist.and.to.have.property("protocol_data"); + }); - expect(offerResponse).to.exist; + test("Post Collection Offer - Polygon", async () => { + const collection = await sdkPolygon.api.getCollection("arttoken-1155-4"); + const paymentTokenAddress = getWETHAddress(sdkPolygon.chain); + const postOrderRequest = { + collectionSlug: collection.slug, + accountAddress: walletAddress, + amount: OFFER_AMOUNT, + quantity: 1, + paymentTokenAddress, + }; + const offerResponse = await sdkPolygon.createCollectionOffer( + postOrderRequest + ); expect(offerResponse).to.exist.and.to.have.property("protocol_data"); }); }); diff --git a/test/integration/setup.ts b/test/integration/setup.ts index f38b040e3..6f86d4c43 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -3,7 +3,8 @@ import { OpenSeaSDK } from "../../src/sdk"; import { Chain } from "../../src/types"; import { MAINNET_API_KEY, - RPC_PROVIDER, + RPC_PROVIDER_MAINNET, + RPC_PROVIDER_POLYGON, WALLET_PRIV_KEY, } from "../utils/constants"; @@ -13,20 +14,40 @@ for (const envVar of ["WALLET_PRIV_KEY"]) { } } -export const TOKEN_ADDRESS = process.env.SELL_ORDER_CONTRACT_ADDRESS; -export const TOKEN_ID = process.env.SELL_ORDER_TOKEN_ID; +export const TOKEN_ADDRESS_MAINNET = process.env.SELL_ORDER_CONTRACT_ADDRESS; +export const TOKEN_ID_MAINNET = process.env.SELL_ORDER_TOKEN_ID; +export const TOKEN_ADDRESS_POLYGON = + process.env.SELL_ORDER_CONTRACT_ADDRESS_POLYGON; +export const TOKEN_ID_POLYGON = process.env.SELL_ORDER_TOKEN_ID_POLYGON; export const LISTING_AMOUNT = process.env.LISTING_AMOUNT ?? "40"; export const ETH_TO_WRAP = process.env.ETH_TO_WRAP; -const wallet = new ethers.Wallet(WALLET_PRIV_KEY as string, RPC_PROVIDER); -export const walletAddress = wallet.address; +const walletMainnet = new ethers.Wallet( + WALLET_PRIV_KEY as string, + RPC_PROVIDER_MAINNET +); +const walletPolygon = new ethers.Wallet( + WALLET_PRIV_KEY as string, + RPC_PROVIDER_POLYGON +); +export const walletAddress = walletMainnet.address; export const sdk = new OpenSeaSDK( - RPC_PROVIDER, + RPC_PROVIDER_MAINNET, { chain: Chain.Mainnet, apiKey: MAINNET_API_KEY, }, (line) => console.info(`MAINNET: ${line}`), - wallet + walletMainnet +); + +export const sdkPolygon = new OpenSeaSDK( + RPC_PROVIDER_POLYGON, + { + chain: Chain.Polygon, + apiKey: MAINNET_API_KEY, + }, + (line) => console.info(`POLYGON: ${line}`), + walletPolygon ); diff --git a/test/sdk/fees.spec.ts b/test/sdk/fees.spec.ts index 67301a03b..d9feda7c6 100644 --- a/test/sdk/fees.spec.ts +++ b/test/sdk/fees.spec.ts @@ -7,11 +7,11 @@ import { BAYC_CONTRACT_ADDRESS, BAYC_TOKEN_ID, MAINNET_API_KEY, - RPC_PROVIDER, + RPC_PROVIDER_MAINNET, } from "../utils/constants"; const client = new OpenSeaSDK( - RPC_PROVIDER, + RPC_PROVIDER_MAINNET, { chain: Chain.Mainnet, apiKey: MAINNET_API_KEY, diff --git a/test/sdk/misc.spec.ts b/test/sdk/misc.spec.ts index 7e0033045..20523c598 100644 --- a/test/sdk/misc.spec.ts +++ b/test/sdk/misc.spec.ts @@ -11,11 +11,11 @@ import { getAddressAfterRemappingSharedStorefrontAddressToLazyMintAdapterAddress import { DAPPER_ADDRESS, MAINNET_API_KEY, - RPC_PROVIDER, + RPC_PROVIDER_MAINNET, } from "../utils/constants"; const client = new OpenSeaSDK( - RPC_PROVIDER, + RPC_PROVIDER_MAINNET, { chain: Chain.Mainnet, apiKey: MAINNET_API_KEY, diff --git a/test/sdk/orders.spec.ts b/test/sdk/orders.spec.ts index 121be3ddd..8807e4815 100644 --- a/test/sdk/orders.spec.ts +++ b/test/sdk/orders.spec.ts @@ -2,10 +2,10 @@ import { assert } from "chai"; import { suite, test } from "mocha"; import { OpenSeaSDK } from "../../src/index"; import { Chain } from "../../src/types"; -import { MAINNET_API_KEY, RPC_PROVIDER } from "../utils/constants"; +import { MAINNET_API_KEY, RPC_PROVIDER_MAINNET } from "../utils/constants"; const client = new OpenSeaSDK( - RPC_PROVIDER, + RPC_PROVIDER_MAINNET, { chain: Chain.Mainnet, apiKey: MAINNET_API_KEY, diff --git a/test/utils/constants.ts b/test/utils/constants.ts index 091c9a581..a26a9e7b4 100644 --- a/test/utils/constants.ts +++ b/test/utils/constants.ts @@ -5,9 +5,14 @@ import { Chain } from "../../src/types"; export const MAINNET_API_KEY = process.env.OPENSEA_API_KEY; export const WALLET_PRIV_KEY = process.env.WALLET_PRIV_KEY; -const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; -export const RPC_PROVIDER = new ethers.providers.JsonRpcProvider( - `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}` +const ALCHEMY_API_KEY_MAINNET = process.env.ALCHEMY_API_KEY; +const ALCHEMY_API_KEY_POLYGON = process.env.ALCHEMY_API_KEY_POLYGON; + +export const RPC_PROVIDER_MAINNET = new ethers.providers.JsonRpcProvider( + `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY_MAINNET}` +); +export const RPC_PROVIDER_POLYGON = new ethers.providers.JsonRpcProvider( + `https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY_POLYGON}` ); export const OFFER_AMOUNT = process.env.OFFER_AMOUNT ?? "0.004";