├── .config
└── typedoc.json
├── .eslintrc.js
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── BUG-FORM.yml
│ ├── FEATURE-FORM.yml
│ └── config.yml
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── cancel.yml
│ ├── code-quality.yml
│ ├── codeql-analysis.yml
│ ├── docs.yml
│ ├── npm-publish.yml
│ └── stale.yml
├── .gitignore
├── .husky
├── pre-commit
└── pre-push
├── .mocharc-integration.json
├── .mocharc-unit.json
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── developerDocs
├── advanced-use-cases.md
├── contributing.md
├── faq.md
├── getting-started.md
├── overview.md
├── quick-start.md
└── sdk-references.md
├── img
└── banner.png
├── package-lock.json
├── package.json
├── renovate.json
├── src
├── abi
│ ├── ERC1155.json
│ ├── ERC20.json
│ └── ERC721.json
├── api
│ ├── api.ts
│ ├── apiPaths.ts
│ ├── index.ts
│ └── types.ts
├── constants.ts
├── index.ts
├── orders
│ ├── privateListings.ts
│ ├── types.ts
│ └── utils.ts
├── sdk.ts
├── types.ts
└── utils
│ ├── index.ts
│ └── utils.ts
├── test
├── api
│ ├── api.spec.ts
│ ├── fulfillment.spec.ts
│ ├── getOrders.spec.ts
│ └── postOrder.validation.spec.ts
├── integration
│ ├── README.md
│ ├── getAccount.spec.ts
│ ├── getCollection.spec.ts
│ ├── getCollectionOffers.spec.ts
│ ├── getListingsAndOffers.spec.ts
│ ├── getNFTs.spec.ts
│ ├── postOrder.spec.ts
│ ├── setup.ts
│ └── wrapEth.spec.ts
├── sdk
│ ├── getBalance.spec.ts
│ ├── misc.spec.ts
│ └── orders.spec.ts
├── utils.spec.ts
└── utils
│ ├── constants.ts
│ ├── setup.ts
│ └── utils.ts
├── tsconfig.build.json
└── tsconfig.json
/.config/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "categorizeByGroup": false,
3 | "categoryOrder": ["Main Classes", "API Response Types", "API Models", "*"],
4 | "entryPoints": ["../src/index.ts"],
5 | "excludePrivate": true,
6 | "excludeReferences": true,
7 | "includeVersion": true,
8 | "out": "../docs",
9 | "validation": {
10 | "notExported": true,
11 | "invalidLink": true,
12 | "notDocumented": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const restrictedGlobals = require("confusing-browser-globals");
2 |
3 | module.exports = {
4 | env: {
5 | browser: true,
6 | node: true,
7 | },
8 | root: true,
9 | ignorePatterns: ["docs", "lib", "coverage", "src/typechain"],
10 | reportUnusedDisableDirectives: true,
11 | parser: "@typescript-eslint/parser",
12 | plugins: ["@typescript-eslint", "import", "prettier"],
13 |
14 | extends: [
15 | "eslint:recommended",
16 | "plugin:@typescript-eslint/recommended",
17 | "plugin:import/errors",
18 | "plugin:import/warnings",
19 | "plugin:import/typescript",
20 | "plugin:prettier/recommended",
21 | ],
22 | rules: {
23 | "no-restricted-globals": ["error"].concat(restrictedGlobals),
24 | "no-restricted-imports": [
25 | "error",
26 | {
27 | patterns: [
28 | {
29 | group: ["src/**", "!src/*"],
30 | message: "Please use relative import for `src` files.",
31 | },
32 | ],
33 | },
34 | ],
35 | curly: ["error"],
36 | "@typescript-eslint/no-unused-vars": [
37 | "error",
38 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
39 | ],
40 | "@typescript-eslint/explicit-module-boundary-types": "off",
41 | "@typescript-eslint/no-empty-interface": "off",
42 | "@typescript-eslint/no-var-requires": "off",
43 | "import/order": [
44 | "error",
45 | {
46 | groups: ["builtin", "external", "internal"],
47 | "newlines-between": "never",
48 | alphabetize: {
49 | order: "asc",
50 | caseInsensitive: true,
51 | },
52 | },
53 | ],
54 | "import/no-unused-modules": [1, { unusedExports: true }],
55 | "no-control-regex": "off",
56 |
57 | "object-shorthand": ["error", "always"],
58 | },
59 | settings: {
60 | "import/resolver": {
61 | node: {
62 | extensions: [".js", ".ts", ".tsx", ".json"],
63 | },
64 | typescript: {
65 | alwaysTryTypes: true,
66 | project: "src",
67 | },
68 | },
69 | },
70 | };
71 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @ProjectOpenSea/protocol
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG-FORM.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: File a bug report
3 | labels: ["bug"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Please ensure that the bug has not already been filed in the issue tracker.
9 |
10 | Thanks for taking the time to report this bug!
11 | - type: dropdown
12 | attributes:
13 | label: Component
14 | description: What component is the bug in?
15 | multiple: true
16 | options:
17 | - API
18 | - Utils
19 | - Other (please describe)
20 | validations:
21 | required: true
22 | - type: checkboxes
23 | attributes:
24 | label: Have you ensured that all of these are up to date?
25 | options:
26 | - label: opensea-js
27 | - label: Node (minimum v16)
28 | - type: input
29 | attributes:
30 | label: What version of opensea-js are you on?
31 | - type: input
32 | attributes:
33 | label: What function is the bug in?
34 | description: Leave empty if not relevant
35 | placeholder: "For example: fulfillOrder"
36 | - type: dropdown
37 | attributes:
38 | label: Operating System
39 | description: What operating system are you on?
40 | options:
41 | - Windows
42 | - macOS (Intel)
43 | - macOS (Apple Silicon)
44 | - Linux
45 | - type: textarea
46 | attributes:
47 | label: Describe the bug
48 | description: Please include relevant code snippets as well that can recreate the bug.
49 | validations:
50 | required: true
51 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE-FORM.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Suggest a feature
3 | labels: ["dev-feature-request"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Please ensure that the feature has not already been requested in the issue tracker.
9 |
10 | Thanks for helping us improve opensea-js!
11 | - type: dropdown
12 | attributes:
13 | label: Component
14 | description: What component is the feature for?
15 | multiple: true
16 | options:
17 | - API
18 | - Utils
19 | - Other (please describe)
20 | validations:
21 | required: true
22 | - type: textarea
23 | attributes:
24 | label: Describe the feature you would like
25 | description: Please also describe what the feature is aiming to solve, if relevant.
26 | validations:
27 | required: true
28 | - type: textarea
29 | attributes:
30 | label: Additional context
31 | description: Add any other context to the feature (like screenshots, resources)
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Support
4 | url: https://github.com/ProjectOpenSea/opensea-js/discussions
5 | about: This issue tracker is only for bugs and feature requests. Support is available in Discussions!
6 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
9 |
10 | ## Motivation
11 |
12 |
19 |
20 | ## Solution
21 |
22 |
26 |
--------------------------------------------------------------------------------
/.github/workflows/cancel.yml:
--------------------------------------------------------------------------------
1 | name: Cancel Workflows
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - main
7 |
8 | jobs:
9 | cancel:
10 | runs-on: ubuntu-latest
11 | timeout-minutes: 2
12 |
13 | steps:
14 | - uses: styfle/cancel-workflow-action@0.12.1
15 | with:
16 | all_but_latest: true
17 | workflow_id: code-quality.yml, codeql-analysis.yml, docs.yml
18 | access_token: ${{ github.token }}
19 |
--------------------------------------------------------------------------------
/.github/workflows/code-quality.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: code-quality
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | env:
9 | CI: true
10 |
11 | jobs:
12 | lint:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: actions/setup-node@v4
17 | with:
18 | node-version-file: .nvmrc
19 | cache: npm
20 |
21 | - name: Install dependencies
22 | run: npm install
23 |
24 | - name: Run linters
25 | run: npm run lint
26 |
27 | test:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - uses: actions/checkout@v4
31 | - uses: actions/setup-node@v4
32 | with:
33 | node-version-file: .nvmrc
34 | cache: npm
35 |
36 | - name: Install dependencies
37 | run: npm install
38 |
39 | - name: Run tests
40 | env:
41 | OPENSEA_API_KEY: ${{ secrets.OPENSEA_API_KEY }}
42 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
43 | run: npm run test
44 |
45 | - name: Create code coverage report
46 | run: npm run coverage-report
47 |
48 | - name: Upload code coverage
49 | uses: coverallsapp/github-action@master
50 | with:
51 | github-token: ${{ secrets.GITHUB_TOKEN }}
52 |
53 | test-integration:
54 | runs-on: ubuntu-latest
55 | steps:
56 | - uses: actions/checkout@v4
57 | - uses: actions/setup-node@v4
58 | with:
59 | node-version-file: .nvmrc
60 | cache: npm
61 |
62 | - name: Install dependencies
63 | run: npm install
64 |
65 | - name: Run integration tests
66 | env:
67 | OPENSEA_API_KEY: ${{ secrets.OPENSEA_API_KEY }}
68 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
69 | ALCHEMY_API_KEY_POLYGON: ${{ secrets.ALCHEMY_API_KEY_POLYGON }}
70 | WALLET_PRIV_KEY: ${{ secrets.WALLET_PRIV_KEY }}
71 | SELL_ORDER_CONTRACT_ADDRESS: ${{ secrets.SELL_ORDER_CONTRACT_ADDRESS }}
72 | SELL_ORDER_TOKEN_ID: ${{ secrets.SELL_ORDER_TOKEN_ID }}
73 | SELL_ORDER_CONTRACT_ADDRESS_POLYGON: ${{ secrets.SELL_ORDER_CONTRACT_ADDRESS_POLYGON }}
74 | SELL_ORDER_TOKEN_ID_POLYGON: ${{ secrets.SELL_ORDER_TOKEN_ID_POLYGON }}
75 | run: npm run test:integration
76 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | analyze:
11 | name: Analyze
12 | runs-on: ubuntu-latest
13 | permissions:
14 | actions: read
15 | contents: read
16 | security-events: write
17 |
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | language: ["javascript"]
22 |
23 | steps:
24 | - name: Checkout repository
25 | uses: actions/checkout@v4
26 |
27 | - name: Initialize CodeQL
28 | uses: github/codeql-action/init@v3
29 | with:
30 | languages: ${{ matrix.language }}
31 |
32 | - name: Autobuild
33 | uses: github/codeql-action/autobuild@v3
34 |
35 | - name: Perform CodeQL Analysis
36 | uses: github/codeql-action/analyze@v3
37 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Documentation
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | build-and-deploy:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version-file: .nvmrc
17 | cache: npm
18 | - uses: tibdex/github-app-token@v2
19 | id: get_token
20 | with:
21 | app_id: ${{ secrets.PUBLIC_DOC_PUBLISHER_APP_ID }}
22 | private_key: ${{ secrets.PUBLIC_DOC_PUBLISHER_PRIVATE_KEY }}
23 |
24 | - name: Install dependencies
25 | run: npm install
26 |
27 | - name: Build docs
28 | run: npm run docs-build
29 |
30 | - name: Deploy 🚀
31 | if: github.ref == 'refs/heads/main'
32 | uses: JamesIves/github-pages-deploy-action@v4.7.3
33 | with:
34 | branch: gh-pages
35 | folder: docs
36 |
37 | - name: Copy developer docs to repository
38 | if: github.ref == 'refs/heads/main'
39 | uses: nkoppel/push-files-to-another-repository@v1.1.4
40 | continue-on-error: true
41 | env:
42 | API_TOKEN_GITHUB: ${{ steps.get_token.outputs.token }}
43 | with:
44 | source-files: "developerDocs/"
45 | destination-username: "ProjectOpenSea"
46 | destination-repository: "developer-docs"
47 | destination-directory: "opensea-js"
48 | destination-branch: "main"
49 | commit-username: "ProjectOpenSea-opensea-js"
50 | commit-message: "Latest docs from opensea-js"
51 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [published]
9 |
10 | jobs:
11 | publish-npm:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: actions/setup-node@v4
16 | with:
17 | node-version-file: .nvmrc
18 | registry-url: https://registry.npmjs.org/
19 | - run: npm install
20 | - run: npm test
21 | env:
22 | OPENSEA_API_KEY: ${{ secrets.OPENSEA_API_KEY }}
23 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
24 | - run: npm publish
25 | env:
26 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
27 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: "Close stale issues"
2 | on:
3 | schedule:
4 | - cron: "0 0 * * *"
5 | workflow_dispatch:
6 |
7 | jobs:
8 | stale:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/stale@v9
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
14 | stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs. Thank you for your contributions. If you believe this was a mistake, please comment."
15 | stale-pr-message: "This PR has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs. Thank you for your contributions. If you believe this was a mistake, please comment."
16 | days-before-stale: 60
17 | days-before-close: 14
18 | operations-per-run: 1000
19 | exempt-pr-labels: "work-in-progress,informational"
20 | exempt-issue-labels: "work-in-progress,informational"
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | _bundles/
2 | node_modules/
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # next.js build output
64 | .next
65 |
66 | lib
67 |
68 | docs
69 |
70 | .DS_Store
71 | .idea/
72 |
73 | # Auto generated typechain contracts
74 | src/typechain/contracts/
75 |
76 | # Yarn
77 | yarn.lock
78 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run check-types
5 |
--------------------------------------------------------------------------------
/.mocharc-integration.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": "ts-node/register/transpile-only, dotenv/config",
3 | "spec": "test/integration/**/*.spec.ts",
4 | "timeout": "25s"
5 | }
6 |
--------------------------------------------------------------------------------
/.mocharc-unit.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": "ts-node/register/transpile-only",
3 | "spec": "test/**/*.spec.ts",
4 | "exclude": "test/integration/**/*.ts",
5 | "timeout": "15s"
6 | }
7 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.16.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | lib
2 | docs
3 | .nyc_output
4 | coverage
5 | src/typechain
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Ozone Networks, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [![Version][version-badge]][version-link]
6 | [![npm][npm-badge]][npm-link]
7 | [![Test CI][ci-badge]][ci-link]
8 | [![Coverage Status][coverage-badge]][coverage-link]
9 | [![License][license-badge]][license-link]
10 | [![Docs][docs-badge]][docs-link]
11 | [![Discussions][discussions-badge]][discussions-link]
12 |
13 | # OpenSea.js
14 |
15 | This is the TypeScript SDK for [OpenSea](https://opensea.io), the largest marketplace for NFTs.
16 |
17 | It allows developers to access the official orderbook, filter it, create listings and offers, and complete trades programmatically.
18 |
19 | Get started by [requesting an API key](https://docs.opensea.io/reference/api-keys) and instantiating your own OpenSea SDK instance. Then you can create orders off-chain or fulfill orders on-chain, and listen to events in the process.
20 |
21 | Happy seafaring! ⛵️
22 |
23 | ## Documentation
24 |
25 | - [Quick Start Guide](developerDocs/quick-start.md)
26 | - [Getting Started Guide](developerDocs/getting-started.md)
27 | - [Advanced Use Cases](developerDocs/advanced-use-cases.md)
28 | - [SDK Reference](https://projectopensea.github.io/opensea-js/)
29 | - [Frequently Asked Questions](developerDocs/faq.md)
30 | - [Contributing](developerDocs/contributing.md)
31 |
32 | ## Changelog
33 |
34 | The changelog for recent versions can be found at:
35 |
36 | - https://docs.opensea.io/changelog
37 | - https://github.com/ProjectOpenSea/opensea-js/releases
38 |
39 | [version-badge]: https://img.shields.io/github/package-json/v/ProjectOpenSea/opensea-js
40 | [version-link]: https://github.com/ProjectOpenSea/opensea-js/releases
41 | [npm-badge]: https://img.shields.io/npm/v/opensea-js?color=red
42 | [npm-link]: https://www.npmjs.com/package/opensea-js
43 | [ci-badge]: https://github.com/ProjectOpenSea/opensea-js/actions/workflows/code-quality.yml/badge.svg
44 | [ci-link]: https://github.com/ProjectOpenSea/opensea-js/actions/workflows/code-quality.yml
45 | [coverage-badge]: https://coveralls.io/repos/github/ProjectOpenSea/opensea-js/badge.svg?branch=main
46 | [coverage-link]: https://coveralls.io/github/ProjectOpenSea/opensea-js?branch=main
47 | [license-badge]: https://img.shields.io/github/license/ProjectOpenSea/opensea-js
48 | [license-link]: https://github.com/ProjectOpenSea/opensea-js/blob/main/LICENSE
49 | [docs-badge]: https://img.shields.io/badge/OpenSea.js-documentation-informational
50 | [docs-link]: https://github.com/ProjectOpenSea/opensea-js#documentation
51 | [discussions-badge]: https://img.shields.io/badge/OpenSea.js-discussions-blueviolet
52 | [discussions-link]: https://github.com/ProjectOpenSea/opensea-js/discussions
53 |
--------------------------------------------------------------------------------
/developerDocs/advanced-use-cases.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Advanced Use Cases
3 | category: 64cbb5277b5f3c0065d96616
4 | slug: opensea-sdk-advanced-use
5 | parentDocSlug: opensea-sdk
6 | order: 2
7 | hidden: false
8 | ---
9 |
10 | - [Scheduling Future Listings](#scheduling-future-listings)
11 | - [Purchasing Items for Other Users](#purchasing-items-for-other-users)
12 | - [Using ERC-20 Tokens Instead of Ether](#using-erc-20-tokens-instead-of-ether)
13 | - [Private Auctions](#private-auctions)
14 | - [Listening to Events](#listening-to-events)
15 |
16 | ## Advanced
17 |
18 | Interested in purchasing for users server-side or with a bot, scheduling future orders, or making bids in different ERC-20 tokens? OpenSea.js can help with that.
19 |
20 | ### Scheduling Future Listings
21 |
22 | You can create listings that aren't fulfillable until a future date. Just pass in a `listingTime` (a UTC timestamp in seconds) to your SDK instance:
23 |
24 | ```typescript
25 | const listingTime = Math.round(Date.now() / 1000 + 60 * 60 * 24); // One day from now
26 | const order = await openseaSDK.createListing({
27 | asset: { tokenAddress, tokenId },
28 | accountAddress,
29 | startAmount: 1,
30 | listingTime,
31 | });
32 | ```
33 |
34 | ### Purchasing Items for Other Users
35 |
36 | You can buy and transfer an item to someone else in one step! Just pass the `recipientAddress` parameter:
37 |
38 | ```typescript
39 | const order = await openseaSDK.api.getOrder({ side: OrderSide.LISTING, ... })
40 | await openseaSDK.fulfillOrder({
41 | order,
42 | accountAddress, // The address of your wallet, which will sign the transaction
43 | recipientAddress // The address of the recipient, i.e. the wallet you're purchasing on behalf of
44 | })
45 | ```
46 |
47 | If the order is a listing (sell order, ask, `OrderSide.LISTING`), the taker is the _buyer_ and this will prompt the buyer to pay for the item(s) but send them to the `recipientAddress`. If the order is an offer (buy order, bid, `OrderSide.OFFER`), the taker is the _seller_ but the bid amount will be sent to the `recipientAddress`.
48 |
49 | This will automatically approve the assets for trading and confirm the transaction for sending them.
50 |
51 | ### Using ERC-20 Tokens Instead of Ether
52 |
53 | Here's an example of listing the Genesis CryptoKitty for $100! No more needing to worry about the exchange rate:
54 |
55 | ```typescript
56 | // CryptoKitties
57 | const tokenAddress = "0x06012c8cf97bead5deae237070f9587f8e7a266d";
58 | // Token address for the DAI stablecoin, which is pegged to $1 USD
59 | const paymentTokenAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
60 |
61 | // The units for `startAmount` and `endAmount` are now in DAI, so $100 USD
62 | const order = await openseaSDK.createListing({
63 | tokenAddress,
64 | tokenId: "1",
65 | accountAddress: OWNERS_WALLET_ADDRESS,
66 | startAmount: 100,
67 | paymentTokenAddress,
68 | });
69 | ```
70 |
71 | You can use `getPaymentToken` to search for payment tokens by address. And you can even list all orders for a specific ERC-20 token by querying the API:
72 |
73 | ```typescript
74 | const token = await openseaSDK.api.getPaymentToken(paymentTokenAddress);
75 |
76 | const order = await openseaSDK.api.getOrders({
77 | side: OrderSide.LISTING,
78 | paymentTokenAddress: token.address,
79 | });
80 | ```
81 |
82 | ### Private Auctions
83 |
84 | You can make offers and listings that can only be fulfilled by an address or email of your choosing. This allows you to negotiate a price in some channel and sell for your chosen price on OpenSea, **without having to trust that the counterparty will abide by your terms!**
85 |
86 | Here's an example of listing a Decentraland parcel for 10 ETH with a specific buyer address allowed to take it. No more needing to worry about whether they'll give you enough back!
87 |
88 | ```typescript
89 | // Address allowed to buy from you
90 | const buyerAddress = "0x123...";
91 | // Decentraland
92 | const tokenAddress = "0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d";
93 | const tokenId =
94 | "115792089237316195423570985008687907832853042650384256231655107562007036952461";
95 |
96 | const listing = await openseaSDK.createListing({
97 | tokenAddress,
98 | tokenId,
99 | accountAddress: OWNERS_WALLET_ADDRESS,
100 | startAmount: 10,
101 | buyerAddress,
102 | });
103 | ```
104 |
105 | ### Listening to Events
106 |
107 | Events are fired whenever transactions or orders are being created, and when transactions return receipts from recently mined blocks on the Ethereum blockchain.
108 |
109 | Our recommendation is that you "forward" OpenSea events to your own store or state management system. Here are examples of listening to the events:
110 |
111 | ```typescript
112 | import { OpenSeaSDK, EventType } from 'opensea-js'
113 | const sdk = new OpenSeaSDK(...);
114 |
115 | handleSDKEvents() {
116 | sdk.addListener(EventType.TransactionCreated, ({ transactionHash, event }) => {
117 | console.info('Transaction created: ', { transactionHash, event })
118 | })
119 | sdk.addListener(EventType.TransactionConfirmed, ({ transactionHash, event }) => {
120 | console.info('Transaction confirmed: ',{ transactionHash, event })
121 | })
122 | sdk.addListener(EventType.TransactionDenied, ({ transactionHash, event }) => {
123 | console.info('Transaction denied: ',{ transactionHash, event })
124 | })
125 | sdk.addListener(EventType.TransactionFailed, ({ transactionHash, event }) => {
126 | console.info('Transaction failed: ',{ transactionHash, event })
127 | })
128 | sdk.addListener(EventType.WrapEth, ({ accountAddress, amount }) => {
129 | console.info('Wrap ETH: ',{ accountAddress, amount })
130 | })
131 | sdk.addListener(EventType.UnwrapWeth, ({ accountAddress, amount }) => {
132 | console.info('Unwrap ETH: ',{ accountAddress, amount })
133 | })
134 | sdk.addListener(EventType.MatchOrders, ({ buy, sell, accountAddress }) => {
135 | console.info('Match orders: ', { buy, sell, accountAddress })
136 | })
137 | sdk.addListener(EventType.CancelOrder, ({ order, accountAddress }) => {
138 | console.info('Cancel order: ', { order, accountAddress })
139 | })
140 | }
141 | ```
142 |
143 | To remove all listeners call `sdk.removeAllListeners()`.
144 |
--------------------------------------------------------------------------------
/developerDocs/contributing.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Contributing
3 | category: 64cbb5277b5f3c0065d96616
4 | slug: opensea-sdk-contributions
5 | parentDocSlug: opensea-sdk
6 | order: 5
7 | hidden: false
8 | ---
9 |
10 | ## Development Information
11 |
12 | **Setup**
13 |
14 | Before any development, install the required NPM dependencies:
15 |
16 | ```bash
17 | npm install
18 | ```
19 |
20 | And install TypeScript if you haven't already:
21 |
22 | ```bash
23 | npm install -g typescript
24 | ```
25 |
26 | **Build**
27 |
28 | Then, lint and build the library into the `lib` directory:
29 |
30 | ```bash
31 | npm run build
32 | ```
33 |
34 | Or run the tests:
35 |
36 | ```bash
37 | npm test
38 | ```
39 |
40 | Note that the tests require access to Alchemy and the OpenSea API. The timeout is adjustable via the `test` script in `package.json`.
41 |
42 | **Testing your branch locally**
43 |
44 | ```sh
45 | npm link # in opensea-js repo
46 | npm link opensea-js # in repo you're working on
47 | ```
48 |
49 | **Generate Documentation**
50 |
51 | Generate html docs, also available for browsing [here](https://projectopensea.github.io/opensea-js/):
52 |
53 | ```bash
54 | npm run docs-build
55 | ```
56 |
57 | **Contributing**
58 |
59 | Contributions welcome! Please use GitHub issues for suggestions/concerns - if you prefer to express your intentions in code, feel free to submit a pull request.
60 |
--------------------------------------------------------------------------------
/developerDocs/faq.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Frequently Asked Questions
3 | category: 64cbb5277b5f3c0065d96616
4 | slug: opensea-sdk-faq
5 | parentDocSlug: opensea-sdk
6 | order: 4
7 | hidden: false
8 | ---
9 |
10 | - [How do I access the source code?](#how-do-i-access-the-source-code)
11 | - [What chains are supported?](#what-chains-are-supported)
12 | - [There is no SDK method for the API request I am trying to call.](#there-is-no-sdk-method-for-the-api-request-i-am-trying-to-call)
13 |
14 | ## How do I access the source code?
15 |
16 | The source code for the SDK can be found on [GitHub](https://github.com/ProjectOpenSea/opensea-js).
17 |
18 | ## What chains are supported?
19 |
20 | See the [Chain enum](https://github.com/ProjectOpenSea/opensea-js/blob/main/src/types.ts#L101) for a complete list of supported chains.
21 |
22 | Please note a number of older SDK methods (API v1) only support Ethereum Mainnet and Sepolia due to Rest API restrictions. Please use methods in the v2 API for multichain capabilities.
23 |
24 | ## Why is there no SDK method for the API request I am trying to call?
25 |
26 | If the SDK does not currently have a specific API, you can use the generic [GET and POST methods](https://github.com/ProjectOpenSea/opensea-js/blob/main/src/api/api.ts#L612-L636) to make any API Request. This repository is also open source, so please feel free to create a pull request.
27 |
--------------------------------------------------------------------------------
/developerDocs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started Guide
3 | category: 64cbb5277b5f3c0065d96616
4 | slug: opensea-sdk-getting-started
5 | parentDocSlug: opensea-sdk
6 | order: 1
7 | hidden: false
8 | ---
9 |
10 | - [Fetching Assets](#fetching-assets)
11 | - [Checking Balances and Ownerships](#checking-balances-and-ownerships)
12 | - [Making Offers](#making-offers)
13 | - [Offer Limits](#offer-limits)
14 | - [Making Listings / Selling Items](#making-listings--selling-items)
15 | - [Creating English Auctions](#creating-english-auctions)
16 | - [Fetching Orders](#fetching-orders)
17 | - [Buying Items](#buying-items)
18 | - [Accepting Offers](#accepting-offers)
19 |
20 | ### Fetching NFTs
21 |
22 | ```TypeScript
23 | const { nft } = await openseaSDK.api.getNFT(tokenAddress, tokenId)
24 | ```
25 |
26 | Also see methods `getNFTsByCollection`, `getNFTsByContract`, and `getNFTsByAccount`.
27 |
28 | #### Checking Balances and Ownerships
29 |
30 | ```typescript
31 | import { TokenStandard } from "opensea-js";
32 |
33 | const asset = {
34 | // CryptoKitties
35 | tokenAddress: "0x06012c8cf97bead5deae237070f9587f8e7a266d",
36 | tokenId: "1",
37 | tokenStandard: TokenStandard.ERC721,
38 | };
39 |
40 | const balance = await openseaSDK.getBalance({
41 | accountAddress,
42 | asset,
43 | });
44 |
45 | const ownsKitty = balance > 0n;
46 | ```
47 |
48 | ### Making Offers
49 |
50 | ```typescript
51 | // Token ID and smart contract address for a non-fungible token:
52 | const { tokenId, tokenAddress } = YOUR_ASSET;
53 | // The offerer's wallet address:
54 | const accountAddress = "0x1234...";
55 | // Value of the offer, in units of the payment token (or wrapped ETH if none is specified)
56 | const startAmount = 1.2;
57 |
58 | const offer = await openseaSDK.createOffer({
59 | asset: {
60 | tokenId,
61 | tokenAddress,
62 | },
63 | accountAddress,
64 | startAmount,
65 | });
66 | ```
67 |
68 | When you make an offer on an item owned by an OpenSea user, **that user will automatically get an email notifying them with the offer amount**, if it's above their desired threshold.
69 |
70 | #### Offer Limits
71 |
72 | Note: The total value of offers must not exceed 1000x wallet balance.
73 |
74 | ### Making Listings / Selling Items
75 |
76 | To sell an asset, call `createListing`:
77 |
78 | ```typescript
79 | // Expire this auction one day from now.
80 | // Note that we convert from the JavaScript timestamp (milliseconds) to seconds:
81 | const expirationTime = Math.round(Date.now() / 1000 + 60 * 60 * 24);
82 |
83 | const listing = await openseaSDK.createListing({
84 | asset: {
85 | tokenId,
86 | tokenAddress,
87 | },
88 | accountAddress,
89 | startAmount: 3,
90 | expirationTime,
91 | });
92 | ```
93 |
94 | The units for `startAmount` are Ether (ETH). If you want to specify another ERC-20 token to use, see [Using ERC-20 Tokens Instead of Ether](#using-erc-20-tokens-instead-of-ether).
95 |
96 | See [Listening to Events](#listening-to-events) to respond to the setup transactions that occur the first time a user sells an item.
97 |
98 | ### Creating Collection and Trait Offers
99 |
100 | Criteria offers, consisting of collection and trait offers, are supported with `openseaSDK.createCollectionOffer()`.
101 |
102 | For trait offers, include `traitType` as the trait name and `traitValue` as the required value for the offer.
103 |
104 | ```typescript
105 | const collection = await sdk.api.getCollection("cool-cats-nft");
106 | const offer = await openseaSDK.createCollectionOffer({
107 | collectionSlug: collection.collection,
108 | accountAddress: walletAddress,
109 | paymentTokenAddress: getWETHAddress(sdk.chain),
110 | amount: 7,
111 | quantity: 1,
112 | traitType: "face",
113 | traitValue: "tvface bobross",
114 | });
115 | ```
116 |
117 | #### Creating English Auctions
118 |
119 | 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.
120 |
121 | To create an English Auction set `englishAuction` to `true`:
122 |
123 | ```typescript
124 | // Create an auction to receive Wrapped Ether (WETH). See note below.
125 | const paymentTokenAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
126 | const englishAuction = true;
127 | // The minimum amount to start the auction at, in normal units (e.g. ETH)
128 | const startAmount = 0;
129 |
130 | const auction = await openseaSDK.createListing({
131 | asset: {
132 | tokenId,
133 | tokenAddress,
134 | },
135 | accountAddress,
136 | startAmount,
137 | expirationTime,
138 | paymentTokenAddress,
139 | englishAuction,
140 | });
141 | ```
142 |
143 | Note that auctions aren't supported with Ether directly due to limitations in Ethereum, so you have to use an ERC20 token, like Wrapped Ether (WETH), a stablecoin like DAI, etc. See [Using ERC-20 Tokens Instead of Ether](#using-erc-20-tokens-instead-of-ether) for more info.
144 |
145 | ### Fetching Orders
146 |
147 | To retrieve a list of offers and auctions on an asset, you can use `getOrders`. Parameters passed into API filter objects are camel-cased and serialized before being sent as [API parameters](https://docs.opensea.io/v2.0/reference):
148 |
149 | ```typescript
150 | // Get offers
151 | const { orders, count } = await openseaSDK.api.getOrders({
152 | assetContractAddress: tokenAddress,
153 | tokenId,
154 | side: OrderSide.OFFER,
155 | });
156 |
157 | // Get listings
158 | const { orders, count } = await openseaSDK.api.getOrders({
159 | assetContractAddress: tokenAddress,
160 | tokenId,
161 | side: OrderSide.LISTING,
162 | });
163 | ```
164 |
165 | Note that the listing price of an asset is equal to the `currentPrice` of the **lowest listing** on the asset. Users can lower their listing price without invalidating previous listing, so all get shipped down until they're canceled, or one is fulfilled.
166 |
167 | #### Fetching All Offers and Best Listings for a given collection
168 |
169 | There are two endpoints that return all offers and listings for a given collection, `getAllOffers` and `getAllListings`.
170 |
171 | ```typescript
172 | const { offers } = await openseaSDK.api.getAllOffers(collectionSlug);
173 | ```
174 |
175 | #### Fetching Best Offers and Best Listings for a given NFT
176 |
177 | There are two endpoints that return the best offer or listing, `getBestOffer` and `getBestListing`.
178 |
179 | ```typescript
180 | const offer = await openseaSDK.api.getBestOffer(collectionSlug, tokenId);
181 | ```
182 |
183 | ### Buying Items
184 |
185 | To buy an item, you need to **fulfill a listing**. To do that, it's just one call:
186 |
187 | ```typescript
188 | const order = await openseaSDK.api.getOrder({ side: OrderSide.LISTING, ... })
189 | const accountAddress = "0x..." // The buyer's wallet address, also the taker
190 | const transactionHash = await openseaSDK.fulfillOrder({ order, accountAddress })
191 | ```
192 |
193 | Note that the `fulfillOrder` promise resolves when the transaction has been confirmed and mined to the blockchain. To get the transaction hash before this happens, add an event listener (see [Listening to Events](#listening-to-events)) for the `TransactionCreated` event.
194 |
195 | If the order is a listing, the taker is the _buyer_ and this will prompt the buyer to pay for the item(s).
196 |
197 | ### Accepting Offers
198 |
199 | Similar to fulfilling listings above, you need to fulfill an offer (buy order) on an item you own to receive the tokens in the offer.
200 |
201 | ```typescript
202 | const order = await openseaSDK.api.getOrder({ side: OrderSide.OFFER, ... })
203 | const accountAddress = "0x..." // The owner's wallet address, also the taker
204 | await openseaSDK.fulfillOrder({ order, accountAddress })
205 | ```
206 |
207 | If the order is an offer, then the taker is the _owner_ and this will prompt the owner to exchange their item(s) for whatever is being offered in return.
208 |
209 | See [Listening to Events](#listening-to-events) below to respond to the setup transactions that occur the first time a user accepts a bid.
210 |
--------------------------------------------------------------------------------
/developerDocs/overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: OpenSea SDK
3 | category: 64cbb5277b5f3c0065d96616
4 | slug: opensea-sdk
5 | hidden: false
6 | ---
7 |
8 | # Overview
9 |
10 | This is the JavaScript SDK for [OpenSea](https://opensea.io), the largest marketplace for NFTs.
11 |
12 | It allows developers to access the official orderbook, filter it, create offers and listings, and complete trades programmatically.
13 |
14 | Get started by [requesting an API key](https://docs.opensea.io/reference/api-keys) and instantiating your own OpenSea SDK instance. Then you can create orders off-chain or fulfill orders on-chain, and listen to events in the process.
15 |
16 | Happy seafaring! ⛵️
17 |
--------------------------------------------------------------------------------
/developerDocs/quick-start.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Quick Start Guide
3 | category: 64cbb5277b5f3c0065d96616
4 | slug: opensea-sdk-quick-start
5 | parentDocSlug: opensea-sdk
6 | order: 0
7 | hidden: false
8 | ---
9 |
10 | # Installation
11 |
12 | Node.js version 16 is the minimum required for the SDK. If you have Node Version Manager (nvm), run `nvm use 16`.
13 |
14 | Then in your project, run:
15 |
16 | ```bash
17 | npm install --save opensea-js
18 | # or
19 | yarn add opensea-js
20 | ```
21 |
22 | # Initialization
23 |
24 | To get started, first [request an API key](https://docs.opensea.io/reference/api-keys). Note the terms of use for using API data. API keys are not needed for testnets.
25 |
26 | Then, create a new OpenSeaSDK client using your web3 provider:
27 |
28 | ```typescript
29 | import { ethers } from "ethers";
30 | import { OpenSeaSDK, Chain } from "opensea-js";
31 |
32 | // This example provider won't let you make transactions, only read-only calls:
33 | const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io");
34 |
35 | const openseaSDK = new OpenSeaSDK(provider, {
36 | chain: Chain.Mainnet,
37 | apiKey: YOUR_API_KEY,
38 | });
39 | ```
40 |
41 | ## Wallet
42 |
43 | Using the example provider above won't let you authorize transactions, which are needed when approving and trading assets and currency. To make transactions, you need a provider with a private key or mnemonic set:
44 |
45 | ```typescript
46 | const walletWithProvider = new ethers.Wallet(PRIVATE_KEY, provider);
47 |
48 | const openseaSDK = new OpenSeaSDK(walletWithProvider, {
49 | chain: Chain.Mainnet,
50 | apiKey: YOUR_API_KEY,
51 | });
52 | ```
53 |
54 | In a browser with web3 or an extension like [MetaMask](https://metamask.io/) or [Coinbase Wallet](https://www.coinbase.com/wallet), you can use `window.ethereum` to access the native provider.
55 |
56 | ## Testnets
57 |
58 | For testnets, please use `Chain.Sepolia`. Rinkeby was deprecated in 2022 and Goerli in 2023.
59 |
--------------------------------------------------------------------------------
/developerDocs/sdk-references.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: SDK Reference
3 | type: link
4 | category: 64cbb5277b5f3c0065d96616
5 | parentDocSlug: opensea-sdk
6 | order: 3
7 | hidden: false
8 | link_url: https://projectopensea.github.io/opensea-js/
9 | ---
10 |
--------------------------------------------------------------------------------
/img/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProjectOpenSea/opensea-js/08b3e5bee4bb2f5f1099bd7333932fbfccb43d58/img/banner.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "opensea-js",
3 | "version": "7.1.19",
4 | "description": "TypeScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data",
5 | "license": "MIT",
6 | "author": "OpenSea Developers",
7 | "homepage": "https://docs.opensea.io/reference/api-overview",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/ProjectOpenSea/opensea-js.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/ProjectOpenSea/opensea-js/issues"
14 | },
15 | "main": "lib/index.js",
16 | "files": [
17 | "lib",
18 | "src"
19 | ],
20 | "scripts": {
21 | "abi-type-gen": "typechain --target=ethers-v6 src/abi/*.json --out-dir=src/typechain/contracts",
22 | "build": "npm run abi-type-gen && tsc -p tsconfig.build.json",
23 | "check-types": "tsc --noEmit --project tsconfig.json",
24 | "coverage-report": "nyc report --reporter=lcov",
25 | "docs-build": "typedoc",
26 | "docs-build-md": "typedoc --plugin typedoc-plugin-markdown",
27 | "eslint:check": "eslint . --max-warnings 0 --ext .js,.ts",
28 | "eslint:fix": "npm run eslint:check -- --fix",
29 | "postinstall": "husky install || exit 0",
30 | "lint": "concurrently \"npm run check-types\" \"npm run prettier:check\" \"npm run eslint:check\"",
31 | "lint:fix": "npm run prettier:fix && npm run eslint:fix",
32 | "prepare": "npm run build",
33 | "prettier:check": "prettier --check .",
34 | "prettier:check:package.json": "prettier-package-json --list-different",
35 | "prettier:fix": "prettier --write .",
36 | "test": "nyc mocha --config ./.mocharc-unit.json",
37 | "test:integration": "nyc mocha --config ./.mocharc-integration.json"
38 | },
39 | "types": "lib/index.d.ts",
40 | "dependencies": {
41 | "@opensea/seaport-js": "^4.0.0",
42 | "ethers": "^6.9.0"
43 | },
44 | "devDependencies": {
45 | "@typechain/ethers-v6": "^0.5.1",
46 | "@types/chai": "4.3.20",
47 | "@types/chai-as-promised": "^7.1.5",
48 | "@types/mocha": "^10.0.0",
49 | "@types/node": "^22.0.0",
50 | "@typescript-eslint/eslint-plugin": "^7.0.0",
51 | "@typescript-eslint/parser": "^7.0.0",
52 | "chai": "^4.4.1",
53 | "chai-as-promised": "^7.1.1",
54 | "concurrently": "^8.2.0",
55 | "confusing-browser-globals": "^1.0.11",
56 | "dotenv": "^16.0.3",
57 | "eslint": "^8.4.1",
58 | "eslint-config-prettier": "^9.0.0",
59 | "eslint-import-resolver-typescript": "^3.0.0",
60 | "eslint-plugin-import": "^2.25.3",
61 | "eslint-plugin-prettier": "^5.0.1",
62 | "husky": "^8.0.3",
63 | "lint-staged": "^15.0.0",
64 | "mocha": "^10.0.0",
65 | "nyc": "^17.0.0",
66 | "prettier": "^3.0.0",
67 | "prettier-package-json": "^2.8.0",
68 | "ts-node": "^10.9.2",
69 | "typechain": "^8.0.0",
70 | "typedoc": "^0.25.0",
71 | "typedoc-plugin-markdown": "^4.0.0",
72 | "typescript": "^5.1.3"
73 | },
74 | "keywords": [
75 | "collectibles",
76 | "crypto",
77 | "ethereum",
78 | "javascript",
79 | "marketplace",
80 | "nft",
81 | "node",
82 | "non-fungible tokens",
83 | "opensea",
84 | "sdk",
85 | "smart contracts",
86 | "typescript"
87 | ],
88 | "engines": {
89 | "node": ">=20.0.0"
90 | },
91 | "lint-staged": {
92 | "package.json": [
93 | "prettier-package-json --write"
94 | ],
95 | "**/*.{ts,tsx,js,jsx,html,md,mdx,yml,json}": [
96 | "prettier --write"
97 | ],
98 | "**/*.{ts,tsx,js,jsx}": [
99 | "eslint --cache --fix"
100 | ]
101 | },
102 | "nyc": {
103 | "exclude": [
104 | "src/utils/tokens/**/*.ts",
105 | "src/typechain"
106 | ]
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "packageRules": [
4 | {
5 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
6 | "automerge": true
7 | }
8 | ],
9 | "schedule": ["before 9am on monday"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/abi/ERC1155.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "anonymous": false,
4 | "inputs": [
5 | {
6 | "indexed": true,
7 | "internalType": "address",
8 | "name": "account",
9 | "type": "address"
10 | },
11 | {
12 | "indexed": true,
13 | "internalType": "address",
14 | "name": "operator",
15 | "type": "address"
16 | },
17 | {
18 | "indexed": false,
19 | "internalType": "bool",
20 | "name": "approved",
21 | "type": "bool"
22 | }
23 | ],
24 | "name": "ApprovalForAll",
25 | "type": "event"
26 | },
27 | {
28 | "anonymous": false,
29 | "inputs": [
30 | {
31 | "indexed": true,
32 | "internalType": "address",
33 | "name": "operator",
34 | "type": "address"
35 | },
36 | {
37 | "indexed": true,
38 | "internalType": "address",
39 | "name": "from",
40 | "type": "address"
41 | },
42 | {
43 | "indexed": true,
44 | "internalType": "address",
45 | "name": "to",
46 | "type": "address"
47 | },
48 | {
49 | "indexed": false,
50 | "internalType": "uint256[]",
51 | "name": "ids",
52 | "type": "uint256[]"
53 | },
54 | {
55 | "indexed": false,
56 | "internalType": "uint256[]",
57 | "name": "values",
58 | "type": "uint256[]"
59 | }
60 | ],
61 | "name": "TransferBatch",
62 | "type": "event"
63 | },
64 | {
65 | "anonymous": false,
66 | "inputs": [
67 | {
68 | "indexed": true,
69 | "internalType": "address",
70 | "name": "operator",
71 | "type": "address"
72 | },
73 | {
74 | "indexed": true,
75 | "internalType": "address",
76 | "name": "from",
77 | "type": "address"
78 | },
79 | {
80 | "indexed": true,
81 | "internalType": "address",
82 | "name": "to",
83 | "type": "address"
84 | },
85 | {
86 | "indexed": false,
87 | "internalType": "uint256",
88 | "name": "id",
89 | "type": "uint256"
90 | },
91 | {
92 | "indexed": false,
93 | "internalType": "uint256",
94 | "name": "value",
95 | "type": "uint256"
96 | }
97 | ],
98 | "name": "TransferSingle",
99 | "type": "event"
100 | },
101 | {
102 | "anonymous": false,
103 | "inputs": [
104 | {
105 | "indexed": false,
106 | "internalType": "string",
107 | "name": "value",
108 | "type": "string"
109 | },
110 | {
111 | "indexed": true,
112 | "internalType": "uint256",
113 | "name": "id",
114 | "type": "uint256"
115 | }
116 | ],
117 | "name": "URI",
118 | "type": "event"
119 | },
120 | {
121 | "inputs": [
122 | {
123 | "internalType": "address",
124 | "name": "account",
125 | "type": "address"
126 | },
127 | {
128 | "internalType": "uint256",
129 | "name": "id",
130 | "type": "uint256"
131 | }
132 | ],
133 | "name": "balanceOf",
134 | "outputs": [
135 | {
136 | "internalType": "uint256",
137 | "name": "",
138 | "type": "uint256"
139 | }
140 | ],
141 | "stateMutability": "view",
142 | "type": "function"
143 | },
144 | {
145 | "inputs": [
146 | {
147 | "internalType": "address[]",
148 | "name": "accounts",
149 | "type": "address[]"
150 | },
151 | {
152 | "internalType": "uint256[]",
153 | "name": "ids",
154 | "type": "uint256[]"
155 | }
156 | ],
157 | "name": "balanceOfBatch",
158 | "outputs": [
159 | {
160 | "internalType": "uint256[]",
161 | "name": "",
162 | "type": "uint256[]"
163 | }
164 | ],
165 | "stateMutability": "view",
166 | "type": "function"
167 | },
168 | {
169 | "inputs": [
170 | {
171 | "internalType": "address",
172 | "name": "account",
173 | "type": "address"
174 | },
175 | {
176 | "internalType": "address",
177 | "name": "operator",
178 | "type": "address"
179 | }
180 | ],
181 | "name": "isApprovedForAll",
182 | "outputs": [
183 | {
184 | "internalType": "bool",
185 | "name": "",
186 | "type": "bool"
187 | }
188 | ],
189 | "stateMutability": "view",
190 | "type": "function"
191 | },
192 | {
193 | "inputs": [
194 | {
195 | "internalType": "address",
196 | "name": "from",
197 | "type": "address"
198 | },
199 | {
200 | "internalType": "address",
201 | "name": "to",
202 | "type": "address"
203 | },
204 | {
205 | "internalType": "uint256[]",
206 | "name": "ids",
207 | "type": "uint256[]"
208 | },
209 | {
210 | "internalType": "uint256[]",
211 | "name": "amounts",
212 | "type": "uint256[]"
213 | },
214 | {
215 | "internalType": "bytes",
216 | "name": "data",
217 | "type": "bytes"
218 | }
219 | ],
220 | "name": "safeBatchTransferFrom",
221 | "outputs": [],
222 | "stateMutability": "nonpayable",
223 | "type": "function"
224 | },
225 | {
226 | "inputs": [
227 | {
228 | "internalType": "address",
229 | "name": "from",
230 | "type": "address"
231 | },
232 | {
233 | "internalType": "address",
234 | "name": "to",
235 | "type": "address"
236 | },
237 | {
238 | "internalType": "uint256",
239 | "name": "id",
240 | "type": "uint256"
241 | },
242 | {
243 | "internalType": "uint256",
244 | "name": "amount",
245 | "type": "uint256"
246 | },
247 | {
248 | "internalType": "bytes",
249 | "name": "data",
250 | "type": "bytes"
251 | }
252 | ],
253 | "name": "safeTransferFrom",
254 | "outputs": [],
255 | "stateMutability": "nonpayable",
256 | "type": "function"
257 | },
258 | {
259 | "inputs": [
260 | {
261 | "internalType": "address",
262 | "name": "operator",
263 | "type": "address"
264 | },
265 | {
266 | "internalType": "bool",
267 | "name": "approved",
268 | "type": "bool"
269 | }
270 | ],
271 | "name": "setApprovalForAll",
272 | "outputs": [],
273 | "stateMutability": "nonpayable",
274 | "type": "function"
275 | },
276 | {
277 | "inputs": [
278 | {
279 | "internalType": "bytes4",
280 | "name": "interfaceId",
281 | "type": "bytes4"
282 | }
283 | ],
284 | "name": "supportsInterface",
285 | "outputs": [
286 | {
287 | "internalType": "bool",
288 | "name": "",
289 | "type": "bool"
290 | }
291 | ],
292 | "stateMutability": "view",
293 | "type": "function"
294 | },
295 | {
296 | "inputs": [
297 | {
298 | "internalType": "uint256",
299 | "name": "id",
300 | "type": "uint256"
301 | }
302 | ],
303 | "name": "uri",
304 | "outputs": [
305 | {
306 | "internalType": "string",
307 | "name": "",
308 | "type": "string"
309 | }
310 | ],
311 | "stateMutability": "view",
312 | "type": "function"
313 | }
314 | ]
315 |
--------------------------------------------------------------------------------
/src/abi/ERC20.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "constant": true,
4 | "inputs": [],
5 | "name": "name",
6 | "outputs": [
7 | {
8 | "name": "",
9 | "type": "string"
10 | }
11 | ],
12 | "payable": false,
13 | "stateMutability": "view",
14 | "type": "function"
15 | },
16 | {
17 | "constant": false,
18 | "inputs": [
19 | {
20 | "name": "_spender",
21 | "type": "address"
22 | },
23 | {
24 | "name": "_value",
25 | "type": "uint256"
26 | }
27 | ],
28 | "name": "approve",
29 | "outputs": [
30 | {
31 | "name": "",
32 | "type": "bool"
33 | }
34 | ],
35 | "payable": false,
36 | "stateMutability": "nonpayable",
37 | "type": "function"
38 | },
39 | {
40 | "constant": true,
41 | "inputs": [],
42 | "name": "totalSupply",
43 | "outputs": [
44 | {
45 | "name": "",
46 | "type": "uint256"
47 | }
48 | ],
49 | "payable": false,
50 | "stateMutability": "view",
51 | "type": "function"
52 | },
53 | {
54 | "constant": false,
55 | "inputs": [
56 | {
57 | "name": "_from",
58 | "type": "address"
59 | },
60 | {
61 | "name": "_to",
62 | "type": "address"
63 | },
64 | {
65 | "name": "_value",
66 | "type": "uint256"
67 | }
68 | ],
69 | "name": "transferFrom",
70 | "outputs": [
71 | {
72 | "name": "",
73 | "type": "bool"
74 | }
75 | ],
76 | "payable": false,
77 | "stateMutability": "nonpayable",
78 | "type": "function"
79 | },
80 | {
81 | "constant": true,
82 | "inputs": [],
83 | "name": "decimals",
84 | "outputs": [
85 | {
86 | "name": "",
87 | "type": "uint8"
88 | }
89 | ],
90 | "payable": false,
91 | "stateMutability": "view",
92 | "type": "function"
93 | },
94 | {
95 | "constant": true,
96 | "inputs": [
97 | {
98 | "name": "_owner",
99 | "type": "address"
100 | }
101 | ],
102 | "name": "balanceOf",
103 | "outputs": [
104 | {
105 | "name": "balance",
106 | "type": "uint256"
107 | }
108 | ],
109 | "payable": false,
110 | "stateMutability": "view",
111 | "type": "function"
112 | },
113 | {
114 | "constant": true,
115 | "inputs": [],
116 | "name": "symbol",
117 | "outputs": [
118 | {
119 | "name": "",
120 | "type": "string"
121 | }
122 | ],
123 | "payable": false,
124 | "stateMutability": "view",
125 | "type": "function"
126 | },
127 | {
128 | "constant": false,
129 | "inputs": [
130 | {
131 | "name": "_to",
132 | "type": "address"
133 | },
134 | {
135 | "name": "_value",
136 | "type": "uint256"
137 | }
138 | ],
139 | "name": "transfer",
140 | "outputs": [
141 | {
142 | "name": "",
143 | "type": "bool"
144 | }
145 | ],
146 | "payable": false,
147 | "stateMutability": "nonpayable",
148 | "type": "function"
149 | },
150 | {
151 | "constant": true,
152 | "inputs": [
153 | {
154 | "name": "_owner",
155 | "type": "address"
156 | },
157 | {
158 | "name": "_spender",
159 | "type": "address"
160 | }
161 | ],
162 | "name": "allowance",
163 | "outputs": [
164 | {
165 | "name": "",
166 | "type": "uint256"
167 | }
168 | ],
169 | "payable": false,
170 | "stateMutability": "view",
171 | "type": "function"
172 | },
173 | {
174 | "payable": true,
175 | "stateMutability": "payable",
176 | "type": "fallback"
177 | },
178 | {
179 | "anonymous": false,
180 | "inputs": [
181 | {
182 | "indexed": true,
183 | "name": "owner",
184 | "type": "address"
185 | },
186 | {
187 | "indexed": true,
188 | "name": "spender",
189 | "type": "address"
190 | },
191 | {
192 | "indexed": false,
193 | "name": "value",
194 | "type": "uint256"
195 | }
196 | ],
197 | "name": "Approval",
198 | "type": "event"
199 | },
200 | {
201 | "anonymous": false,
202 | "inputs": [
203 | {
204 | "indexed": true,
205 | "name": "from",
206 | "type": "address"
207 | },
208 | {
209 | "indexed": true,
210 | "name": "to",
211 | "type": "address"
212 | },
213 | {
214 | "indexed": false,
215 | "name": "value",
216 | "type": "uint256"
217 | }
218 | ],
219 | "name": "Transfer",
220 | "type": "event"
221 | }
222 | ]
223 |
--------------------------------------------------------------------------------
/src/abi/ERC721.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "constant": false,
4 | "inputs": [
5 | {
6 | "internalType": "address",
7 | "name": "to",
8 | "type": "address"
9 | },
10 | {
11 | "internalType": "uint256",
12 | "name": "tokenId",
13 | "type": "uint256"
14 | }
15 | ],
16 | "name": "approve",
17 | "outputs": [],
18 | "payable": false,
19 | "stateMutability": "nonpayable",
20 | "type": "function"
21 | },
22 | {
23 | "constant": false,
24 | "inputs": [
25 | {
26 | "internalType": "address",
27 | "name": "to",
28 | "type": "address"
29 | },
30 | {
31 | "internalType": "uint256",
32 | "name": "tokenId",
33 | "type": "uint256"
34 | }
35 | ],
36 | "name": "mint",
37 | "outputs": [],
38 | "payable": false,
39 | "stateMutability": "nonpayable",
40 | "type": "function"
41 | },
42 | {
43 | "constant": false,
44 | "inputs": [
45 | {
46 | "internalType": "address",
47 | "name": "from",
48 | "type": "address"
49 | },
50 | {
51 | "internalType": "address",
52 | "name": "to",
53 | "type": "address"
54 | },
55 | {
56 | "internalType": "uint256",
57 | "name": "tokenId",
58 | "type": "uint256"
59 | }
60 | ],
61 | "name": "safeTransferFrom",
62 | "outputs": [],
63 | "payable": false,
64 | "stateMutability": "nonpayable",
65 | "type": "function"
66 | },
67 | {
68 | "constant": false,
69 | "inputs": [
70 | {
71 | "internalType": "address",
72 | "name": "from",
73 | "type": "address"
74 | },
75 | {
76 | "internalType": "address",
77 | "name": "to",
78 | "type": "address"
79 | },
80 | {
81 | "internalType": "uint256",
82 | "name": "tokenId",
83 | "type": "uint256"
84 | },
85 | {
86 | "internalType": "bytes",
87 | "name": "_data",
88 | "type": "bytes"
89 | }
90 | ],
91 | "name": "safeTransferFrom",
92 | "outputs": [],
93 | "payable": false,
94 | "stateMutability": "nonpayable",
95 | "type": "function"
96 | },
97 | {
98 | "constant": false,
99 | "inputs": [
100 | {
101 | "internalType": "address",
102 | "name": "to",
103 | "type": "address"
104 | },
105 | {
106 | "internalType": "bool",
107 | "name": "approved",
108 | "type": "bool"
109 | }
110 | ],
111 | "name": "setApprovalForAll",
112 | "outputs": [],
113 | "payable": false,
114 | "stateMutability": "nonpayable",
115 | "type": "function"
116 | },
117 | {
118 | "constant": false,
119 | "inputs": [
120 | {
121 | "internalType": "address",
122 | "name": "from",
123 | "type": "address"
124 | },
125 | {
126 | "internalType": "address",
127 | "name": "to",
128 | "type": "address"
129 | },
130 | {
131 | "internalType": "uint256",
132 | "name": "tokenId",
133 | "type": "uint256"
134 | }
135 | ],
136 | "name": "transferFrom",
137 | "outputs": [],
138 | "payable": false,
139 | "stateMutability": "nonpayable",
140 | "type": "function"
141 | },
142 | {
143 | "inputs": [],
144 | "payable": false,
145 | "stateMutability": "nonpayable",
146 | "type": "constructor"
147 | },
148 | {
149 | "anonymous": false,
150 | "inputs": [
151 | {
152 | "indexed": true,
153 | "internalType": "address",
154 | "name": "from",
155 | "type": "address"
156 | },
157 | {
158 | "indexed": true,
159 | "internalType": "address",
160 | "name": "to",
161 | "type": "address"
162 | },
163 | {
164 | "indexed": true,
165 | "internalType": "uint256",
166 | "name": "tokenId",
167 | "type": "uint256"
168 | }
169 | ],
170 | "name": "Transfer",
171 | "type": "event"
172 | },
173 | {
174 | "anonymous": false,
175 | "inputs": [
176 | {
177 | "indexed": true,
178 | "internalType": "address",
179 | "name": "owner",
180 | "type": "address"
181 | },
182 | {
183 | "indexed": true,
184 | "internalType": "address",
185 | "name": "approved",
186 | "type": "address"
187 | },
188 | {
189 | "indexed": true,
190 | "internalType": "uint256",
191 | "name": "tokenId",
192 | "type": "uint256"
193 | }
194 | ],
195 | "name": "Approval",
196 | "type": "event"
197 | },
198 | {
199 | "anonymous": false,
200 | "inputs": [
201 | {
202 | "indexed": true,
203 | "internalType": "address",
204 | "name": "owner",
205 | "type": "address"
206 | },
207 | {
208 | "indexed": true,
209 | "internalType": "address",
210 | "name": "operator",
211 | "type": "address"
212 | },
213 | {
214 | "indexed": false,
215 | "internalType": "bool",
216 | "name": "approved",
217 | "type": "bool"
218 | }
219 | ],
220 | "name": "ApprovalForAll",
221 | "type": "event"
222 | },
223 | {
224 | "constant": true,
225 | "inputs": [
226 | {
227 | "internalType": "address",
228 | "name": "owner",
229 | "type": "address"
230 | }
231 | ],
232 | "name": "balanceOf",
233 | "outputs": [
234 | {
235 | "internalType": "uint256",
236 | "name": "",
237 | "type": "uint256"
238 | }
239 | ],
240 | "payable": false,
241 | "stateMutability": "view",
242 | "type": "function"
243 | },
244 | {
245 | "constant": true,
246 | "inputs": [
247 | {
248 | "internalType": "uint256",
249 | "name": "tokenId",
250 | "type": "uint256"
251 | }
252 | ],
253 | "name": "getApproved",
254 | "outputs": [
255 | {
256 | "internalType": "address",
257 | "name": "",
258 | "type": "address"
259 | }
260 | ],
261 | "payable": false,
262 | "stateMutability": "view",
263 | "type": "function"
264 | },
265 | {
266 | "constant": true,
267 | "inputs": [
268 | {
269 | "internalType": "address",
270 | "name": "owner",
271 | "type": "address"
272 | },
273 | {
274 | "internalType": "address",
275 | "name": "operator",
276 | "type": "address"
277 | }
278 | ],
279 | "name": "isApprovedForAll",
280 | "outputs": [
281 | {
282 | "internalType": "bool",
283 | "name": "",
284 | "type": "bool"
285 | }
286 | ],
287 | "payable": false,
288 | "stateMutability": "view",
289 | "type": "function"
290 | },
291 | {
292 | "constant": true,
293 | "inputs": [
294 | {
295 | "internalType": "uint256",
296 | "name": "tokenId",
297 | "type": "uint256"
298 | }
299 | ],
300 | "name": "ownerOf",
301 | "outputs": [
302 | {
303 | "internalType": "address",
304 | "name": "",
305 | "type": "address"
306 | }
307 | ],
308 | "payable": false,
309 | "stateMutability": "view",
310 | "type": "function"
311 | },
312 | {
313 | "constant": true,
314 | "inputs": [
315 | {
316 | "internalType": "bytes4",
317 | "name": "interfaceId",
318 | "type": "bytes4"
319 | }
320 | ],
321 | "name": "supportsInterface",
322 | "outputs": [
323 | {
324 | "internalType": "bool",
325 | "name": "",
326 | "type": "bool"
327 | }
328 | ],
329 | "payable": false,
330 | "stateMutability": "view",
331 | "type": "function"
332 | }
333 | ]
334 |
--------------------------------------------------------------------------------
/src/api/api.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import {
3 | getCollectionPath,
4 | getCollectionsPath,
5 | getOrdersAPIPath,
6 | getPostCollectionOfferPath,
7 | getBuildOfferPath,
8 | getListNFTsByCollectionPath,
9 | getListNFTsByContractPath,
10 | getNFTPath,
11 | getRefreshMetadataPath,
12 | getCollectionOffersPath,
13 | getListNFTsByAccountPath,
14 | getBestOfferAPIPath,
15 | getBestListingAPIPath,
16 | getAllOffersAPIPath,
17 | getAllListingsAPIPath,
18 | getPaymentTokenPath,
19 | getAccountPath,
20 | getCollectionStatsPath,
21 | getBestListingsAPIPath,
22 | getCancelOrderPath,
23 | } from "./apiPaths";
24 | import {
25 | BuildOfferResponse,
26 | GetCollectionResponse,
27 | GetCollectionsResponse,
28 | ListNFTsResponse,
29 | GetNFTResponse,
30 | ListCollectionOffersResponse,
31 | GetOrdersResponse,
32 | GetBestOfferResponse,
33 | GetBestListingResponse,
34 | GetOffersResponse,
35 | GetListingsResponse,
36 | CollectionOffer,
37 | CollectionOrderByOption,
38 | CancelOrderResponse,
39 | GetCollectionsArgs,
40 | } from "./types";
41 | import { API_BASE_MAINNET, API_BASE_TESTNET } from "../constants";
42 | import {
43 | FulfillmentDataResponse,
44 | OrderAPIOptions,
45 | OrdersPostQueryResponse,
46 | OrdersQueryOptions,
47 | OrdersQueryResponse,
48 | OrderV2,
49 | ProtocolData,
50 | } from "../orders/types";
51 | import {
52 | serializeOrdersQueryOptions,
53 | deserializeOrder,
54 | getFulfillmentDataPath,
55 | getFulfillListingPayload,
56 | getFulfillOfferPayload,
57 | getBuildCollectionOfferPayload,
58 | getPostCollectionOfferPayload,
59 | } from "../orders/utils";
60 | import {
61 | Chain,
62 | OpenSeaAPIConfig,
63 | OpenSeaAccount,
64 | OpenSeaCollection,
65 | OpenSeaCollectionStats,
66 | OpenSeaPaymentToken,
67 | OrderSide,
68 | } from "../types";
69 | import {
70 | paymentTokenFromJSON,
71 | collectionFromJSON,
72 | isTestChain,
73 | accountFromJSON,
74 | } from "../utils/utils";
75 |
76 | /**
77 | * The API class for the OpenSea SDK.
78 | * @category Main Classes
79 | */
80 | export class OpenSeaAPI {
81 | /**
82 | * Base url for the API
83 | */
84 | public readonly apiBaseUrl: string;
85 | /**
86 | * Default size to use for fetching orders
87 | */
88 | public pageSize = 20;
89 | /**
90 | * Logger function to use when debugging
91 | */
92 | public logger: (arg: string) => void;
93 |
94 | private apiKey: string | undefined;
95 | private chain: Chain;
96 |
97 | /**
98 | * Create an instance of the OpenSeaAPI
99 | * @param config OpenSeaAPIConfig for setting up the API, including an optional API key, Chain name, and base URL
100 | * @param logger Optional function for logging debug strings before and after requests are made. Defaults to no logging
101 | */
102 | constructor(config: OpenSeaAPIConfig, logger?: (arg: string) => void) {
103 | this.apiKey = config.apiKey;
104 | this.chain = config.chain ?? Chain.Mainnet;
105 |
106 | if (config.apiBaseUrl) {
107 | this.apiBaseUrl = config.apiBaseUrl;
108 | } else {
109 | this.apiBaseUrl = isTestChain(this.chain)
110 | ? API_BASE_TESTNET
111 | : API_BASE_MAINNET;
112 | }
113 |
114 | // Debugging: default to nothing
115 | this.logger = logger ?? ((arg: string) => arg);
116 | }
117 |
118 | /**
119 | * Gets an order from API based on query options.
120 | * @param options
121 | * @param options.side The side of the order (listing or offer)
122 | * @param options.protocol The protocol, typically seaport, to query orders for
123 | * @param options.orderDirection The direction to sort the orders
124 | * @param options.orderBy The field to sort the orders by
125 | * @param options.limit The number of orders to retrieve
126 | * @param options.maker Filter by the wallet address of the order maker
127 | * @param options.taker Filter by wallet address of the order taker
128 | * @param options.asset_contract_address Address of the NFT's contract
129 | * @param options.token_ids String array of token IDs to filter by.
130 | * @param options.listed_after Filter by orders listed after the Unix epoch timestamp in seconds
131 | * @param options.listed_before Filter by orders listed before the Unix epoch timestamp in seconds
132 | * @returns The first {@link OrderV2} returned by the API
133 | *
134 | * @throws An error if there are no matching orders.
135 | */
136 | public async getOrder({
137 | side,
138 | protocol = "seaport",
139 | orderDirection = "desc",
140 | orderBy = "created_date",
141 | ...restOptions
142 | }: Omit): Promise {
143 | const { orders } = await this.get(
144 | getOrdersAPIPath(this.chain, protocol, side),
145 | serializeOrdersQueryOptions({
146 | limit: 1,
147 | orderBy,
148 | orderDirection,
149 | ...restOptions,
150 | }),
151 | );
152 | if (orders.length === 0) {
153 | throw new Error("Not found: no matching order found");
154 | }
155 | return deserializeOrder(orders[0]);
156 | }
157 |
158 | /**
159 | * Gets a list of orders from API based on query options.
160 | * @param options
161 | * @param options.side The side of the order (buy or sell)
162 | * @param options.protocol The protocol, typically seaport, to query orders for
163 | * @param options.orderDirection The direction to sort the orders
164 | * @param options.orderBy The field to sort the orders by
165 | * @param options.limit The number of orders to retrieve
166 | * @param options.maker Filter by the wallet address of the order maker
167 | * @param options.taker Filter by wallet address of the order taker
168 | * @param options.asset_contract_address Address of the NFT's contract
169 | * @param options.token_ids String array of token IDs to filter by.
170 | * @param options.listed_after Filter by orders listed after the Unix epoch timestamp in seconds
171 | * @param options.listed_before Filter by orders listed before the Unix epoch timestamp in seconds
172 | * @returns The {@link GetOrdersResponse} returned by the API.
173 | */
174 | public async getOrders({
175 | side,
176 | protocol = "seaport",
177 | orderDirection = "desc",
178 | orderBy = "created_date",
179 | ...restOptions
180 | }: Omit): Promise {
181 | const response = await this.get(
182 | getOrdersAPIPath(this.chain, protocol, side),
183 | serializeOrdersQueryOptions({
184 | limit: this.pageSize,
185 | orderBy,
186 | orderDirection,
187 | ...restOptions,
188 | }),
189 | );
190 | return {
191 | ...response,
192 | orders: response.orders.map(deserializeOrder),
193 | };
194 | }
195 |
196 | /**
197 | * Gets all offers for a given collection.
198 | * @param collectionSlug The slug of the collection.
199 | * @param limit The number of offers to return. Must be between 1 and 100. Default: 100
200 | * @param next The cursor for the next page of results. This is returned from a previous request.
201 | * @returns The {@link GetOffersResponse} returned by the API.
202 | */
203 | public async getAllOffers(
204 | collectionSlug: string,
205 | limit?: number,
206 | next?: string,
207 | ): Promise {
208 | const response = await this.get(
209 | getAllOffersAPIPath(collectionSlug),
210 | {
211 | limit,
212 | next,
213 | },
214 | );
215 | return response;
216 | }
217 |
218 | /**
219 | * Gets all listings for a given collection.
220 | * @param collectionSlug The slug of the collection.
221 | * @param limit The number of listings to return. Must be between 1 and 100. Default: 100
222 | * @param next The cursor for the next page of results. This is returned from a previous request.
223 | * @returns The {@link GetListingsResponse} returned by the API.
224 | */
225 | public async getAllListings(
226 | collectionSlug: string,
227 | limit?: number,
228 | next?: string,
229 | ): Promise {
230 | const response = await this.get(
231 | getAllListingsAPIPath(collectionSlug),
232 | {
233 | limit,
234 | next,
235 | },
236 | );
237 | return response;
238 | }
239 |
240 | /**
241 | * Gets the best offer for a given token.
242 | * @param collectionSlug The slug of the collection.
243 | * @param tokenId The token identifier.
244 | * @returns The {@link GetBestOfferResponse} returned by the API.
245 | */
246 | public async getBestOffer(
247 | collectionSlug: string,
248 | tokenId: string | number,
249 | ): Promise {
250 | const response = await this.get(
251 | getBestOfferAPIPath(collectionSlug, tokenId),
252 | );
253 | return response;
254 | }
255 |
256 | /**
257 | * Gets the best listing for a given token.
258 | * @param collectionSlug The slug of the collection.
259 | * @param tokenId The token identifier.
260 | * @returns The {@link GetBestListingResponse} returned by the API.
261 | */
262 | public async getBestListing(
263 | collectionSlug: string,
264 | tokenId: string | number,
265 | ): Promise {
266 | const response = await this.get(
267 | getBestListingAPIPath(collectionSlug, tokenId),
268 | );
269 | return response;
270 | }
271 |
272 | /**
273 | * Gets the best listings for a given collection.
274 | * @param collectionSlug The slug of the collection.
275 | * @param limit The number of listings to return. Must be between 1 and 100. Default: 100
276 | * @param next The cursor for the next page of results. This is returned from a previous request.
277 | * @returns The {@link GetListingsResponse} returned by the API.
278 | */
279 | public async getBestListings(
280 | collectionSlug: string,
281 | limit?: number,
282 | next?: string,
283 | ): Promise {
284 | const response = await this.get(
285 | getBestListingsAPIPath(collectionSlug),
286 | {
287 | limit,
288 | next,
289 | },
290 | );
291 | return response;
292 | }
293 |
294 | /**
295 | * Generate the data needed to fulfill a listing or an offer onchain.
296 | * @param fulfillerAddress The wallet address which will be used to fulfill the order
297 | * @param orderHash The hash of the order to fulfill
298 | * @param protocolAddress The address of the seaport contract
299 | * @side The side of the order (buy or sell)
300 | * @returns The {@link FulfillmentDataResponse}
301 | */
302 | public async generateFulfillmentData(
303 | fulfillerAddress: string,
304 | orderHash: string,
305 | protocolAddress: string,
306 | side: OrderSide,
307 | ): Promise {
308 | let payload: object | null = null;
309 | if (side === OrderSide.LISTING) {
310 | payload = getFulfillListingPayload(
311 | fulfillerAddress,
312 | orderHash,
313 | protocolAddress,
314 | this.chain,
315 | );
316 | } else {
317 | payload = getFulfillOfferPayload(
318 | fulfillerAddress,
319 | orderHash,
320 | protocolAddress,
321 | this.chain,
322 | );
323 | }
324 | const response = await this.post(
325 | getFulfillmentDataPath(side),
326 | payload,
327 | );
328 | return response;
329 | }
330 |
331 | /**
332 | * Post an order to OpenSea.
333 | * @param order The order to post
334 | * @param apiOptions
335 | * @param apiOptions.protocol The protocol, typically seaport, to post the order to.
336 | * @param apiOptions.side The side of the order (buy or sell).
337 | * @param apiOptions.protocolAddress The address of the seaport contract.
338 | * @param options
339 | * @returns The {@link OrderV2} posted to the API.
340 | */
341 | public async postOrder(
342 | order: ProtocolData,
343 | apiOptions: OrderAPIOptions,
344 | ): Promise {
345 | const { protocol = "seaport", side, protocolAddress } = apiOptions;
346 |
347 | // Validate required fields
348 | if (!side) {
349 | throw new Error("apiOptions.side is required");
350 | }
351 | if (!protocolAddress) {
352 | throw new Error("apiOptions.protocolAddress is required");
353 | }
354 | if (!order) {
355 | throw new Error("order data is required");
356 | }
357 |
358 | // Validate protocol value
359 | if (protocol !== "seaport") {
360 | throw new Error("Currently only 'seaport' protocol is supported");
361 | }
362 |
363 | // Validate side value
364 | if (side !== "ask" && side !== "bid") {
365 | throw new Error("side must be either 'ask' or 'bid'");
366 | }
367 |
368 | // Validate protocolAddress format
369 | if (!/^0x[a-fA-F0-9]{40}$/.test(protocolAddress)) {
370 | throw new Error("Invalid protocol address format");
371 | }
372 |
373 | const response = await this.post(
374 | getOrdersAPIPath(this.chain, protocol, side),
375 | { ...order, protocol_address: protocolAddress },
376 | );
377 | return deserializeOrder(response.order);
378 | }
379 |
380 | /**
381 | * Build a OpenSea collection offer.
382 | * @param offererAddress The wallet address which is creating the offer.
383 | * @param quantity The number of NFTs requested in the offer.
384 | * @param collectionSlug The slug (identifier) of the collection to build the offer for.
385 | * @param offerProtectionEnabled Build the offer on OpenSea's signed zone to provide offer protections from receiving an item which is disabled from trading.
386 | * @param traitType If defined, the trait name to create the collection offer for.
387 | * @param traitValue If defined, the trait value to create the collection offer for.
388 | * @returns The {@link BuildOfferResponse} returned by the API.
389 | */
390 | public async buildOffer(
391 | offererAddress: string,
392 | quantity: number,
393 | collectionSlug: string,
394 | offerProtectionEnabled = true,
395 | traitType?: string,
396 | traitValue?: string,
397 | ): Promise {
398 | if (traitType || traitValue) {
399 | if (!traitType || !traitValue) {
400 | throw new Error(
401 | "Both traitType and traitValue must be defined if one is defined.",
402 | );
403 | }
404 | }
405 | const payload = getBuildCollectionOfferPayload(
406 | offererAddress,
407 | quantity,
408 | collectionSlug,
409 | offerProtectionEnabled,
410 | traitType,
411 | traitValue,
412 | );
413 | const response = await this.post(
414 | getBuildOfferPath(),
415 | payload,
416 | );
417 | return response;
418 | }
419 |
420 | /**
421 | * Get a list collection offers for a given slug.
422 | * @param slug The slug (identifier) of the collection to list offers for
423 | * @returns The {@link ListCollectionOffersResponse} returned by the API.
424 | */
425 | public async getCollectionOffers(
426 | slug: string,
427 | ): Promise {
428 | return await this.get(
429 | getCollectionOffersPath(slug),
430 | );
431 | }
432 |
433 | /**
434 | * Post a collection offer to OpenSea.
435 | * @param order The collection offer to post.
436 | * @param slug The slug (identifier) of the collection to post the offer for.
437 | * @param traitType If defined, the trait name to create the collection offer for.
438 | * @param traitValue If defined, the trait value to create the collection offer for.
439 | * @returns The {@link Offer} returned to the API.
440 | */
441 | public async postCollectionOffer(
442 | order: ProtocolData,
443 | slug: string,
444 | traitType?: string,
445 | traitValue?: string,
446 | ): Promise {
447 | const payload = getPostCollectionOfferPayload(
448 | slug,
449 | order,
450 | traitType,
451 | traitValue,
452 | );
453 | return await this.post(
454 | getPostCollectionOfferPath(),
455 | payload,
456 | );
457 | }
458 |
459 | /**
460 | * Fetch multiple NFTs for a collection.
461 | * @param slug The slug (identifier) of the collection
462 | * @param limit The number of NFTs to retrieve. Must be greater than 0 and less than 51.
463 | * @param next Cursor to retrieve the next page of NFTs
464 | * @returns The {@link ListNFTsResponse} returned by the API.
465 | */
466 | public async getNFTsByCollection(
467 | slug: string,
468 | limit: number | undefined = undefined,
469 | next: string | undefined = undefined,
470 | ): Promise {
471 | const response = await this.get(
472 | getListNFTsByCollectionPath(slug),
473 | {
474 | limit,
475 | next,
476 | },
477 | );
478 | return response;
479 | }
480 |
481 | /**
482 | * Fetch multiple NFTs for a contract.
483 | * @param address The NFT's contract address.
484 | * @param limit The number of NFTs to retrieve. Must be greater than 0 and less than 51.
485 | * @param next Cursor to retrieve the next page of NFTs.
486 | * @param chain The NFT's chain.
487 | * @returns The {@link ListNFTsResponse} returned by the API.
488 | */
489 | public async getNFTsByContract(
490 | address: string,
491 | limit: number | undefined = undefined,
492 | next: string | undefined = undefined,
493 | chain: Chain = this.chain,
494 | ): Promise {
495 | const response = await this.get(
496 | getListNFTsByContractPath(chain, address),
497 | {
498 | limit,
499 | next,
500 | },
501 | );
502 | return response;
503 | }
504 |
505 | /**
506 | * Fetch NFTs owned by an account.
507 | * @param address The address of the account
508 | * @param limit The number of NFTs to retrieve. Must be greater than 0 and less than 51.
509 | * @param next Cursor to retrieve the next page of NFTs
510 | * @param chain The chain to query. Defaults to the chain set in the constructor.
511 | * @returns The {@link ListNFTsResponse} returned by the API.
512 | */
513 | public async getNFTsByAccount(
514 | address: string,
515 | limit: number | undefined = undefined,
516 | next: string | undefined = undefined,
517 | chain = this.chain,
518 | ): Promise {
519 | const response = await this.get(
520 | getListNFTsByAccountPath(chain, address),
521 | {
522 | limit,
523 | next,
524 | },
525 | );
526 |
527 | return response;
528 | }
529 |
530 | /**
531 | * Fetch metadata, traits, ownership information, and rarity for a single NFT.
532 | * @param address The NFT's contract address.
533 | * @param identifier the identifier of the NFT (i.e. Token ID)
534 | * @param chain The NFT's chain.
535 | * @returns The {@link GetNFTResponse} returned by the API.
536 | */
537 | public async getNFT(
538 | address: string,
539 | identifier: string,
540 | chain = this.chain,
541 | ): Promise {
542 | const response = await this.get(
543 | getNFTPath(chain, address, identifier),
544 | );
545 | return response;
546 | }
547 |
548 | /**
549 | * Fetch an OpenSea collection.
550 | * @param slug The slug (identifier) of the collection.
551 | * @returns The {@link OpenSeaCollection} returned by the API.
552 | */
553 | public async getCollection(slug: string): Promise {
554 | const path = getCollectionPath(slug);
555 | const response = await this.get(path);
556 | return collectionFromJSON(response);
557 | }
558 |
559 | /**
560 | * Fetch a list of OpenSea collections.
561 | * @param orderBy The order to return the collections in. Default: CREATED_DATE
562 | * @param chain The chain to filter the collections on. Default: all chains
563 | * @param creatorUsername The creator's OpenSea username to filter the collections on.
564 | * @param includeHidden If hidden collections should be returned. Default: false
565 | * @param limit The limit of collections to return.
566 | * @param next The cursor for the next page of results. This is returned from a previous request.
567 | * @returns List of {@link OpenSeaCollection} returned by the API.
568 | */
569 | public async getCollections(
570 | orderBy: CollectionOrderByOption = CollectionOrderByOption.CREATED_DATE,
571 | chain?: Chain,
572 | creatorUsername?: string,
573 | includeHidden: boolean = false,
574 | limit?: number,
575 | next?: string,
576 | ): Promise {
577 | const path = getCollectionsPath();
578 | const args: GetCollectionsArgs = {
579 | order_by: orderBy,
580 | chain,
581 | creator_username: creatorUsername,
582 | include_hidden: includeHidden,
583 | limit,
584 | next,
585 | };
586 | const response = await this.get(path, args);
587 | response.collections = response.collections.map((collection) =>
588 | collectionFromJSON(collection),
589 | );
590 | return response;
591 | }
592 |
593 | /**
594 | * Fetch stats for an OpenSea collection.
595 | * @param slug The slug (identifier) of the collection.
596 | * @returns The {@link OpenSeaCollection} returned by the API.
597 | */
598 | public async getCollectionStats(
599 | slug: string,
600 | ): Promise {
601 | const path = getCollectionStatsPath(slug);
602 | const response = await this.get(path);
603 | return response as OpenSeaCollectionStats;
604 | }
605 |
606 | /**
607 | * Fetch a payment token.
608 | * @param query Query to use for getting tokens. See {@link OpenSeaPaymentTokenQuery}.
609 | * @param next The cursor for the next page of results. This is returned from a previous request.
610 | * @returns The {@link OpenSeaPaymentToken} returned by the API.
611 | */
612 | public async getPaymentToken(
613 | address: string,
614 | chain = this.chain,
615 | ): Promise {
616 | const json = await this.get(
617 | getPaymentTokenPath(chain, address),
618 | );
619 | return paymentTokenFromJSON(json);
620 | }
621 |
622 | /**
623 | * Fetch account for an address.
624 | * @param query Query to use for getting tokens. See {@link OpenSeaPaymentTokenQuery}.
625 | * @param next The cursor for the next page of results. This is returned from a previous request.
626 | * @returns The {@link GetAccountResponse} returned by the API.
627 | */
628 | public async getAccount(address: string): Promise {
629 | const json = await this.get(getAccountPath(address));
630 | return accountFromJSON(json);
631 | }
632 |
633 | /**
634 | * Force refresh the metadata for an NFT.
635 | * @param address The address of the NFT's contract.
636 | * @param identifier The identifier of the NFT.
637 | * @param chain The chain where the NFT is located.
638 | * @returns The response from the API.
639 | */
640 | public async refreshNFTMetadata(
641 | address: string,
642 | identifier: string,
643 | chain: Chain = this.chain,
644 | ): Promise {
645 | const response = await this.post(
646 | getRefreshMetadataPath(chain, address, identifier),
647 | {},
648 | );
649 |
650 | return response;
651 | }
652 |
653 | /**
654 | * Offchain cancel an order, offer or listing, by its order hash when protected by the SignedZone.
655 | * Protocol and Chain are required to prevent hash collisions.
656 | * Please note cancellation is only assured if a fulfillment signature was not vended prior to cancellation.
657 | * @param protocolAddress The Seaport address for the order.
658 | * @param orderHash The order hash, or external identifier, of the order.
659 | * @param chain The chain where the order is located.
660 | * @param offererSignature An EIP-712 signature from the offerer of the order.
661 | * If this is not provided, the user associated with the API Key will be checked instead.
662 | * The signature must be a EIP-712 signature consisting of the order's Seaport contract's
663 | * name, version, address, and chain. The struct to sign is `OrderHash` containing a
664 | * single bytes32 field.
665 | * @returns The response from the API.
666 | */
667 | public async offchainCancelOrder(
668 | protocolAddress: string,
669 | orderHash: string,
670 | chain: Chain = this.chain,
671 | offererSignature?: string,
672 | ): Promise {
673 | const response = await this.post(
674 | getCancelOrderPath(chain, protocolAddress, orderHash),
675 | { offererSignature },
676 | );
677 | return response;
678 | }
679 |
680 | /**
681 | * Generic fetch method for any API endpoint
682 | * @param apiPath Path to URL endpoint under API
683 | * @param query URL query params. Will be used to create a URLSearchParams object.
684 | * @returns @typeParam T The response from the API.
685 | */
686 | public async get(apiPath: string, query: object = {}): Promise {
687 | const qs = this.objectToSearchParams(query);
688 | const url = `${this.apiBaseUrl}${apiPath}?${qs}`;
689 | return await this._fetch(url);
690 | }
691 |
692 | /**
693 | * Generic post method for any API endpoint.
694 | * @param apiPath Path to URL endpoint under API
695 | * @param body Data to send.
696 | * @param opts ethers ConnectionInfo, similar to Fetch API.
697 | * @returns @typeParam T The response from the API.
698 | */
699 | public async post(
700 | apiPath: string,
701 | body?: object,
702 | opts?: object,
703 | ): Promise {
704 | const url = `${this.apiBaseUrl}${apiPath}`;
705 | return await this._fetch(url, opts, body);
706 | }
707 |
708 | private objectToSearchParams(params: object = {}) {
709 | const urlSearchParams = new URLSearchParams();
710 |
711 | Object.entries(params).forEach(([key, value]) => {
712 | if (value && Array.isArray(value)) {
713 | value.forEach((item) => item && urlSearchParams.append(key, item));
714 | } else if (value) {
715 | urlSearchParams.append(key, value);
716 | }
717 | });
718 |
719 | return urlSearchParams.toString();
720 | }
721 |
722 | /**
723 | * Get from an API Endpoint, sending auth token in headers
724 | * @param opts ethers ConnectionInfo, similar to Fetch API
725 | * @param body Optional body to send. If set, will POST, otherwise GET
726 | */
727 | private async _fetch(url: string, headers?: object, body?: object) {
728 | // Create the fetch request
729 | const req = new ethers.FetchRequest(url);
730 |
731 | // Set the headers
732 | headers = {
733 | "x-app-id": "opensea-js",
734 | ...(this.apiKey ? { "X-API-KEY": this.apiKey } : {}),
735 | ...headers,
736 | };
737 | for (const [key, value] of Object.entries(headers)) {
738 | req.setHeader(key, value);
739 | }
740 |
741 | // Set the body if provided
742 | if (body) {
743 | req.body = body;
744 | }
745 |
746 | // Set the throttle params
747 | req.setThrottleParams({ slotInterval: 1000 });
748 |
749 | this.logger(
750 | `Sending request: ${url} ${JSON.stringify({
751 | request: req,
752 | headers: req.headers,
753 | })}`,
754 | );
755 |
756 | const response = await req.send();
757 | if (!response.ok()) {
758 | // If an errors array is returned, throw with the error messages.
759 | const errors = response.bodyJson?.errors;
760 | if (errors?.length > 0) {
761 | let errorMessage = errors.join(", ");
762 | if (errorMessage === "[object Object]") {
763 | errorMessage = JSON.stringify(errors);
764 | }
765 | throw new Error(`Server Error: ${errorMessage}`);
766 | } else {
767 | // Otherwise, let ethers throw a SERVER_ERROR since it will include
768 | // more context about the request and response.
769 | response.assertOk();
770 | }
771 | }
772 | return response.bodyJson;
773 | }
774 | }
775 |
--------------------------------------------------------------------------------
/src/api/apiPaths.ts:
--------------------------------------------------------------------------------
1 | import { OrderProtocol } from "../orders/types";
2 | import { Chain, OrderSide } from "../types";
3 |
4 | export const getOrdersAPIPath = (
5 | chain: Chain,
6 | protocol: OrderProtocol,
7 | side: OrderSide,
8 | ) => {
9 | const sidePath = side === OrderSide.LISTING ? "listings" : "offers";
10 | return `/v2/orders/${chain}/${protocol}/${sidePath}`;
11 | };
12 |
13 | export const getAllOffersAPIPath = (collectionSlug: string) => {
14 | return `/v2/offers/collection/${collectionSlug}/all`;
15 | };
16 |
17 | export const getAllListingsAPIPath = (collectionSlug: string) => {
18 | return `/v2/listings/collection/${collectionSlug}/all`;
19 | };
20 |
21 | export const getBestOfferAPIPath = (
22 | collectionSlug: string,
23 | tokenId: string | number,
24 | ) => {
25 | return `/v2/offers/collection/${collectionSlug}/nfts/${tokenId}/best`;
26 | };
27 |
28 | export const getBestListingAPIPath = (
29 | collectionSlug: string,
30 | tokenId: string | number,
31 | ) => {
32 | return `/v2/listings/collection/${collectionSlug}/nfts/${tokenId}/best`;
33 | };
34 |
35 | export const getBestListingsAPIPath = (collectionSlug: string) => {
36 | return `/v2/listings/collection/${collectionSlug}/best`;
37 | };
38 |
39 | export const getCollectionPath = (slug: string) => {
40 | return `/api/v2/collections/${slug}`;
41 | };
42 |
43 | export const getCollectionsPath = () => {
44 | return "/api/v2/collections";
45 | };
46 |
47 | export const getCollectionStatsPath = (slug: string) => {
48 | return `/api/v2/collections/${slug}/stats`;
49 | };
50 |
51 | export const getPaymentTokenPath = (chain: Chain, address: string) => {
52 | return `/v2/chain/${chain}/payment_token/${address}`;
53 | };
54 |
55 | export const getAccountPath = (address: string) => {
56 | return `/v2/accounts/${address}`;
57 | };
58 |
59 | export const getBuildOfferPath = () => {
60 | return `/v2/offers/build`;
61 | };
62 |
63 | export const getPostCollectionOfferPath = () => {
64 | return `/v2/offers`;
65 | };
66 |
67 | export const getCollectionOffersPath = (slug: string) => {
68 | return `/v2/offers/collection/${slug}`;
69 | };
70 |
71 | export const getListNFTsByCollectionPath = (slug: string) => {
72 | return `/v2/collection/${slug}/nfts`;
73 | };
74 |
75 | export const getListNFTsByContractPath = (chain: Chain, address: string) => {
76 | return `/v2/chain/${chain}/contract/${address}/nfts`;
77 | };
78 |
79 | export const getListNFTsByAccountPath = (chain: Chain, address: string) => {
80 | return `/v2/chain/${chain}/account/${address}/nfts`;
81 | };
82 |
83 | export const getNFTPath = (
84 | chain: Chain,
85 | address: string,
86 | identifier: string,
87 | ) => {
88 | return `/v2/chain/${chain}/contract/${address}/nfts/${identifier}`;
89 | };
90 |
91 | export const getRefreshMetadataPath = (
92 | chain: Chain,
93 | address: string,
94 | identifier: string,
95 | ) => {
96 | return `/v2/chain/${chain}/contract/${address}/nfts/${identifier}/refresh`;
97 | };
98 |
99 | export const getCancelOrderPath = (
100 | chain: Chain,
101 | protocolAddress: string,
102 | orderHash: string,
103 | ) => {
104 | return `/v2/orders/chain/${chain}/protocol/${protocolAddress}/${orderHash}/cancel`;
105 | };
106 |
--------------------------------------------------------------------------------
/src/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./api";
2 |
--------------------------------------------------------------------------------
/src/api/types.ts:
--------------------------------------------------------------------------------
1 | import { ConsiderationItem } from "@opensea/seaport-js/lib/types";
2 | import {
3 | OrderType,
4 | OrderV2,
5 | ProtocolData,
6 | QueryCursors,
7 | } from "../orders/types";
8 | import { OpenSeaCollection } from "../types";
9 |
10 | /**
11 | * Response from OpenSea API for building an offer.
12 | * @category API Response Types
13 | */
14 | export type BuildOfferResponse = {
15 | /** A portion of the parameters needed to submit a criteria offer, i.e. collection offer. */
16 | partialParameters: PartialParameters;
17 | };
18 |
19 | type PartialParameters = {
20 | consideration: ConsiderationItem[];
21 | zone: string;
22 | zoneHash: string;
23 | };
24 |
25 | /**
26 | * Criteria for collection or trait offers.
27 | * @category API Response Types
28 | */
29 | type Criteria = {
30 | /** The collection for the criteria */
31 | collection: CollectionCriteria;
32 | /** The contract for the criteria */
33 | contract: ContractCriteria;
34 | /** Represents a list of token ids which can be used to fulfill the criteria offer. */
35 | encoded_token_ids?: string;
36 | /** The trait for the criteria */
37 | trait?: TraitCriteria;
38 | };
39 |
40 | /**
41 | * Criteria for trait offers.
42 | * @category API Response Types
43 | */
44 | type TraitCriteria = {
45 | type: string;
46 | value: string;
47 | };
48 |
49 | type CollectionCriteria = {
50 | slug: string;
51 | };
52 |
53 | type ContractCriteria = {
54 | address: string;
55 | };
56 |
57 | /**
58 | * Query args for Get Collections
59 | * @category API Query Args
60 | */
61 | export interface GetCollectionsArgs {
62 | order_by?: string;
63 | limit?: number;
64 | next?: string;
65 | chain?: string;
66 | creator_username?: string;
67 | include_hidden?: boolean;
68 | }
69 |
70 | /**
71 | * Response from OpenSea API for fetching a single collection.
72 | * @category API Response Types
73 | */
74 | export type GetCollectionResponse = {
75 | /** Collection object. See {@link OpenSeaCollection} */
76 | collection: OpenSeaCollection;
77 | };
78 |
79 | /**
80 | * Response from OpenSea API for fetching a list of collections.
81 | * @category API Response Types
82 | */
83 | export type GetCollectionsResponse = QueryCursorsV2 & {
84 | /** List of collections. See {@link OpenSeaCollection} */
85 | collections: OpenSeaCollection[];
86 | };
87 |
88 | export enum CollectionOrderByOption {
89 | CREATED_DATE = "created_date",
90 | ONE_DAY_CHANGE = "one_day_change",
91 | SEVEN_DAY_VOLUME = "seven_day_volume",
92 | SEVEN_DAY_CHANGE = "seven_day_change",
93 | NUM_OWNERS = "num_owners",
94 | MARKET_CAP = "market_cap",
95 | }
96 |
97 | /**
98 | * Base Order type shared between Listings and Offers.
99 | * @category API Models
100 | */
101 | export type Order = {
102 | /** Offer Identifier */
103 | order_hash: string;
104 | /** Chain the offer exists on */
105 | chain: string;
106 | /** The protocol data for the order. Only 'seaport' is currently supported. */
107 | protocol_data: ProtocolData;
108 | /** The contract address of the protocol. */
109 | protocol_address: string;
110 | /** The price of the order. */
111 | price: Price;
112 | };
113 |
114 | /**
115 | * Offer type.
116 | * @category API Models
117 | */
118 | export type Offer = Order & {
119 | /** The criteria for the offer if it is a collection or trait offer. */
120 | criteria?: Criteria;
121 | };
122 |
123 | /**
124 | * Collection Offer type.
125 | * @category API Models
126 | */
127 | export type CollectionOffer = Required> & Offer;
128 |
129 | /**
130 | * Price response.
131 | * @category API Models
132 | */
133 | export type Price = {
134 | currency: string;
135 | decimals: number;
136 | value: string;
137 | };
138 |
139 | /**
140 | * Listing order type.
141 | * @category API Models
142 | */
143 | export type Listing = Order & {
144 | /** The order type of the listing. */
145 | type: OrderType;
146 | };
147 |
148 | /**
149 | * Response from OpenSea API for fetching a list of collection offers.
150 | * @category API Response Types
151 | */
152 | export type ListCollectionOffersResponse = {
153 | /** List of {@link Offer} */
154 | offers: CollectionOffer[];
155 | };
156 |
157 | /**
158 | * Response from OpenSea API for fetching a list of NFTs.
159 | * @category API Response Types
160 | */
161 | export type ListNFTsResponse = {
162 | /** List of {@link NFT} */
163 | nfts: NFT[];
164 | /** Cursor for next page of results. */
165 | next: string;
166 | };
167 |
168 | /**
169 | * Response from OpenSea API for fetching a single NFT.
170 | * @category API Response Types
171 | */
172 | export type GetNFTResponse = {
173 | /** See {@link NFT} */
174 | nft: NFT;
175 | };
176 |
177 | /**
178 | * Response from OpenSea API for fetching Orders.
179 | * @category API Response Types
180 | */
181 | export type GetOrdersResponse = QueryCursors & {
182 | /** List of {@link OrderV2} */
183 | orders: OrderV2[];
184 | };
185 |
186 | /**
187 | * Base query cursors response from OpenSea API.
188 | * @category API Response Types
189 | */
190 | export type QueryCursorsV2 = {
191 | next?: string;
192 | };
193 |
194 | /**
195 | * Response from OpenSea API for fetching offers.
196 | * @category API Response Types
197 | */
198 | export type GetOffersResponse = QueryCursorsV2 & {
199 | offers: Offer[];
200 | };
201 |
202 | /**
203 | * Response from OpenSea API for fetching listings.
204 | * @category API Response Types
205 | */
206 | export type GetListingsResponse = QueryCursorsV2 & {
207 | listings: Listing[];
208 | };
209 |
210 | /**
211 | * Response from OpenSea API for fetching a best offer.
212 | * @category API Response Types
213 | */
214 | export type GetBestOfferResponse = Offer | CollectionOffer;
215 |
216 | /**
217 | * Response from OpenSea API for fetching a best listing.
218 | * @category API Response Types
219 | */
220 | export type GetBestListingResponse = Listing;
221 |
222 | /**
223 | * Response from OpenSea API for offchain canceling an order.
224 | * @category API Response Types
225 | */
226 | export type CancelOrderResponse = {
227 | last_signature_issued_valid_until: string | null;
228 | };
229 |
230 | /**
231 | * NFT type returned by OpenSea API.
232 | * @category API Models
233 | */
234 | export type NFT = {
235 | /** NFT Identifier (also commonly referred to as tokenId) */
236 | identifier: string;
237 | /** Slug identifier of collection */
238 | collection: string;
239 | /** Address of contract */
240 | contract: string;
241 | /** Token standard, i.e. ERC721, ERC1155, etc. */
242 | token_standard: string;
243 | /** Name of NFT */
244 | name: string;
245 | /** Description of NFT */
246 | description: string;
247 | /** URL of image */
248 | image_url: string;
249 | /** URL of metadata */
250 | metadata_url: string;
251 | /** URL on OpenSea */
252 | opensea_url: string;
253 | /** Date of latest NFT update */
254 | updated_at: string;
255 | /** Whether NFT is disabled for trading on OpenSea */
256 | is_disabled: boolean;
257 | /** Whether NFT is NSFW (Not Safe For Work) */
258 | is_nsfw: boolean;
259 | /** Traits for the NFT, returns null if the NFT has than 50 traits */
260 | traits: Trait[] | null;
261 | /** Creator of the NFT */
262 | creator: string;
263 | /** Owners of the NFT */
264 | owners: {
265 | address: string;
266 | quantity: number;
267 | }[];
268 | /** Rarity of the NFT */
269 | rarity: null | {
270 | strategy_id: string | null;
271 | strategy_version: string | null;
272 | rank: number | null;
273 | score: number | null;
274 | calculated_at: string;
275 | max_rank: number | null;
276 | tokens_scored: number | null;
277 | ranking_features: null | {
278 | unique_attribute_count: number;
279 | };
280 | };
281 | };
282 |
283 | /**
284 | * Trait type returned by OpenSea API.
285 | * @category API Models
286 | */
287 | export type Trait = {
288 | /** The name of the trait category (e.g. 'Background') */
289 | trait_type: string;
290 | /** A field indicating how to display. None is used for string traits. */
291 | display_type: TraitDisplayType;
292 | /** Ceiling for possible numeric trait values */
293 | max_value: string;
294 | /** The value of the trait (e.g. 'Red') */
295 | value: string | number | Date;
296 | };
297 |
298 | /**
299 | * Trait display type returned by OpenSea API.
300 | * @category API Models
301 | */
302 | export enum TraitDisplayType {
303 | NUMBER = "number",
304 | BOOST_PERCENTAGE = "boost_percentage",
305 | BOOST_NUMBER = "boost_number",
306 | AUTHOR = "author",
307 | DATE = "date",
308 | /** "None" is used for string traits */
309 | NONE = "None",
310 | }
311 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import { FixedNumber } from "ethers";
2 |
3 | export const FIXED_NUMBER_100 = FixedNumber.fromValue(100);
4 | export const INVERSE_BASIS_POINT = 10_000n; // 100 basis points per 1%
5 | export const MAX_EXPIRATION_MONTHS = 1;
6 |
7 | export const API_BASE_MAINNET = "https://api.opensea.io";
8 | export const API_BASE_TESTNET = "https://testnets-api.opensea.io";
9 |
10 | // eslint-disable-next-line import/no-unused-modules
11 | export const SIGNED_ZONE = "0x000056f7000000ece9003ca63978907a00ffd100";
12 |
13 | export const ENGLISH_AUCTION_ZONE_MAINNETS =
14 | "0x110b2b128a9ed1be5ef3232d8e4e41640df5c2cd";
15 | export const ENGLISH_AUCTION_ZONE_TESTNETS =
16 | "0x9B814233894Cd227f561B78Cc65891AA55C62Ad2";
17 |
18 | const SHARED_STOREFRONT_ADDRESS_MAINNET =
19 | "0x495f947276749ce646f68ac8c248420045cb7b5e";
20 | const SHARED_STOREFRONT_ADDRESS_POLYGON =
21 | "0x2953399124f0cbb46d2cbacd8a89cf0599974963";
22 | const SHARED_STOREFRONT_ADDRESS_KLAYTN =
23 | "0x5bc519d852f7ca2c8cf2d095299d5bb2d13f02c9";
24 | export const SHARED_STOREFRONT_ADDRESSES = [
25 | SHARED_STOREFRONT_ADDRESS_MAINNET,
26 | SHARED_STOREFRONT_ADDRESS_POLYGON,
27 | SHARED_STOREFRONT_ADDRESS_KLAYTN,
28 | ].map((address) => address.toLowerCase());
29 | export const SHARED_STOREFRONT_LAZY_MINT_ADAPTER_CROSS_CHAIN_ADDRESS =
30 | "0xa604060890923ff400e8c6f5290461a83aedacec";
31 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { OpenSeaSDK } from "./sdk";
2 |
3 | /**
4 | * @example
5 | * // Example Setup
6 | * ```ts
7 | * import { ethers } from 'ethers'
8 | * import { OpenSeaSDK, Chain } from 'opensea-js'
9 | * const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io')
10 | * const client = new OpenSeaSDK(provider, {
11 | * chain: Chain.Mainnet
12 | * })
13 | * ```
14 | */
15 | export {
16 | // Main SDK export
17 | OpenSeaSDK,
18 | };
19 |
20 | export * from "./types";
21 | export * from "./api/types";
22 | export * from "./orders/types";
23 |
--------------------------------------------------------------------------------
/src/orders/privateListings.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ConsiderationInputItem,
3 | CreateInputItem,
4 | MatchOrdersFulfillment,
5 | Order,
6 | OrderWithCounter,
7 | } from "@opensea/seaport-js/lib/types";
8 | import { isCurrencyItem } from "@opensea/seaport-js/lib/utils/item";
9 | import { generateRandomSalt } from "@opensea/seaport-js/lib/utils/order";
10 |
11 | export const getPrivateListingConsiderations = (
12 | offer: CreateInputItem[],
13 | privateSaleRecipient: string,
14 | ): ConsiderationInputItem[] => {
15 | return offer.map((item) => {
16 | return { ...item, recipient: privateSaleRecipient };
17 | });
18 | };
19 |
20 | export const constructPrivateListingCounterOrder = (
21 | order: OrderWithCounter,
22 | privateSaleRecipient: string,
23 | ): Order => {
24 | // Counter order offers up all the items in the private listing consideration
25 | // besides the items that are going to the private listing recipient
26 | const paymentItems = order.parameters.consideration.filter(
27 | (item) =>
28 | item.recipient.toLowerCase() !== privateSaleRecipient.toLowerCase(),
29 | );
30 |
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");
40 | }
41 |
42 | const { aggregatedStartAmount, aggregatedEndAmount } = paymentItems.reduce(
43 | ({ aggregatedStartAmount, aggregatedEndAmount }, item) => ({
44 | aggregatedStartAmount: aggregatedStartAmount + BigInt(item.startAmount),
45 | aggregatedEndAmount: aggregatedEndAmount + BigInt(item.endAmount),
46 | }),
47 | {
48 | aggregatedStartAmount: 0n,
49 | aggregatedEndAmount: 0n,
50 | },
51 | );
52 |
53 | const counterOrder: Order = {
54 | parameters: {
55 | ...order.parameters,
56 | 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 | ],
66 | // The consideration here is empty as the original private listing order supplies
67 | // the taker address to receive the desired items.
68 | consideration: [],
69 | salt: generateRandomSalt(),
70 | totalOriginalConsiderationItems: 0,
71 | },
72 | signature: "0x",
73 | };
74 |
75 | return counterOrder;
76 | };
77 |
78 | export const getPrivateListingFulfillments = (
79 | privateListingOrder: OrderWithCounter,
80 | ): MatchOrdersFulfillment[] => {
81 | const nftRelatedFulfillments: MatchOrdersFulfillment[] = [];
82 |
83 | // For the original order, we need to match everything offered with every consideration item
84 | // on the original order that's set to go to the private listing recipient
85 | privateListingOrder.parameters.offer.forEach((offerItem, offerIndex) => {
86 | const considerationIndex =
87 | privateListingOrder.parameters.consideration.findIndex(
88 | (considerationItem) =>
89 | considerationItem.itemType === offerItem.itemType &&
90 | considerationItem.token === offerItem.token &&
91 | considerationItem.identifierOrCriteria ===
92 | offerItem.identifierOrCriteria,
93 | );
94 | if (considerationIndex === -1) {
95 | throw new Error(
96 | "Could not find matching offer item in the consideration for private listing",
97 | );
98 | }
99 | nftRelatedFulfillments.push({
100 | offerComponents: [
101 | {
102 | orderIndex: 0,
103 | itemIndex: offerIndex,
104 | },
105 | ],
106 | considerationComponents: [
107 | {
108 | orderIndex: 0,
109 | itemIndex: considerationIndex,
110 | },
111 | ],
112 | });
113 | });
114 |
115 | const currencyRelatedFulfillments: MatchOrdersFulfillment[] = [];
116 |
117 | // For the original order, we need to match everything offered with every consideration item
118 | // on the original order that's set to go to the private listing recipient
119 | privateListingOrder.parameters.consideration.forEach(
120 | (considerationItem, considerationIndex) => {
121 | if (!isCurrencyItem(considerationItem)) {
122 | return;
123 | }
124 | // We always match the offer item (index 0) of the counter order (index 1)
125 | // with all of the payment items on the private listing
126 | currencyRelatedFulfillments.push({
127 | offerComponents: [
128 | {
129 | orderIndex: 1,
130 | itemIndex: 0,
131 | },
132 | ],
133 | considerationComponents: [
134 | {
135 | orderIndex: 0,
136 | itemIndex: considerationIndex,
137 | },
138 | ],
139 | });
140 | },
141 | );
142 |
143 | return [...nftRelatedFulfillments, ...currencyRelatedFulfillments];
144 | };
145 |
--------------------------------------------------------------------------------
/src/orders/types.ts:
--------------------------------------------------------------------------------
1 | import { BasicOrderParametersStruct } from "@opensea/seaport-js/lib/typechain-types/seaport/contracts/Seaport";
2 | import { AdvancedOrder, OrderWithCounter } from "@opensea/seaport-js/lib/types";
3 | import { OpenSeaAccount, OrderSide } from "../types";
4 |
5 | // Protocol data
6 | type OrderProtocolToProtocolData = {
7 | seaport: OrderWithCounter;
8 | };
9 | export type OrderProtocol = keyof OrderProtocolToProtocolData;
10 | export type ProtocolData =
11 | OrderProtocolToProtocolData[keyof OrderProtocolToProtocolData];
12 |
13 | export enum OrderType {
14 | BASIC = "basic",
15 | ENGLISH = "english",
16 | CRITERIA = "criteria",
17 | }
18 |
19 | type OrderFee = {
20 | account: OpenSeaAccount;
21 | basisPoints: string;
22 | };
23 |
24 | /**
25 | * The latest OpenSea Order schema.
26 | */
27 | export type OrderV2 = {
28 | /** The date the order was created. */
29 | createdDate: string;
30 | /** The date the order was closed. */
31 | closingDate: string | null;
32 | /** The date the order was listed. Order can be created before the listing time. */
33 | listingTime: number;
34 | /** The date the order expires. */
35 | expirationTime: number;
36 | /** The hash of the order. */
37 | orderHash: string | null;
38 | /** The account that created the order. */
39 | maker: OpenSeaAccount;
40 | /** The account that filled the order. */
41 | taker: OpenSeaAccount | null;
42 | /** The protocol data for the order. Only 'seaport' is currently supported. */
43 | protocolData: ProtocolData;
44 | /** The contract address of the protocol. */
45 | protocolAddress: string;
46 | /** The current price of the order. */
47 | currentPrice: bigint;
48 | /** The maker fees for the order. */
49 | makerFees: OrderFee[];
50 | /** The taker fees for the order. */
51 | takerFees: OrderFee[];
52 | /** The side of the order. Listing/Offer */
53 | side: OrderSide;
54 | /** The type of the order. Basic/English/Criteria */
55 | orderType: OrderType;
56 | /** Whether or not the maker has cancelled the order. */
57 | cancelled: boolean;
58 | /** Whether or not the order is finalized. */
59 | finalized: boolean;
60 | /** Whether or not the order is marked invalid and therefore not fillable. */
61 | markedInvalid: boolean;
62 | /** The signature the order is signed with. */
63 | clientSignature: string | null;
64 | /** Amount of items left in the order which can be taken. */
65 | remainingQuantity: number;
66 | };
67 |
68 | export type FulfillmentDataResponse = {
69 | protocol: string;
70 | fulfillment_data: FulfillmentData;
71 | };
72 |
73 | type FulfillmentData = {
74 | transaction: Transaction;
75 | orders: ProtocolData[];
76 | };
77 |
78 | type Transaction = {
79 | function: string;
80 | chain: number;
81 | to: string;
82 | value: number;
83 | input_data: {
84 | orders: OrderWithCounter[] | AdvancedOrder[] | BasicOrderParametersStruct[];
85 | };
86 | };
87 |
88 | // API query types
89 | type OpenOrderOrderingOption = "created_date" | "eth_price";
90 | type OrderByDirection = "asc" | "desc";
91 |
92 | export type OrderAPIOptions = {
93 | protocol?: OrderProtocol;
94 | protocolAddress?: string;
95 | side: OrderSide;
96 | };
97 |
98 | export type OrdersQueryOptions = OrderAPIOptions & {
99 | limit?: number;
100 | cursor?: string;
101 | next?: string;
102 |
103 | paymentTokenAddress?: string;
104 | maker?: string;
105 | taker?: string;
106 | owner?: string;
107 | listedAfter?: number | string;
108 | listedBefore?: number | string;
109 | tokenId?: string;
110 | tokenIds?: string[];
111 | assetContractAddress?: string;
112 | orderBy?: OpenOrderOrderingOption;
113 | orderDirection?: OrderByDirection;
114 | onlyEnglish?: boolean;
115 | };
116 |
117 | export type SerializedOrderV2 = {
118 | created_date: string;
119 | closing_date: string | null;
120 | listing_time: number;
121 | expiration_time: number;
122 | order_hash: string | null;
123 | maker: unknown;
124 | taker: unknown | null;
125 | protocol_data: ProtocolData;
126 | protocol_address: string;
127 | current_price: string;
128 | maker_fees: {
129 | account: unknown;
130 | basis_points: string;
131 | }[];
132 | taker_fees: {
133 | account: unknown;
134 | basis_points: string;
135 | }[];
136 | side: OrderSide;
137 | order_type: OrderType;
138 | cancelled: boolean;
139 | finalized: boolean;
140 | marked_invalid: boolean;
141 | client_signature: string | null;
142 | remaining_quantity: number;
143 | };
144 |
145 | export type QueryCursors = {
146 | next: string | null;
147 | previous: string | null;
148 | };
149 |
150 | export type OrdersQueryResponse = QueryCursors & {
151 | orders: SerializedOrderV2[];
152 | };
153 |
154 | export type OrdersPostQueryResponse = { order: SerializedOrderV2 };
155 |
--------------------------------------------------------------------------------
/src/orders/utils.ts:
--------------------------------------------------------------------------------
1 | import { CROSS_CHAIN_SEAPORT_V1_6_ADDRESS } from "@opensea/seaport-js/lib/constants";
2 | import {
3 | OrdersQueryOptions,
4 | OrderV2,
5 | SerializedOrderV2,
6 | ProtocolData,
7 | } from "./types";
8 | import { Chain, OrderSide } from "../types";
9 | import { accountFromJSON } from "../utils";
10 |
11 | export const DEFAULT_SEAPORT_CONTRACT_ADDRESS =
12 | CROSS_CHAIN_SEAPORT_V1_6_ADDRESS;
13 |
14 | export const getPostCollectionOfferPayload = (
15 | collectionSlug: string,
16 | protocol_data: ProtocolData,
17 | traitType?: string,
18 | traitValue?: string,
19 | ) => {
20 | const payload = {
21 | criteria: {
22 | collection: { slug: collectionSlug },
23 | },
24 | protocol_data,
25 | protocol_address: DEFAULT_SEAPORT_CONTRACT_ADDRESS,
26 | };
27 | if (traitType && traitValue) {
28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
29 | (payload.criteria as any).trait = {
30 | type: traitType,
31 | value: traitValue,
32 | };
33 | }
34 | return payload;
35 | };
36 |
37 | export const getBuildCollectionOfferPayload = (
38 | offererAddress: string,
39 | quantity: number,
40 | collectionSlug: string,
41 | offerProtectionEnabled: boolean,
42 | traitType?: string,
43 | traitValue?: string,
44 | ) => {
45 | const payload = {
46 | offerer: offererAddress,
47 | quantity,
48 | criteria: {
49 | collection: {
50 | slug: collectionSlug,
51 | },
52 | },
53 | protocol_address: DEFAULT_SEAPORT_CONTRACT_ADDRESS,
54 | offer_protection_enabled: offerProtectionEnabled,
55 | };
56 | if (traitType && traitValue) {
57 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
58 | (payload.criteria as any).trait = {
59 | type: traitType,
60 | value: traitValue,
61 | };
62 | }
63 | return payload;
64 | };
65 |
66 | export const getFulfillmentDataPath = (side: OrderSide) => {
67 | const sidePath = side === OrderSide.LISTING ? "listings" : "offers";
68 | return `/v2/${sidePath}/fulfillment_data`;
69 | };
70 |
71 | export const getFulfillListingPayload = (
72 | fulfillerAddress: string,
73 | order_hash: string,
74 | protocolAddress: string,
75 | chain: Chain,
76 | ) => {
77 | return {
78 | listing: {
79 | hash: order_hash,
80 | chain,
81 | protocol_address: protocolAddress,
82 | },
83 | fulfiller: {
84 | address: fulfillerAddress,
85 | },
86 | };
87 | };
88 |
89 | export const getFulfillOfferPayload = (
90 | fulfillerAddress: string,
91 | order_hash: string,
92 | protocolAddress: string,
93 | chain: Chain,
94 | ) => {
95 | return {
96 | offer: {
97 | hash: order_hash,
98 | chain,
99 | protocol_address: protocolAddress,
100 | },
101 | fulfiller: {
102 | address: fulfillerAddress,
103 | },
104 | };
105 | };
106 |
107 | type OrdersQueryPathOptions = "protocol" | "side";
108 | export const serializeOrdersQueryOptions = (
109 | options: Omit,
110 | ) => {
111 | return {
112 | limit: options.limit,
113 | cursor: options.cursor,
114 |
115 | payment_token_address: options.paymentTokenAddress,
116 | maker: options.maker,
117 | taker: options.taker,
118 | owner: options.owner,
119 | listed_after: options.listedAfter,
120 | listed_before: options.listedBefore,
121 | token_ids: options.tokenIds ?? [options.tokenId],
122 | asset_contract_address: options.assetContractAddress,
123 | order_by: options.orderBy,
124 | order_direction: options.orderDirection,
125 | only_english: options.onlyEnglish,
126 | };
127 | };
128 |
129 | export const deserializeOrder = (order: SerializedOrderV2): OrderV2 => {
130 | return {
131 | createdDate: order.created_date,
132 | closingDate: order.closing_date,
133 | listingTime: order.listing_time,
134 | expirationTime: order.expiration_time,
135 | orderHash: order.order_hash,
136 | maker: accountFromJSON(order.maker),
137 | taker: order.taker ? accountFromJSON(order.taker) : null,
138 | protocolData: order.protocol_data,
139 | protocolAddress: order.protocol_address,
140 | currentPrice: BigInt(order.current_price),
141 | makerFees: order.maker_fees.map(({ account, basis_points }) => ({
142 | account: accountFromJSON(account),
143 | basisPoints: basis_points,
144 | })),
145 | takerFees: order.taker_fees.map(({ account, basis_points }) => ({
146 | account: accountFromJSON(account),
147 | basisPoints: basis_points,
148 | })),
149 | side: order.side,
150 | orderType: order.order_type,
151 | cancelled: order.cancelled,
152 | finalized: order.finalized,
153 | markedInvalid: order.marked_invalid,
154 | clientSignature: order.client_signature,
155 | remainingQuantity: order.remaining_quantity,
156 | };
157 | };
158 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { BigNumberish } from "ethers";
2 | import type { OrderV2 } from "./orders/types";
3 |
4 | /**
5 | * Events emitted by the SDK which can be used by frontend applications
6 | * to update state or show useful messages to users.
7 | * @category Events
8 | */
9 | export enum EventType {
10 | /**
11 | * Emitted when the transaction is sent to the network and the application
12 | * is waiting for the transaction to be mined.
13 | */
14 | TransactionCreated = "TransactionCreated",
15 | /**
16 | * Emitted when the transaction has succeeded is mined and confirmed.
17 | */
18 | TransactionConfirmed = "TransactionConfirmed",
19 | /**
20 | * Emitted when the transaction has failed to be submitted.
21 | */
22 | TransactionDenied = "TransactionDenied",
23 | /**
24 | * Emitted when the transaction has failed to be mined.
25 | */
26 | TransactionFailed = "TransactionFailed",
27 | /**
28 | * Emitted when the {@link OpenSeaSDK.wrapEth} method is called.
29 | */
30 | WrapEth = "WrapEth",
31 | /**
32 | * Emitted when the {@link OpenSeaSDK.unwrapWeth} method is called.
33 | */
34 | UnwrapWeth = "UnwrapWeth",
35 | /**
36 | * Emitted when fulfilling a public or private order.
37 | */
38 | MatchOrders = "MatchOrders",
39 | /**
40 | * Emitted when the {@link OpenSeaSDK.cancelOrder} method is called.
41 | */
42 | CancelOrder = "CancelOrder",
43 | /**
44 | * Emitted when the {@link OpenSeaSDK.approveOrder} method is called.
45 | */
46 | ApproveOrder = "ApproveOrder",
47 | /**
48 | * Emitted when the {@link OpenSeaSDK.transfer} method is called.
49 | */
50 | Transfer = "Transfer",
51 | }
52 |
53 | /**
54 | * Data that gets sent with each {@link EventType}
55 | * @category Events
56 | */
57 | export interface EventData {
58 | /**
59 | * Wallet address of the user who initiated the event.
60 | */
61 | accountAddress?: string;
62 | /**
63 | * Amount of ETH sent when wrapping or unwrapping.
64 | */
65 | amount?: BigNumberish;
66 | /**
67 | * The transaction hash of the event.
68 | */
69 | transactionHash?: string;
70 | /**
71 | * The {@link EventType} of the event.
72 | */
73 | event?: EventType;
74 | /**
75 | * Error which occurred when transaction was denied or failed.
76 | */
77 | error?: unknown;
78 | /**
79 | * The {@link OrderV2} object.
80 | */
81 | orderV2?: OrderV2;
82 | }
83 |
84 | /**
85 | * OpenSea API configuration object
86 | * @param chain `Chain` to use. Defaults to Ethereum Mainnet (`Chain.Mainnet`)
87 | * @param apiKey API key to use. Not required for testnets
88 | * @param apiBaseUrl Optional base URL to use for the API
89 | */
90 | export interface OpenSeaAPIConfig {
91 | chain?: Chain;
92 | apiKey?: string;
93 | apiBaseUrl?: string;
94 | }
95 |
96 | /**
97 | * Each of the possible chains that OpenSea supports.
98 | * ⚠️NOTE: When adding to this list, also add to the util functions `getChainId` and `getWETHAddress`
99 | */
100 | export enum Chain {
101 | // Mainnet Chains
102 | /** Ethereum */
103 | Mainnet = "ethereum",
104 | /** Polygon */
105 | Polygon = "matic",
106 | /** Klaytn */
107 | Klaytn = "klaytn",
108 | /** Base */
109 | Base = "base",
110 | /** Blast */
111 | Blast = "blast",
112 | /** Arbitrum */
113 | Arbitrum = "arbitrum",
114 | /** Arbitrum Nova */
115 | ArbitrumNova = "arbitrum_nova",
116 | /** Avalanche */
117 | Avalanche = "avalanche",
118 | /** Optimism */
119 | Optimism = "optimism",
120 | /** Solana */
121 | Solana = "solana",
122 | /** Zora */
123 | Zora = "zora",
124 | /** Sei */
125 | Sei = "sei",
126 | /** B3 */
127 | B3 = "b3",
128 | /** Bera Chain */
129 | BeraChain = "bera_chain",
130 | /** ApeChain */
131 | ApeChain = "ape_chain",
132 | /** Flow */
133 | Flow = "flow",
134 | /** Ronin */
135 | Ronin = "ronin",
136 | /** Abstract */
137 | Abstract = "abstract",
138 | // Testnet Chains
139 | // ⚠️NOTE: When adding to this list, also add to the util function `isTestChain`
140 | /** Sepolia */
141 | Sepolia = "sepolia",
142 | /** Polygon Amoy */
143 | Amoy = "amoy",
144 | /** Klaytn Baobab */
145 | Baobab = "baobab",
146 | /** Base Testnet */
147 | BaseSepolia = "base_sepolia",
148 | /** Blast Testnet */
149 | BlastSepolia = "blast_sepolia",
150 | /** Arbitrum Sepolia */
151 | ArbitrumSepolia = "arbitrum_sepolia",
152 | /** Avalanche Fuji */
153 | Fuji = "avalanche_fuji",
154 | /** Optimism Sepolia */
155 | OptimismSepolia = "optimism_sepolia",
156 | /** Solana Devnet */
157 | SolanaDevnet = "soldev",
158 | /** Zora Sepolia */
159 | ZoraSepolia = "zora_sepolia",
160 | /** Sei Testnet */
161 | SeiTestnet = "sei_testnet",
162 | /** B3 Sepolia */
163 | B3Sepolia = "b3_sepolia",
164 | /** Flow Testnet */
165 | FlowTestnet = "flow_testnet",
166 | /** Saigon Testnet */
167 | SaigonTestnet = "saigon_testnet",
168 | /** Abstract Testnet */
169 | AbstractTestnet = "abstract_testnet",
170 | }
171 |
172 | /**
173 | * Order side: listing (ask) or offer (bid)
174 | */
175 | export enum OrderSide {
176 | LISTING = "ask",
177 | OFFER = "bid",
178 | }
179 |
180 | /**
181 | * Token standards
182 | */
183 | export enum TokenStandard {
184 | ERC20 = "ERC20",
185 | ERC721 = "ERC721",
186 | ERC1155 = "ERC1155",
187 | }
188 |
189 | /**
190 | * The collection's approval status within OpenSea.
191 | * Can be one of:
192 | * - not_requested: brand new collections
193 | * - requested: collections that requested safelisting on our site
194 | * - approved: collections that are approved on our site and can be found in search results
195 | * - verified: verified collections
196 | */
197 | export enum SafelistStatus {
198 | NOT_REQUESTED = "not_requested",
199 | REQUESTED = "requested",
200 | APPROVED = "approved",
201 | VERIFIED = "verified",
202 | DISABLED_TOP_TRENDING = "disabled_top_trending",
203 | }
204 |
205 | /**
206 | * Collection fees
207 | * @category API Models
208 | */
209 | export interface Fee {
210 | fee: number;
211 | recipient: string;
212 | required: boolean;
213 | }
214 |
215 | /**
216 | * Generic Blockchain Asset.
217 | * @category API Models
218 | */
219 | export interface Asset {
220 | /** The asset's token ID, or null if ERC-20 */
221 | tokenId: string | null;
222 | /** The asset's contract address */
223 | tokenAddress: string;
224 | /** The token standard (e.g. "ERC721") for this asset */
225 | tokenStandard?: TokenStandard;
226 | /** Optional for ENS names */
227 | name?: string;
228 | /** Optional for fungible items */
229 | decimals?: number;
230 | }
231 |
232 | /**
233 | * Generic Blockchain Asset, with tokenId required.
234 | * @category API Models
235 | */
236 | export interface AssetWithTokenId extends Asset {
237 | /** The asset's token ID */
238 | tokenId: string;
239 | }
240 |
241 | /**
242 | * Generic Blockchain Asset, with tokenStandard required.
243 | * @category API Models
244 | */
245 | export interface AssetWithTokenStandard extends Asset {
246 | /** The token standard (e.g. "ERC721") for this asset */
247 | tokenStandard: TokenStandard;
248 | }
249 |
250 | interface OpenSeaCollectionStatsIntervalData {
251 | interval: "one_day" | "seven_day" | "thirty_day";
252 | volume: number;
253 | volume_diff: number;
254 | volume_change: number;
255 | sales: number;
256 | sales_diff: number;
257 | average_price: number;
258 | }
259 |
260 | /**
261 | * OpenSea Collection Stats
262 | * @category API Models
263 | */
264 | export interface OpenSeaCollectionStats {
265 | total: {
266 | volume: number;
267 | sales: number;
268 | average_price: number;
269 | num_owners: number;
270 | market_cap: number;
271 | floor_price: number;
272 | floor_price_symbol: string;
273 | };
274 | intervals: OpenSeaCollectionStatsIntervalData[];
275 | }
276 |
277 | export interface RarityStrategy {
278 | strategyId: string;
279 | strategyVersion: string;
280 | calculatedAt: string;
281 | maxRank: number;
282 | tokensScored: number;
283 | }
284 |
285 | /**
286 | * OpenSea collection metadata.
287 | * @category API Models
288 | */
289 | export interface OpenSeaCollection {
290 | /** Name of the collection */
291 | name: string;
292 | /** The identifier (slug) of the collection */
293 | collection: string;
294 | /** Description of the collection */
295 | description: string;
296 | /** Image for the collection */
297 | imageUrl: string;
298 | /** Banner image for the collection */
299 | bannerImageUrl: string;
300 | /** Owner address of the collection */
301 | owner: string;
302 | /** The collection's safelist status */
303 | safelistStatus: SafelistStatus;
304 | /** The category of the collection */
305 | category: string;
306 | /** If the collection is disabled */
307 | isDisabled: boolean;
308 | /** If the collection is NSFW (not safe for work) */
309 | isNSFW: boolean;
310 | /** If trait offers are enabled */
311 | traitOffersEnabled: boolean;
312 | /** If collection offers are enabled */
313 | collectionOffersEnabled: boolean;
314 | /** The OpenSea url for the collection */
315 | openseaUrl: string;
316 | /** The project url for the collection */
317 | projectUrl: string;
318 | /** The wiki url for the collection */
319 | wikiUrl: string;
320 | /** The discord url for the collection */
321 | discordUrl: string;
322 | /** The telegram url for the collection */
323 | telegramUrl: string;
324 | /** The twitter username for the collection */
325 | twitterUsername: string;
326 | /** The instagram username for the collection */
327 | instagramUsername: string;
328 | /** The contracts for the collection */
329 | contracts: { address: string; chain: Chain }[];
330 | /** Accounts allowed to edit this collection */
331 | editors: string[];
332 | /** The fees for the collection */
333 | fees: Fee[];
334 | /** The rarity strategy for the collection */
335 | rarity: RarityStrategy | null;
336 | /** Payment tokens allowed for orders for this collection */
337 | paymentTokens: OpenSeaPaymentToken[];
338 | /** The total supply of the collection (minted minus burned) */
339 | totalSupply: number;
340 | /** The created date of the collection */
341 | createdDate: string;
342 | /** When defined, the zone required for orders for the collection */
343 | requiredZone?: string;
344 | }
345 |
346 | /**
347 | * Full annotated Fungible Token spec with OpenSea metadata
348 | */
349 | export interface OpenSeaPaymentToken {
350 | name: string;
351 | symbol: string;
352 | decimals: number;
353 | address: string;
354 | chain: Chain;
355 | imageUrl?: string;
356 | ethPrice?: string;
357 | usdPrice?: string;
358 | }
359 |
360 | /**
361 | * Query interface for payment tokens
362 | * @category API Models
363 | */
364 | export interface OpenSeaPaymentTokensQuery {
365 | symbol?: string;
366 | address?: string;
367 | limit?: number;
368 | next?: string;
369 | }
370 |
371 | /**
372 | * OpenSea Account
373 | * @category API Models
374 | */
375 | export interface OpenSeaAccount {
376 | address: string;
377 | username: string;
378 | profileImageUrl: string;
379 | bannerImageUrl: string;
380 | website: string;
381 | socialMediaAccounts: SocialMediaAccount[];
382 | bio: string;
383 | joinedDate: string;
384 | }
385 | /**
386 | * Social media account
387 | * @category API Models
388 | */
389 | export interface SocialMediaAccount {
390 | platform: string;
391 | username: string;
392 | }
393 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./utils";
2 |
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CROSS_CHAIN_SEAPORT_V1_6_ADDRESS,
3 | ItemType,
4 | } from "@opensea/seaport-js/lib/constants";
5 | import { ethers, FixedNumber } from "ethers";
6 | import {
7 | FIXED_NUMBER_100,
8 | MAX_EXPIRATION_MONTHS,
9 | SHARED_STOREFRONT_ADDRESSES,
10 | SHARED_STOREFRONT_LAZY_MINT_ADAPTER_CROSS_CHAIN_ADDRESS,
11 | } from "../constants";
12 | import {
13 | Chain,
14 | Fee,
15 | OpenSeaAccount,
16 | OpenSeaCollection,
17 | OpenSeaPaymentToken,
18 | RarityStrategy,
19 | TokenStandard,
20 | } from "../types";
21 |
22 | /* eslint-disable @typescript-eslint/no-explicit-any */
23 | export const collectionFromJSON = (collection: any): OpenSeaCollection => {
24 | return {
25 | name: collection.name,
26 | collection: collection.collection,
27 | description: collection.description,
28 | imageUrl: collection.image_url,
29 | bannerImageUrl: collection.banner_image_url,
30 | owner: collection.owner,
31 | safelistStatus: collection.safelist_status,
32 | category: collection.category,
33 | isDisabled: collection.is_disabled,
34 | isNSFW: collection.is_nsfw,
35 | traitOffersEnabled: collection.trait_offers_enabled,
36 | collectionOffersEnabled: collection.collection_offers_enabled,
37 | openseaUrl: collection.opensea_url,
38 | projectUrl: collection.project_url,
39 | wikiUrl: collection.wiki_url,
40 | discordUrl: collection.discord_url,
41 | telegramUrl: collection.telegram_url,
42 | twitterUsername: collection.twitter_username,
43 | instagramUsername: collection.instagram_username,
44 | contracts: (collection.contracts ?? []).map((contract: any) => ({
45 | address: contract.address,
46 | chain: contract.chain,
47 | })),
48 | editors: collection.editors,
49 | fees: (collection.fees ?? []).map(feeFromJSON),
50 | rarity: rarityFromJSON(collection.rarity),
51 | paymentTokens: (collection.payment_tokens ?? []).map(paymentTokenFromJSON),
52 | totalSupply: collection.total_supply,
53 | createdDate: collection.created_date,
54 | requiredZone: collection.required_zone,
55 | };
56 | };
57 |
58 | export const rarityFromJSON = (rarity: any): RarityStrategy | null => {
59 | if (!rarity) {
60 | return null;
61 | }
62 | const fromJSON: RarityStrategy = {
63 | strategyId: rarity.strategy_id,
64 | strategyVersion: rarity.strategy_version,
65 | calculatedAt: rarity.calculated_at,
66 | maxRank: rarity.max_rank,
67 | tokensScored: rarity.tokens_scored,
68 | };
69 | return fromJSON;
70 | };
71 |
72 | export const paymentTokenFromJSON = (token: any): OpenSeaPaymentToken => {
73 | const fromJSON: OpenSeaPaymentToken = {
74 | name: token.name,
75 | symbol: token.symbol,
76 | decimals: token.decimals,
77 | address: token.address,
78 | chain: token.chain,
79 | imageUrl: token.image,
80 | ethPrice: token.eth_price,
81 | usdPrice: token.usd_price,
82 | };
83 | return fromJSON;
84 | };
85 |
86 | export const accountFromJSON = (account: any): OpenSeaAccount => {
87 | return {
88 | address: account.address,
89 | username: account.username,
90 | profileImageUrl: account.profile_image_url,
91 | bannerImageUrl: account.banner_image_url,
92 | website: account.website,
93 | socialMediaAccounts: (account.social_media_accounts ?? []).map(
94 | (acct: any) => ({
95 | platform: acct.platform,
96 | username: acct.username,
97 | }),
98 | ),
99 | bio: account.bio,
100 | joinedDate: account.joined_date,
101 | };
102 | };
103 |
104 | export const feeFromJSON = (fee: any): Fee => {
105 | const fromJSON: Fee = {
106 | fee: fee.fee,
107 | recipient: fee.recipient,
108 | required: fee.required,
109 | };
110 | return fromJSON;
111 | };
112 |
113 | /**
114 | * Estimate gas usage for a transaction.
115 | * @param provider The Provider
116 | * @param from Address sending transaction
117 | * @param to Destination contract address
118 | * @param data Data to send to contract
119 | * @param value Value in ETH to send with data
120 | */
121 | export async function estimateGas(
122 | provider: ethers.Provider,
123 | { from, to, data, value = 0n }: ethers.Transaction,
124 | ) {
125 | return await provider.estimateGas({
126 | from,
127 | to,
128 | value: value.toString(),
129 | data,
130 | });
131 | }
132 |
133 | /**
134 | * The longest time that an order is valid for is one month from the current date
135 | * @returns unix timestamp
136 | */
137 | export const getMaxOrderExpirationTimestamp = () => {
138 | const maxExpirationDate = new Date();
139 |
140 | maxExpirationDate.setMonth(
141 | maxExpirationDate.getMonth() + MAX_EXPIRATION_MONTHS,
142 | );
143 | maxExpirationDate.setDate(maxExpirationDate.getDate() - 1);
144 |
145 | return Math.round(maxExpirationDate.getTime() / 1000);
146 | };
147 |
148 | interface ErrorWithCode extends Error {
149 | code: string;
150 | }
151 |
152 | export const hasErrorCode = (error: unknown): error is ErrorWithCode => {
153 | const untypedError = error as Partial;
154 | return !!untypedError.code;
155 | };
156 |
157 | export const getAssetItemType = (tokenStandard: TokenStandard) => {
158 | switch (tokenStandard) {
159 | case "ERC20":
160 | return ItemType.ERC20;
161 | case "ERC721":
162 | return ItemType.ERC721;
163 | case "ERC1155":
164 | return ItemType.ERC1155;
165 | default:
166 | throw new Error(`Unknown schema name: ${tokenStandard}`);
167 | }
168 | };
169 |
170 | export const getChainId = (chain: Chain) => {
171 | switch (chain) {
172 | case Chain.Mainnet:
173 | return "1";
174 | case Chain.Polygon:
175 | return "137";
176 | case Chain.Amoy:
177 | return "80002";
178 | case Chain.Sepolia:
179 | return "11155111";
180 | case Chain.Klaytn:
181 | return "8217";
182 | case Chain.Baobab:
183 | return "1001";
184 | case Chain.Avalanche:
185 | return "43114";
186 | case Chain.Fuji:
187 | return "43113";
188 | case Chain.Arbitrum:
189 | return "42161";
190 | case Chain.ArbitrumNova:
191 | return "42170";
192 | case Chain.ArbitrumSepolia:
193 | return "421614";
194 | case Chain.Blast:
195 | return "238";
196 | case Chain.BlastSepolia:
197 | return "168587773";
198 | case Chain.Base:
199 | return "8453";
200 | case Chain.BaseSepolia:
201 | return "84532";
202 | case Chain.Optimism:
203 | return "10";
204 | case Chain.OptimismSepolia:
205 | return "11155420";
206 | case Chain.Zora:
207 | return "7777777";
208 | case Chain.ZoraSepolia:
209 | return "999999999";
210 | case Chain.Sei:
211 | return "1329";
212 | case Chain.SeiTestnet:
213 | return "1328";
214 | case Chain.B3:
215 | return "8333";
216 | case Chain.B3Sepolia:
217 | return "1993";
218 | case Chain.BeraChain:
219 | return "80094";
220 | case Chain.Flow:
221 | return "747";
222 | case Chain.FlowTestnet:
223 | return "545";
224 | case Chain.ApeChain:
225 | return "33139";
226 | case Chain.Ronin:
227 | return "2020";
228 | case Chain.SaigonTestnet:
229 | return "2021";
230 | case Chain.Abstract:
231 | return "2741";
232 | case Chain.AbstractTestnet:
233 | return "11124";
234 | default:
235 | throw new Error(`Unknown chainId for ${chain}`);
236 | }
237 | };
238 |
239 | /** This should be the wrapped native asset for the chain. */
240 | export const getWETHAddress = (chain: Chain) => {
241 | switch (chain) {
242 | case Chain.Mainnet:
243 | return "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
244 | case Chain.Polygon:
245 | return "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619";
246 | case Chain.Amoy:
247 | return "0x52eF3d68BaB452a294342DC3e5f464d7f610f72E";
248 | case Chain.Sepolia:
249 | return "0x7b79995e5f793a07bc00c21412e50ecae098e7f9";
250 | case Chain.Klaytn:
251 | return "0xfd844c2fca5e595004b17615f891620d1cb9bbb2";
252 | case Chain.Baobab:
253 | return "0x9330dd6713c8328a8d82b14e3f60a0f0b4cc7bfb";
254 | case Chain.Avalanche:
255 | return "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7";
256 | case Chain.Fuji:
257 | return "0xd00ae08403B9bbb9124bB305C09058E32C39A48c";
258 | case Chain.Arbitrum:
259 | return "0x82af49447d8a07e3bd95bd0d56f35241523fbab1";
260 | case Chain.ArbitrumNova:
261 | return "0x722e8bdd2ce80a4422e880164f2079488e115365";
262 | case Chain.ArbitrumSepolia:
263 | return "0x980b62da83eff3d4576c647993b0c1d7faf17c73";
264 | case Chain.Blast:
265 | return "0x4300000000000000000000000000000000000004";
266 | case Chain.BlastSepolia:
267 | return "0x4200000000000000000000000000000000000023";
268 | // OP Chains have WETH at the same address
269 | case Chain.Base:
270 | case Chain.BaseSepolia:
271 | case Chain.Optimism:
272 | case Chain.OptimismSepolia:
273 | case Chain.Zora:
274 | case Chain.ZoraSepolia:
275 | case Chain.B3:
276 | case Chain.B3Sepolia:
277 | return "0x4200000000000000000000000000000000000006";
278 | case Chain.BeraChain:
279 | return "0x6969696969696969696969696969696969696969";
280 | case Chain.Sei:
281 | return "0xe30fedd158a2e3b13e9badaeabafc5516e95e8c7";
282 | case Chain.SeiTestnet:
283 | return "0x3921ea6cf927be80211bb57f19830700285b0ada";
284 | case Chain.Flow:
285 | return "0xd3bf53dac106a0290b0483ecbc89d40fcc961f3e";
286 | case Chain.FlowTestnet:
287 | return "0x23b1864b73c6E7Cd6D90bDFa3E62B159eBDdbAb3";
288 | case Chain.ApeChain:
289 | return "0x48b62137edfa95a428d35c09e44256a739f6b557";
290 | case Chain.Ronin:
291 | return "0xe514d9deb7966c8be0ca922de8a064264ea6bcd4";
292 | case Chain.SaigonTestnet:
293 | return "0xa959726154953bae111746e265e6d754f48570e6";
294 | case Chain.Abstract:
295 | return "0x3439153eb7af838ad19d56e1571fbd09333c2809";
296 | case Chain.AbstractTestnet:
297 | return "0x9edcde0257f2386ce177c3a7fcdd97787f0d841d";
298 | default:
299 | throw new Error(`Unknown WETH address for ${chain}`);
300 | }
301 | };
302 |
303 | /**
304 | * Checks if the token address is the shared storefront address and if so replaces
305 | * that address with the lazy mint adapter address. Otherwise, returns the input token address
306 | * @param tokenAddress token address
307 | * @returns input token address or lazy mint adapter address
308 | */
309 | export const getAddressAfterRemappingSharedStorefrontAddressToLazyMintAdapterAddress =
310 | (tokenAddress: string): string => {
311 | return SHARED_STOREFRONT_ADDRESSES.includes(tokenAddress.toLowerCase())
312 | ? SHARED_STOREFRONT_LAZY_MINT_ADAPTER_CROSS_CHAIN_ADDRESS
313 | : tokenAddress;
314 | };
315 |
316 | /**
317 | * Sums up the basis points for fees.
318 | * @param fees The fees to sum up
319 | * @returns sum of basis points
320 | */
321 | export const totalBasisPointsForFees = (fees: Fee[]): bigint => {
322 | const feeBasisPoints = fees.map((fee) => basisPointsForFee(fee));
323 | const totalBasisPoints = feeBasisPoints.reduce(
324 | (sum, basisPoints) => basisPoints + sum,
325 | 0n,
326 | );
327 | return totalBasisPoints;
328 | };
329 |
330 | /**
331 | * Converts a fee to its basis points representation.
332 | * @param fee The fee to convert
333 | * @returns the basis points
334 | */
335 | export const basisPointsForFee = (fee: Fee): bigint => {
336 | return BigInt(
337 | FixedNumber.fromString(fee.fee.toString())
338 | .mul(FIXED_NUMBER_100)
339 | .toFormat(0) // format to 0 decimal places to convert to bigint
340 | .toString(),
341 | );
342 | };
343 |
344 | /**
345 | * Checks whether the current chain is a test chain.
346 | * @param chain Chain to check.
347 | * @returns True if the chain is a test chain.
348 | */
349 | export const isTestChain = (chain: Chain): boolean => {
350 | switch (chain) {
351 | case Chain.Sepolia:
352 | case Chain.Amoy:
353 | case Chain.Baobab:
354 | case Chain.BaseSepolia:
355 | case Chain.BlastSepolia:
356 | case Chain.ArbitrumSepolia:
357 | case Chain.Fuji:
358 | case Chain.OptimismSepolia:
359 | case Chain.SolanaDevnet:
360 | case Chain.ZoraSepolia:
361 | case Chain.SeiTestnet:
362 | case Chain.B3Sepolia:
363 | case Chain.FlowTestnet:
364 | case Chain.SaigonTestnet:
365 | case Chain.AbstractTestnet:
366 | return true;
367 | default:
368 | return false;
369 | }
370 | };
371 |
372 | /**
373 | * Returns if a protocol address is valid.
374 | * @param protocolAddress The protocol address
375 | */
376 | export const isValidProtocol = (protocolAddress: string): boolean => {
377 | const checkSumAddress = ethers.getAddress(protocolAddress);
378 | const validProtocolAddresses = [CROSS_CHAIN_SEAPORT_V1_6_ADDRESS].map(
379 | (address) => ethers.getAddress(address),
380 | );
381 | return validProtocolAddresses.includes(checkSumAddress);
382 | };
383 |
384 | /**
385 | * Throws an error if the protocol address is not valid.
386 | * @param protocolAddress The protocol address
387 | */
388 | export const requireValidProtocol = (protocolAddress: string) => {
389 | if (!isValidProtocol(protocolAddress)) {
390 | throw new Error(`Unsupported protocol address: ${protocolAddress}`);
391 | }
392 | };
393 |
394 | /**
395 | * Decodes an encoded string of token IDs into an array of individual token IDs using bigint for precise calculations.
396 | *
397 | * The encoded token IDs can be in the following formats:
398 | * 1. Single numbers: '123' => ['123']
399 | * 2. Comma-separated numbers: '1,2,3,4' => ['1', '2', '3', '4']
400 | * 3. Ranges of numbers: '5:8' => ['5', '6', '7', '8']
401 | * 4. Combinations of single numbers and ranges: '1,3:5,8' => ['1', '3', '4', '5', '8']
402 | * 5. Wildcard '*' (matches all token IDs): '*' => ['*']
403 | *
404 | * @param encodedTokenIds - The encoded string of token IDs to be decoded.
405 | * @returns An array of individual token IDs after decoding the input.
406 | *
407 | * @throws {Error} If the input is not correctly formatted or if bigint operations fail.
408 | *
409 | * @example
410 | * const encoded = '1,3:5,8';
411 | * const decoded = decodeTokenIds(encoded); // Output: ['1', '3', '4', '5', '8']
412 | *
413 | * @example
414 | * const encodedWildcard = '*';
415 | * const decodedWildcard = decodeTokenIds(encodedWildcard); // Output: ['*']
416 | *
417 | * @example
418 | * const emptyEncoded = '';
419 | * const decodedEmpty = decodeTokenIds(emptyEncoded); // Output: []
420 | */
421 | export const decodeTokenIds = (encodedTokenIds: string): string[] => {
422 | if (encodedTokenIds === "*") {
423 | return ["*"];
424 | }
425 |
426 | const validFormatRegex = /^(\d+(:\d+)?)(,\d+(:\d+)?)*$/;
427 |
428 | if (!validFormatRegex.test(encodedTokenIds)) {
429 | throw new Error(
430 | "Invalid input format. Expected a valid comma-separated list of numbers and ranges.",
431 | );
432 | }
433 |
434 | const ranges = encodedTokenIds.split(",");
435 | const tokenIds: string[] = [];
436 |
437 | for (const range of ranges) {
438 | if (range.includes(":")) {
439 | const [startStr, endStr] = range.split(":");
440 | const start = BigInt(startStr);
441 | const end = BigInt(endStr);
442 | const diff = end - start + 1n;
443 |
444 | if (diff <= 0) {
445 | throw new Error(
446 | `Invalid range. End value: ${end} must be greater than or equal to the start value: ${start}.`,
447 | );
448 | }
449 |
450 | for (let i = 0n; i < diff; i += 1n) {
451 | tokenIds.push((start + i).toString());
452 | }
453 | } else {
454 | const tokenId = BigInt(range);
455 | tokenIds.push(tokenId.toString());
456 | }
457 | }
458 |
459 | return tokenIds;
460 | };
461 |
--------------------------------------------------------------------------------
/test/api/api.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { suite, test } from "mocha";
3 | import { Chain } from "../../src";
4 | import { getWETHAddress } from "../../src/utils";
5 | import {
6 | BAYC_CONTRACT_ADDRESS,
7 | mainAPI,
8 | MAINNET_API_KEY,
9 | testnetAPI,
10 | } from "../utils/constants";
11 |
12 | suite("API", () => {
13 | test("API has correct base url", () => {
14 | assert.equal(mainAPI.apiBaseUrl, "https://api.opensea.io");
15 | assert.equal(testnetAPI.apiBaseUrl, "https://testnets-api.opensea.io");
16 | });
17 |
18 | test("Includes API key in request", async () => {
19 | const oldLogger = mainAPI.logger;
20 |
21 | const logPromise = new Promise((resolve, reject) => {
22 | mainAPI.logger = (log) => {
23 | try {
24 | assert.include(log, `"x-api-key":"${MAINNET_API_KEY}"`);
25 | resolve();
26 | } catch (e) {
27 | reject(e);
28 | } finally {
29 | mainAPI.logger = oldLogger;
30 | }
31 | };
32 | const wethAddress = getWETHAddress(Chain.Mainnet);
33 | mainAPI.getPaymentToken(wethAddress);
34 | });
35 |
36 | await logPromise;
37 | });
38 |
39 | test("API handles errors", async () => {
40 | // 404 Not found for random token id
41 | try {
42 | await mainAPI.getNFT(BAYC_CONTRACT_ADDRESS, "404040");
43 | } catch (error) {
44 | assert.include((error as Error).message, "not found");
45 | }
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/test/api/fulfillment.spec.ts:
--------------------------------------------------------------------------------
1 | import "../utils/setup";
2 | import { assert } from "chai";
3 | import { suite, test } from "mocha";
4 | import { OrderSide } from "../../src/types";
5 | import { mainAPI } from "../utils/constants";
6 |
7 | suite("Generating fulfillment data", () => {
8 | test(`Generate fulfillment data for listing`, async () => {
9 | const order = await mainAPI.getOrder({
10 | protocol: "seaport",
11 | side: OrderSide.LISTING,
12 | });
13 |
14 | if (order.orderHash == null) {
15 | return;
16 | }
17 |
18 | const fulfillment = await mainAPI.generateFulfillmentData(
19 | "0x000000000000000000000000000000000000dEaD",
20 | order.orderHash,
21 | order.protocolAddress,
22 | order.side,
23 | );
24 |
25 | assert.exists(fulfillment.fulfillment_data.orders[0].signature);
26 | });
27 |
28 | test(`Generate fulfillment data for offer`, async () => {
29 | const order = await mainAPI.getOrder({
30 | protocol: "seaport",
31 | side: OrderSide.OFFER,
32 | });
33 |
34 | if (order.orderHash == null) {
35 | return;
36 | }
37 |
38 | const fulfillment = await mainAPI.generateFulfillmentData(
39 | "0x000000000000000000000000000000000000dEaD",
40 | order.orderHash,
41 | order.protocolAddress,
42 | order.side,
43 | );
44 |
45 | assert.exists(fulfillment.fulfillment_data.orders[0].signature);
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/test/api/getOrders.spec.ts:
--------------------------------------------------------------------------------
1 | import "../utils/setup";
2 | import { expect } from "chai";
3 | import { suite, test } from "mocha";
4 | import { OrderSide } from "../../src/types";
5 | import {
6 | BAYC_CONTRACT_ADDRESS,
7 | BAYC_TOKEN_IDS,
8 | mainAPI,
9 | } from "../utils/constants";
10 | import { expectValidOrder } from "../utils/utils";
11 |
12 | suite("Getting orders", () => {
13 | [OrderSide.LISTING, OrderSide.OFFER].forEach((side) => {
14 | test(`getOrder should return a single order > ${side}`, async () => {
15 | const order = await mainAPI.getOrder({
16 | protocol: "seaport",
17 | side,
18 | });
19 | expectValidOrder(order);
20 | });
21 | });
22 |
23 | test(`getOrder should throw if no order found`, async () => {
24 | await expect(
25 | mainAPI.getOrder({
26 | protocol: "seaport",
27 | side: OrderSide.LISTING,
28 | maker: "0x000000000000000000000000000000000000dEaD",
29 | }),
30 | )
31 | .to.eventually.be.rejected.and.be.an.instanceOf(Error)
32 | .and.have.property("message", "Not found: no matching order found");
33 | });
34 |
35 | [OrderSide.LISTING, OrderSide.OFFER].forEach((side) => {
36 | test(`getOrders should return a list of orders > ${side}`, async () => {
37 | const { orders, next, previous } = await mainAPI.getOrders({
38 | protocol: "seaport",
39 | side,
40 | tokenIds: BAYC_TOKEN_IDS,
41 | assetContractAddress: BAYC_CONTRACT_ADDRESS,
42 | });
43 | orders.map((order) => expectValidOrder(order));
44 | expect(next).to.not.be.undefined;
45 | expect(previous).to.not.be.undefined;
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/test/api/postOrder.validation.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai";
2 | import { suite, test } from "mocha";
3 | import { OpenSeaAPI } from "../../src/api/api";
4 | import { OrderAPIOptions } from "../../src/orders/types";
5 | import { Chain, OrderSide } from "../../src/types";
6 |
7 | /* eslint-disable @typescript-eslint/no-explicit-any */
8 | suite("API: postOrder validation", () => {
9 | const api = new OpenSeaAPI({ chain: Chain.Mainnet });
10 | const mockOrder: any = {
11 | parameters: {
12 | offerer: "0x1234567890123456789012345678901234567890",
13 | zone: "0x1234567890123456789012345678901234567890",
14 | orderType: 0,
15 | startTime: "1234567890",
16 | endTime: "9876543210",
17 | zoneHash:
18 | "0x0000000000000000000000000000000000000000000000000000000000000000",
19 | salt: "1234567890",
20 | offer: [],
21 | consideration: [],
22 | totalOriginalConsiderationItems: 0,
23 | conduitKey:
24 | "0x0000000000000000000000000000000000000000000000000000000000000000",
25 | },
26 | signature: "0x",
27 | };
28 |
29 | test("should throw error when side is missing", async () => {
30 | const apiOptions = {
31 | protocolAddress: "0x1234567890123456789012345678901234567890",
32 | };
33 |
34 | try {
35 | await api.postOrder(mockOrder, apiOptions as OrderAPIOptions);
36 | expect.fail("Should have thrown an error");
37 | } catch (error: any) {
38 | expect(error.message).to.equal("apiOptions.side is required");
39 | }
40 | });
41 |
42 | test("should throw error when protocolAddress is missing", async () => {
43 | const apiOptions = {
44 | side: OrderSide.LISTING,
45 | };
46 |
47 | try {
48 | await api.postOrder(mockOrder, apiOptions as OrderAPIOptions);
49 | expect.fail("Should have thrown an error");
50 | } catch (error: any) {
51 | expect(error.message).to.equal("apiOptions.protocolAddress is required");
52 | }
53 | });
54 |
55 | test("should throw error when order is missing", async () => {
56 | const apiOptions = {
57 | side: OrderSide.LISTING,
58 | protocolAddress: "0x1234567890123456789012345678901234567890",
59 | } as OrderAPIOptions;
60 |
61 | try {
62 | await api.postOrder(null as any, apiOptions);
63 | expect.fail("Should have thrown an error");
64 | } catch (error: any) {
65 | expect(error.message).to.equal("order data is required");
66 | }
67 | });
68 |
69 | test("should throw error for unsupported protocol", async () => {
70 | const apiOptions = {
71 | protocol: "unsupported" as "seaport",
72 | side: OrderSide.LISTING,
73 | protocolAddress: "0x1234567890123456789012345678901234567890",
74 | };
75 |
76 | try {
77 | await api.postOrder(mockOrder, apiOptions);
78 | expect.fail("Should have thrown an error");
79 | } catch (error: any) {
80 | expect(error.message).to.equal(
81 | "Currently only 'seaport' protocol is supported",
82 | );
83 | }
84 | });
85 |
86 | test("should throw error for invalid side value", async () => {
87 | const apiOptions = {
88 | side: "invalid_side" as OrderSide,
89 | protocolAddress: "0x1234567890123456789012345678901234567890",
90 | };
91 |
92 | try {
93 | await api.postOrder(mockOrder, apiOptions);
94 | expect.fail("Should have thrown an error");
95 | } catch (error: any) {
96 | expect(error.message).to.equal("side must be either 'ask' or 'bid'");
97 | }
98 | });
99 |
100 | test("should throw error for invalid protocol address format", async () => {
101 | const apiOptions = {
102 | side: OrderSide.LISTING,
103 | protocolAddress: "invalid_address",
104 | } as OrderAPIOptions;
105 |
106 | try {
107 | await api.postOrder(mockOrder, apiOptions);
108 | expect.fail("Should have thrown an error");
109 | } catch (error: any) {
110 | expect(error.message).to.equal("Invalid protocol address format");
111 | }
112 | });
113 | });
114 |
--------------------------------------------------------------------------------
/test/integration/README.md:
--------------------------------------------------------------------------------
1 | # Integration Tests
2 |
3 | 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.
4 |
5 | ### Environment variables:
6 |
7 | 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:
8 |
9 | ```bash
10 | OPENSEA_API_KEY="" # OpenSea API Key
11 | ALCHEMY_API_KEY="" # Alchemy API Key for ETH Mainnet
12 | ALCHEMY_API_KEY_POLYGON="" # Alchemy API Key for Polygon
13 | WALLET_PRIV_KEY="0x" # Wallet private key
14 |
15 | # The following needs to be an NFT owned by the wallet address derived from WALLET_PRIV_KEY
16 | ## Mainnet
17 | SELL_ORDER_CONTRACT_ADDRESS="0x"
18 | SELL_ORDER_TOKEN_ID="123"
19 | ## Polygon
20 | SELL_ORDER_CONTRACT_ADDRESS_POLYGON="0x"
21 | SELL_ORDER_TOKEN_ID_POLYGON="123"
22 | ```
23 |
24 | Optional:
25 |
26 | ```bash
27 | OFFER_AMOUNT="0.004" # Defaults to 0.004
28 | LISTING_AMOUNT="40" # Defaults to 40
29 | ```
30 |
31 | #### WETH Tests
32 |
33 | This test requires ETH in your wallet and an amount for the transaction fee. Please note THIS TEST COSTS ETH TO RUN.
34 |
35 | If you would like to run this test, you need to add `ETH_TO_WRAP = "0.001"` to your `.env` file.
36 |
37 | ### How to run:
38 |
39 | ```
40 | npm run test:integration
41 | ```
42 |
--------------------------------------------------------------------------------
/test/integration/getAccount.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { suite, test } from "mocha";
3 | import { sdk } from "./setup";
4 |
5 | suite("SDK: getAccount", () => {
6 | test("Get Account", async () => {
7 | const address = "0xfba662e1a8e91a350702cf3b87d0c2d2fb4ba57f";
8 | const account = await sdk.api.getAccount(address);
9 |
10 | assert(account, "Account should not be null");
11 | assert.equal(account.address, address, "Address should match");
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/test/integration/getCollection.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { suite, test } from "mocha";
3 | import { sdk } from "./setup";
4 | import { CollectionOrderByOption } from "../../src/api/types";
5 | import { SafelistStatus } from "../../src/types";
6 |
7 | suite("SDK: getCollection", () => {
8 | test("Get Verified Collection", async () => {
9 | const slug = "cool-cats-nft";
10 | const collection = await sdk.api.getCollection(slug);
11 |
12 | assert(collection, "Collection should not be null");
13 | assert(collection.name, "Collection name should exist");
14 | assert.equal(collection.collection, slug, "Collection slug should match.");
15 | assert.equal(
16 | collection.safelistStatus,
17 | SafelistStatus.VERIFIED,
18 | "Collection should be verified.",
19 | );
20 | });
21 |
22 | test("Get Collections", async () => {
23 | const response = await sdk.api.getCollections();
24 | const { collections, next } = response;
25 | assert(collections[0], "Collection should not be null");
26 | assert(collections[0].name, "Collection name should exist");
27 | assert(next, "Next cursor should be included");
28 |
29 | const response2 = await sdk.api.getCollections(
30 | CollectionOrderByOption.MARKET_CAP,
31 | );
32 | const { collections: collectionsByMarketCap, next: nextByMarketCap } =
33 | response2;
34 | assert(collectionsByMarketCap[0], "Collection should not be null");
35 | assert(collectionsByMarketCap[0].name, "Collection name should exist");
36 | assert(nextByMarketCap, "Next cursor should be included");
37 |
38 | assert(
39 | collectionsByMarketCap[0].name != collections[0].name,
40 | "Collection order should differ",
41 | );
42 | });
43 |
44 | test("Get Collection Stats", async () => {
45 | const slug = "cool-cats-nft";
46 | const stats = await sdk.api.getCollectionStats(slug);
47 |
48 | assert(stats, "Stats should not be null");
49 | assert(stats.total.volume, "Volume should not be null");
50 | assert(stats.total.sales, "Sales should not be null");
51 | assert(stats.intervals, "Intervals should exist");
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/test/integration/getCollectionOffers.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { suite, test } from "mocha";
3 | import { sdk } from "./setup";
4 | import { decodeTokenIds } from "../../src/utils/utils";
5 |
6 | suite("SDK: getCollectionOffers", () => {
7 | test("Get Collection Offers", async () => {
8 | const slug = "cool-cats-nft";
9 | const response = await sdk.api.getCollectionOffers(slug);
10 |
11 | assert(response, "Response should not be null");
12 | assert(response.offers, "Collection offers should not be null");
13 | assert(response.offers.length > 0, "Collection offers should not be empty");
14 | const offer = response.offers[0];
15 | assert(offer.order_hash, "Order hash should not be null");
16 | const tokens = offer.criteria.encoded_token_ids;
17 | assert(tokens, "Criteria should not be null");
18 |
19 | const encodedTokenIds = offer.criteria.encoded_token_ids;
20 | assert(encodedTokenIds, "Encoded tokens should not be null");
21 |
22 | const decodedTokenIds = decodeTokenIds(encodedTokenIds);
23 | assert(decodedTokenIds[0], "Decoded tokens should not be null");
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/integration/getListingsAndOffers.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { suite, test } from "mocha";
3 | import { sdk } from "./setup";
4 |
5 | suite("SDK: getAllOffers", () => {
6 | test("Get All Offers", async () => {
7 | const slug = "cool-cats-nft";
8 | const response = await sdk.api.getAllOffers(slug);
9 |
10 | assert(response, "Response should not be null");
11 | assert(response.offers[0].order_hash, "Order hash should not be null");
12 | assert(response.offers[0].chain, "Chain should not be null");
13 | assert(
14 | response.offers[0].protocol_address,
15 | "Protocol address should not be null",
16 | );
17 | assert(
18 | response.offers[0].protocol_data,
19 | "Protocol data should not be null",
20 | );
21 | });
22 | });
23 |
24 | suite("SDK: getAllListings", () => {
25 | test("Get All Listings", async () => {
26 | const slug = "cool-cats-nft";
27 | const response = await sdk.api.getAllListings(slug);
28 |
29 | assert(response, "Response should not be null");
30 | assert(response.listings[0].order_hash, "Order hash should not be null");
31 | assert(response.listings[0].chain, "Chain should not be null");
32 | assert(
33 | response.listings[0].protocol_address,
34 | "Protocol address should not be null",
35 | );
36 | assert(
37 | response.listings[0].protocol_data,
38 | "Protocol data should not be null",
39 | );
40 | assert(response.next, "Cursor for next page should not be null");
41 |
42 | // Should get the next page of listings
43 | const responsePage2 = await sdk.api.getAllListings(
44 | slug,
45 | undefined,
46 | response.next,
47 | );
48 | assert(responsePage2, "Response should not be null");
49 | assert.notDeepEqual(
50 | response.listings,
51 | responsePage2.listings,
52 | "Response of second page should not equal the response of first page",
53 | );
54 | assert.notEqual(
55 | response.next,
56 | responsePage2.next,
57 | "Next cursor should change",
58 | );
59 | });
60 | });
61 |
62 | suite("SDK: getBestOffer", () => {
63 | test("Get Best Offer", async () => {
64 | const slug = "cool-cats-nft";
65 | const tokenId = 1;
66 | const response = await sdk.api.getBestOffer(slug, tokenId);
67 |
68 | assert.isString(response.price.currency, "Currency should be a string");
69 | assert.isNumber(response.price.decimals, "Decimals should be a number");
70 | assert.isString(response.price.value, "Price value should be a string");
71 |
72 | assert(response, "Response should not be null");
73 | assert(response.order_hash, "Order hash should not be null");
74 | assert(response.chain, "Chain should not be null");
75 | assert(response.protocol_address, "Protocol address should not be null");
76 | assert(response.protocol_data, "Protocol data should not be null");
77 | });
78 | });
79 |
80 | suite("SDK: getBestListing", () => {
81 | test("Get Best Listing", async () => {
82 | const slug = "cool-cats-nft";
83 | const { listings } = await sdk.api.getAllListings(slug);
84 | const listing = listings[0];
85 | const tokenId =
86 | listing.protocol_data.parameters.offer[0].identifierOrCriteria;
87 | const response = await sdk.api.getBestListing(slug, tokenId);
88 |
89 | assert(response, "Response should not be null");
90 | assert(response.order_hash, "Order hash should not be null");
91 | assert(response.chain, "Chain should not be null");
92 | assert(response.protocol_address, "Protocol address should not be null");
93 | assert(response.protocol_data, "Protocol data should not be null");
94 | assert.equal(
95 | listing.order_hash,
96 | response.order_hash,
97 | "Order hashes should match",
98 | );
99 | assert.equal(
100 | listing.protocol_address,
101 | response.protocol_address,
102 | "Protocol addresses should match",
103 | );
104 | });
105 | });
106 |
107 | suite("SDK: getBestListings", () => {
108 | test("Get Best Listing", async () => {
109 | const slug = "cool-cats-nft";
110 | const response = await sdk.api.getBestListings(slug);
111 |
112 | assert(response, "Response should not be null");
113 | assert(response.listings, "Listings should not be null");
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/test/integration/getNFTs.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert, expect } from "chai";
2 | import { suite, test } from "mocha";
3 | import { sdk } from "./setup";
4 | import { Chain } from "../../src/types";
5 |
6 | suite("SDK: NFTs", () => {
7 | test("Get NFTs By Collection", async () => {
8 | const response = await sdk.api.getNFTsByCollection("proof-moonbirds");
9 | assert(response, "Response should exist.");
10 | assert.equal(response.nfts.length, 50, "Response should include 50 NFTs");
11 | assert(response.next, "Response should have a next cursor");
12 | });
13 |
14 | test("Get NFTs By Contract", async () => {
15 | const tokenAddress = "0x4768cbf202f365fbf704b9b9d397551a0443909b"; // Roo Troop
16 | const response = await sdk.api.getNFTsByContract(
17 | tokenAddress,
18 | undefined,
19 | undefined,
20 | Chain.Polygon,
21 | );
22 | assert(response, "Response should exist.");
23 | assert.equal(response.nfts.length, 50, "Response should include 50 NFTs");
24 | assert(response.next, "Response should have a next cursor");
25 | });
26 |
27 | test("Get NFTs By Account", async () => {
28 | const address = "0xfBa662e1a8e91a350702cF3b87D0C2d2Fb4BA57F";
29 | const response = await sdk.api.getNFTsByAccount(address);
30 | assert(response, "Response should exist.");
31 | assert.equal(response.nfts.length, 50, "Response should include 50 NFTs");
32 | assert(response.next, "Response should have a next cursor");
33 | });
34 |
35 | test("Get NFT", async () => {
36 | const tokenAddress = "0x4768cbf202f365fbf704b9b9d397551a0443909b"; // Roo Troop
37 | const tokenId = "2";
38 | const response = await sdk.api.getNFT(tokenAddress, tokenId, Chain.Polygon);
39 | assert(response.nft, "Response should contain nft.");
40 | assert.equal(response.nft.contract, tokenAddress, "The address matches");
41 | assert.equal(response.nft.identifier, tokenId, "The token id matches");
42 | });
43 |
44 | test("Refresh NFT", async () => {
45 | const tokenAddress = "0x4768cbf202f365fbf704b9b9d397551a0443909b"; // Roo Troop
46 | const identifier = "3";
47 | const response = await sdk.api.refreshNFTMetadata(
48 | tokenAddress,
49 | identifier,
50 | Chain.Polygon,
51 | );
52 | assert(response, "Response should exist.");
53 | expect(response).to.contain(`contract ${tokenAddress}`);
54 | expect(response).to.contain(`token_id ${identifier}`);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/integration/postOrder.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai";
2 | import { suite, test } from "mocha";
3 | import {
4 | LISTING_AMOUNT,
5 | TOKEN_ADDRESS_MAINNET,
6 | TOKEN_ADDRESS_POLYGON,
7 | TOKEN_ID_MAINNET,
8 | TOKEN_ID_POLYGON,
9 | sdk,
10 | sdkPolygon,
11 | walletAddress,
12 | } from "./setup";
13 | import { ENGLISH_AUCTION_ZONE_MAINNETS } from "../../src/constants";
14 | import { getWETHAddress } from "../../src/utils";
15 | import { OFFER_AMOUNT } from "../utils/constants";
16 | import { expectValidOrder } from "../utils/utils";
17 |
18 | const ONE_HOUR = Math.floor(Date.now() / 1000) + 3600;
19 | const expirationTime = ONE_HOUR;
20 |
21 | suite("SDK: order posting", () => {
22 | test("Post Offer - Mainnet", async () => {
23 | const offer = {
24 | accountAddress: walletAddress,
25 | startAmount: +OFFER_AMOUNT,
26 | asset: {
27 | tokenAddress: "0x1a92f7381b9f03921564a437210bb9396471050c",
28 | tokenId: "2288",
29 | },
30 | expirationTime,
31 | };
32 | const order = await sdk.createOffer(offer);
33 | expectValidOrder(order);
34 | expect(order.expirationTime).to.equal(expirationTime);
35 | expect(order.protocolData.parameters.endTime).to.equal(
36 | expirationTime.toString(),
37 | );
38 | });
39 |
40 | test("Post Offer - Polygon", async () => {
41 | const offer = {
42 | accountAddress: walletAddress,
43 | startAmount: +OFFER_AMOUNT,
44 | asset: {
45 | tokenAddress: "0x1a92f7381b9f03921564a437210bb9396471050c",
46 | tokenId: "2288",
47 | },
48 | expirationTime,
49 | };
50 | const order = await sdk.createOffer(offer);
51 | expectValidOrder(order);
52 | });
53 |
54 | test("Post Listing - Mainnet", async function () {
55 | if (!TOKEN_ADDRESS_MAINNET || !TOKEN_ID_MAINNET) {
56 | this.skip();
57 | }
58 | const listing = {
59 | accountAddress: walletAddress,
60 | startAmount: LISTING_AMOUNT,
61 | asset: {
62 | tokenAddress: TOKEN_ADDRESS_MAINNET as string,
63 | tokenId: TOKEN_ID_MAINNET as string,
64 | },
65 | expirationTime,
66 | };
67 | const order = await sdk.createListing(listing);
68 | expectValidOrder(order);
69 | });
70 |
71 | test("Post English Auction Listing - Mainnet", async function () {
72 | if (!TOKEN_ADDRESS_MAINNET || !TOKEN_ID_MAINNET) {
73 | this.skip();
74 | }
75 | const listing = {
76 | accountAddress: walletAddress,
77 | startAmount: LISTING_AMOUNT,
78 | asset: {
79 | tokenAddress: TOKEN_ADDRESS_MAINNET as string,
80 | tokenId: TOKEN_ID_MAINNET as string,
81 | },
82 | englishAuction: true,
83 | expirationTime,
84 | };
85 | try {
86 | const order = await sdk.createListing(listing);
87 | expectValidOrder(order);
88 | expect(order.protocolData.parameters.zone.toLowerCase()).to.equal(
89 | ENGLISH_AUCTION_ZONE_MAINNETS,
90 | );
91 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
92 | } catch (error: any) {
93 | expect(
94 | error.message.includes(
95 | "There is already a live auction for this item. You can only have one auction live at any time.",
96 | ),
97 | );
98 | }
99 | });
100 |
101 | test("Post Listing - Polygon", async function () {
102 | if (!TOKEN_ADDRESS_POLYGON || !TOKEN_ID_POLYGON) {
103 | this.skip();
104 | }
105 | const listing = {
106 | accountAddress: walletAddress,
107 | startAmount: +LISTING_AMOUNT * 1_000_000,
108 | asset: {
109 | tokenAddress: TOKEN_ADDRESS_POLYGON,
110 | tokenId: TOKEN_ID_POLYGON,
111 | },
112 | expirationTime,
113 | };
114 | const order = await sdkPolygon.createListing(listing);
115 | expectValidOrder(order);
116 | });
117 |
118 | test("Post Collection Offer - Mainnet", async () => {
119 | const collection = await sdk.api.getCollection("cool-cats-nft");
120 | const paymentTokenAddress = getWETHAddress(sdk.chain);
121 | const postOrderRequest = {
122 | collectionSlug: collection.collection,
123 | accountAddress: walletAddress,
124 | amount: OFFER_AMOUNT,
125 | quantity: 1,
126 | paymentTokenAddress,
127 | expirationTime,
128 | };
129 | const offerResponse = await sdk.createCollectionOffer(postOrderRequest);
130 | expect(offerResponse).to.exist.and.to.have.property("protocol_address");
131 | expect(offerResponse).to.exist.and.to.have.property("protocol_data");
132 | expect(offerResponse).to.exist.and.to.have.property("order_hash");
133 |
134 | // Cancel the order using self serve API key tied to the offerer
135 | const { protocol_address, order_hash } = offerResponse!;
136 | const cancelResponse = await sdk.offchainCancelOrder(
137 | protocol_address,
138 | order_hash,
139 | );
140 | expect(cancelResponse).to.exist.and.to.have.property(
141 | "last_signature_issued_valid_until",
142 | );
143 | });
144 |
145 | test("Post Collection Offer - Polygon", async () => {
146 | const collection = await sdkPolygon.api.getCollection("arttoken-1155-4");
147 | const paymentTokenAddress = getWETHAddress(sdkPolygon.chain);
148 | const postOrderRequest = {
149 | collectionSlug: collection.collection,
150 | accountAddress: walletAddress,
151 | amount: 0.0001,
152 | quantity: 1,
153 | paymentTokenAddress,
154 | expirationTime,
155 | };
156 | const offerResponse =
157 | await sdkPolygon.createCollectionOffer(postOrderRequest);
158 | expect(offerResponse).to.exist.and.to.have.property("protocol_address");
159 | expect(offerResponse).to.exist.and.to.have.property("protocol_data");
160 | expect(offerResponse).to.exist.and.to.have.property("order_hash");
161 |
162 | // Cancel the order using the offerer signature, deriving it from the ethers signer
163 | const { protocol_address, order_hash } = offerResponse!;
164 | const cancelResponse = await sdkPolygon.offchainCancelOrder(
165 | protocol_address,
166 | order_hash,
167 | undefined,
168 | undefined,
169 | true,
170 | );
171 | expect(cancelResponse).to.exist.and.to.have.property(
172 | "last_signature_issued_valid_until",
173 | );
174 | });
175 |
176 | test("Post Trait Offer - Ethereum", async () => {
177 | const collection = await sdk.api.getCollection("cool-cats-nft");
178 | const paymentTokenAddress = getWETHAddress(sdk.chain);
179 | const postOrderRequest = {
180 | collectionSlug: collection.collection,
181 | accountAddress: walletAddress,
182 | amount: OFFER_AMOUNT,
183 | quantity: 1,
184 | paymentTokenAddress,
185 | traitType: "face",
186 | traitValue: "tvface bobross",
187 | expirationTime,
188 | };
189 | const offerResponse = await sdk.createCollectionOffer(postOrderRequest);
190 | expect(offerResponse).to.exist.and.to.have.property("protocol_data");
191 | expect(offerResponse?.criteria.trait).to.deep.equal({
192 | type: "face",
193 | value: "tvface bobross",
194 | });
195 | });
196 | });
197 |
--------------------------------------------------------------------------------
/test/integration/setup.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { OpenSeaSDK } from "../../src/sdk";
3 | import { Chain } from "../../src/types";
4 | import {
5 | MAINNET_API_KEY,
6 | RPC_PROVIDER_MAINNET,
7 | RPC_PROVIDER_POLYGON,
8 | WALLET_PRIV_KEY,
9 | } from "../utils/constants";
10 |
11 | for (const envVar of ["WALLET_PRIV_KEY"]) {
12 | if (!process.env[envVar]) {
13 | throw new Error(`${envVar} must be set for integration tests`);
14 | }
15 | }
16 |
17 | export const TOKEN_ADDRESS_MAINNET = process.env.SELL_ORDER_CONTRACT_ADDRESS;
18 | export const TOKEN_ID_MAINNET = process.env.SELL_ORDER_TOKEN_ID;
19 | export const TOKEN_ADDRESS_POLYGON =
20 | process.env.SELL_ORDER_CONTRACT_ADDRESS_POLYGON;
21 | export const TOKEN_ID_POLYGON = process.env.SELL_ORDER_TOKEN_ID_POLYGON;
22 | export const LISTING_AMOUNT = process.env.LISTING_AMOUNT ?? "40";
23 | export const ETH_TO_WRAP = process.env.ETH_TO_WRAP;
24 |
25 | const walletMainnet = new ethers.Wallet(
26 | WALLET_PRIV_KEY as string,
27 | RPC_PROVIDER_MAINNET,
28 | );
29 | const walletPolygon = new ethers.Wallet(
30 | WALLET_PRIV_KEY as string,
31 | RPC_PROVIDER_POLYGON,
32 | );
33 | export const walletAddress = walletMainnet.address;
34 |
35 | export const sdk = new OpenSeaSDK(
36 | walletMainnet,
37 | {
38 | chain: Chain.Mainnet,
39 | apiKey: MAINNET_API_KEY,
40 | },
41 | (line) => console.info(`MAINNET: ${line}`),
42 | );
43 |
44 | export const sdkPolygon = new OpenSeaSDK(
45 | walletPolygon,
46 | {
47 | chain: Chain.Polygon,
48 | apiKey: MAINNET_API_KEY,
49 | },
50 | (line) => console.info(`POLYGON: ${line}`),
51 | );
52 |
--------------------------------------------------------------------------------
/test/integration/wrapEth.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { parseEther } from "ethers";
3 | import { describe, test } from "mocha";
4 | import { ETH_TO_WRAP, sdk, walletAddress } from "./setup";
5 | import { TokenStandard } from "../../src/types";
6 | import { getWETHAddress } from "../../src/utils";
7 |
8 | describe("SDK: WETH", () => {
9 | test("Wrap ETH and Unwrap", async function () {
10 | if (!ETH_TO_WRAP) {
11 | this.skip();
12 | }
13 |
14 | const wethAsset = {
15 | tokenAddress: getWETHAddress(sdk.chain),
16 | tokenId: null,
17 | tokenStandard: TokenStandard.ERC20,
18 | };
19 | const startingWethBalance = await sdk.getBalance({
20 | accountAddress: walletAddress,
21 | asset: wethAsset,
22 | });
23 |
24 | await sdk.wrapEth({
25 | amountInEth: ETH_TO_WRAP,
26 | accountAddress: walletAddress,
27 | });
28 |
29 | const endingWethBalance = await sdk.getBalance({
30 | accountAddress: walletAddress,
31 | asset: wethAsset,
32 | });
33 |
34 | const ethToWrapInWei = parseEther(ETH_TO_WRAP);
35 |
36 | assert.equal(
37 | startingWethBalance + ethToWrapInWei,
38 | endingWethBalance,
39 | "Balances should match.",
40 | );
41 |
42 | await sdk.unwrapWeth({
43 | amountInEth: ETH_TO_WRAP,
44 | accountAddress: walletAddress,
45 | });
46 |
47 | const finalWethBalance = await sdk.getBalance({
48 | accountAddress: walletAddress,
49 | asset: wethAsset,
50 | });
51 | assert.equal(
52 | startingWethBalance.toString(),
53 | finalWethBalance.toString(),
54 | "Balances should match.",
55 | );
56 | }).timeout(30000);
57 | });
58 |
--------------------------------------------------------------------------------
/test/sdk/getBalance.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { ethers } from "ethers";
3 | import { suite, test } from "mocha";
4 | import { OpenSeaSDK } from "../../src/index";
5 | import { Chain, TokenStandard } from "../../src/types";
6 | import { MAINNET_API_KEY, RPC_PROVIDER_MAINNET } from "../utils/constants";
7 |
8 | const client = new OpenSeaSDK(
9 | RPC_PROVIDER_MAINNET,
10 | {
11 | chain: Chain.Mainnet,
12 | apiKey: MAINNET_API_KEY,
13 | },
14 | (line) => console.info(`MAINNET: ${line}`),
15 | );
16 |
17 | suite("SDK: getBalance", () => {
18 | const accountAddress = "0x000000000000000000000000000000000000dEaD";
19 |
20 | test("Returns balance for ERC20", async () => {
21 | const asset = {
22 | tokenStandard: TokenStandard.ERC20,
23 | // WETH
24 | tokenAddress: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
25 | tokenId: null,
26 | };
27 | const balance = await client.getBalance({ accountAddress, asset });
28 | assert(balance > ethers.parseEther("0.05"));
29 | });
30 |
31 | test("Returns balance for ERC721", async () => {
32 | const asset = {
33 | tokenStandard: TokenStandard.ERC721,
34 | tokenAddress: "0x0cdd3cb3bcd969c2b389488b51fb093cc0d703b1",
35 | tokenId: "183",
36 | };
37 | const balance = await client.getBalance({ accountAddress, asset });
38 | assert(balance === 1n);
39 | });
40 |
41 | test("Returns balance for ERC1155", async () => {
42 | const asset = {
43 | tokenStandard: TokenStandard.ERC1155,
44 | tokenAddress: "0x1e196b7873b8456437309ba3fa748fa6f1602da8",
45 | tokenId: "21",
46 | };
47 | const balance = await client.getBalance({ accountAddress, asset });
48 | assert(balance >= 2n);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/sdk/misc.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert, expect } from "chai";
2 | import { ethers } from "ethers";
3 | import { suite, test } from "mocha";
4 | import {
5 | SHARED_STOREFRONT_LAZY_MINT_ADAPTER_CROSS_CHAIN_ADDRESS,
6 | SHARED_STOREFRONT_ADDRESSES,
7 | } from "../../src/constants";
8 | import { OpenSeaSDK } from "../../src/index";
9 | import { Chain } from "../../src/types";
10 | import {
11 | decodeTokenIds,
12 | getAddressAfterRemappingSharedStorefrontAddressToLazyMintAdapterAddress,
13 | } from "../../src/utils/utils";
14 | import {
15 | DAPPER_ADDRESS,
16 | MAINNET_API_KEY,
17 | RPC_PROVIDER_MAINNET,
18 | } from "../utils/constants";
19 |
20 | const client = new OpenSeaSDK(
21 | RPC_PROVIDER_MAINNET,
22 | {
23 | chain: Chain.Mainnet,
24 | apiKey: MAINNET_API_KEY,
25 | },
26 | (line) => console.info(`MAINNET: ${line}`),
27 | );
28 |
29 | suite("SDK: misc", () => {
30 | test("Instance has public methods", () => {
31 | assert.equal(typeof client.wrapEth, "function");
32 | });
33 |
34 | test("Instance exposes API methods", () => {
35 | assert.equal(typeof client.api.getOrder, "function");
36 | assert.equal(typeof client.api.getOrders, "function");
37 | });
38 |
39 | test("Checks that a non-shared storefront address is not remapped", async () => {
40 | const address = DAPPER_ADDRESS;
41 | assert.equal(
42 | getAddressAfterRemappingSharedStorefrontAddressToLazyMintAdapterAddress(
43 | address,
44 | ),
45 | address,
46 | );
47 | });
48 |
49 | test("Checks that shared storefront addresses are remapped to lazy mint adapter address", async () => {
50 | for (const address of SHARED_STOREFRONT_ADDRESSES) {
51 | assert.equal(
52 | getAddressAfterRemappingSharedStorefrontAddressToLazyMintAdapterAddress(
53 | address,
54 | ),
55 | SHARED_STOREFRONT_LAZY_MINT_ADAPTER_CROSS_CHAIN_ADDRESS,
56 | );
57 | }
58 | });
59 |
60 | test("Checks that upper case shared storefront addresses are remapped to lazy mint adapter address", async () => {
61 | for (const address of SHARED_STOREFRONT_ADDRESSES) {
62 | assert.equal(
63 | getAddressAfterRemappingSharedStorefrontAddressToLazyMintAdapterAddress(
64 | address.toUpperCase(),
65 | ),
66 | SHARED_STOREFRONT_LAZY_MINT_ADAPTER_CROSS_CHAIN_ADDRESS,
67 | );
68 | }
69 | });
70 |
71 | test("Should throw an error when using methods that need a provider or wallet with the accountAddress", async () => {
72 | const wallet = ethers.Wallet.createRandom();
73 | const accountAddress = wallet.address;
74 | const expectedErrorMessage = `Specified accountAddress is not available through wallet or provider: ${accountAddress}`;
75 |
76 | /* eslint-disable @typescript-eslint/no-explicit-any */
77 | try {
78 | await client.wrapEth({ amountInEth: "0.1", accountAddress });
79 | throw new Error("should have thrown");
80 | } catch (e: any) {
81 | expect(e.message).to.include(expectedErrorMessage);
82 | }
83 |
84 | try {
85 | await client.unwrapWeth({ amountInEth: "0.1", accountAddress });
86 | throw new Error("should have thrown");
87 | } catch (e: any) {
88 | expect(e.message).to.include(expectedErrorMessage);
89 | }
90 |
91 | const asset = {} as any;
92 |
93 | try {
94 | await client.createOffer({ asset, startAmount: 1, accountAddress });
95 | throw new Error("should have thrown");
96 | } catch (e: any) {
97 | expect(e.message).to.include(expectedErrorMessage);
98 | }
99 |
100 | try {
101 | await client.createListing({ asset, startAmount: 1, accountAddress });
102 | throw new Error("should have thrown");
103 | } catch (e: any) {
104 | expect(e.message).to.include(expectedErrorMessage);
105 | }
106 |
107 | try {
108 | await client.createCollectionOffer({
109 | collectionSlug: "",
110 | amount: 1,
111 | quantity: 1,
112 | paymentTokenAddress: "",
113 | accountAddress,
114 | });
115 | throw new Error("should have thrown");
116 | } catch (e: any) {
117 | expect(e.message).to.include(expectedErrorMessage);
118 | }
119 |
120 | const order = {} as any;
121 |
122 | try {
123 | await client.fulfillOrder({ order, accountAddress });
124 | throw new Error("should have thrown");
125 | } catch (e: any) {
126 | expect(e.message).to.include(expectedErrorMessage);
127 | }
128 |
129 | try {
130 | await client.cancelOrder({ order, accountAddress });
131 | throw new Error("should have thrown");
132 | } catch (e: any) {
133 | expect(e.message).to.include(expectedErrorMessage);
134 | }
135 |
136 | try {
137 | await client.approveOrder({
138 | ...order,
139 | maker: { address: accountAddress },
140 | });
141 | throw new Error("should have thrown");
142 | } catch (e: any) {
143 | expect(e.message).to.include(expectedErrorMessage);
144 | }
145 | /* eslint-enable @typescript-eslint/no-explicit-any */
146 | });
147 |
148 | describe("decodeTokenIds", () => {
149 | it('should return ["*"] when given "*" as input', () => {
150 | expect(decodeTokenIds("*")).to.deep.equal(["*"]);
151 | });
152 |
153 | it("should correctly decode a single number", () => {
154 | expect(decodeTokenIds("123")).to.deep.equal(["123"]);
155 | });
156 |
157 | it("should correctly decode multiple comma-separated numbers", () => {
158 | expect(decodeTokenIds("1,2,3,4")).to.deep.equal(["1", "2", "3", "4"]);
159 | });
160 |
161 | it("should correctly decode a single number", () => {
162 | expect(decodeTokenIds("10:10")).to.deep.equal(["10"]);
163 | });
164 |
165 | it("should correctly decode a range of numbers", () => {
166 | expect(decodeTokenIds("5:8")).to.deep.equal(["5", "6", "7", "8"]);
167 | });
168 |
169 | it("should correctly decode multiple ranges of numbers", () => {
170 | expect(decodeTokenIds("1:3,7:9")).to.deep.equal([
171 | "1",
172 | "2",
173 | "3",
174 | "7",
175 | "8",
176 | "9",
177 | ]);
178 | });
179 |
180 | it("should correctly decode a mix of single numbers and ranges", () => {
181 | expect(decodeTokenIds("1,3:5,8")).to.deep.equal([
182 | "1",
183 | "3",
184 | "4",
185 | "5",
186 | "8",
187 | ]);
188 | });
189 |
190 | it("should throw an error for invalid input format", () => {
191 | expect(() => decodeTokenIds("1:3:5,8")).to.throw(
192 | "Invalid input format. Expected a valid comma-separated list of numbers and ranges.",
193 | );
194 | expect(() => decodeTokenIds("1;3:5,8")).to.throw(
195 | "Invalid input format. Expected a valid comma-separated list of numbers and ranges.",
196 | );
197 | });
198 |
199 | it("should throw an error for invalid range format", () => {
200 | expect(() => decodeTokenIds("5:2")).throws(
201 | "Invalid range. End value: 2 must be greater than or equal to the start value: 5.",
202 | );
203 | });
204 |
205 | it("should handle very large input numbers", () => {
206 | const encoded = "10000000000000000000000000:10000000000000000000000002";
207 | expect(decodeTokenIds(encoded)).deep.equal([
208 | "10000000000000000000000000",
209 | "10000000000000000000000001",
210 | "10000000000000000000000002",
211 | ]);
212 | });
213 | });
214 | });
215 |
--------------------------------------------------------------------------------
/test/sdk/orders.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { suite, test } from "mocha";
3 | import { OpenSeaSDK } from "../../src/index";
4 | import { Chain } from "../../src/types";
5 | import { MAINNET_API_KEY, RPC_PROVIDER_MAINNET } from "../utils/constants";
6 |
7 | const client = new OpenSeaSDK(
8 | RPC_PROVIDER_MAINNET,
9 | {
10 | chain: Chain.Mainnet,
11 | apiKey: MAINNET_API_KEY,
12 | },
13 | (line) => console.info(`MAINNET: ${line}`),
14 | );
15 |
16 | suite("SDK: orders", () => {
17 | test("Fungible tokens filter", async () => {
18 | const manaAddress = "0x0f5d2fb29fb7d3cfee444a200298f468908cc942";
19 | const manaPaymentToken = await client.api.getPaymentToken(manaAddress);
20 | assert.isNotNull(manaPaymentToken);
21 | assert.equal(manaPaymentToken.name, "Decentraland MANA");
22 | assert.equal(manaPaymentToken.address, manaAddress);
23 | assert.equal(manaPaymentToken.decimals, 18);
24 |
25 | const daiAddress = "0x6b175474e89094c44da98b954eedeac495271d0f";
26 | const daiPaymentToken = await client.api.getPaymentToken(daiAddress);
27 | assert.isNotNull(daiPaymentToken);
28 | assert.equal(daiPaymentToken.name, "Dai Stablecoin");
29 | assert.equal(daiPaymentToken.decimals, 18);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/test/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CROSS_CHAIN_SEAPORT_V1_5_ADDRESS,
3 | CROSS_CHAIN_SEAPORT_V1_6_ADDRESS,
4 | } from "@opensea/seaport-js/lib/constants";
5 | import { expect } from "chai";
6 | import { ethers } from "ethers";
7 | import { suite, test } from "mocha";
8 | import { isValidProtocol } from "../src/utils/utils";
9 |
10 | suite("Utils: utils", () => {
11 | test("isValidProtocol works with all forms of address", async () => {
12 | const randomAddress = ethers.Wallet.createRandom().address;
13 |
14 | // Mapping of [address, isValid]
15 | const addressesToCheck: [string, boolean][] = [
16 | [CROSS_CHAIN_SEAPORT_V1_6_ADDRESS, true],
17 | [CROSS_CHAIN_SEAPORT_V1_5_ADDRESS, false], // 1.5 is no longer supported
18 | [randomAddress, false],
19 | ];
20 |
21 | // Check default, lowercase, and checksum addresses
22 | const formatsToCheck = (address: string) => [
23 | address,
24 | address.toLowerCase(),
25 | ethers.getAddress(address),
26 | ];
27 |
28 | for (const [address, isValid] of addressesToCheck) {
29 | for (const formattedAddress of formatsToCheck(address)) {
30 | expect(isValidProtocol(formattedAddress)).to.equal(isValid);
31 | }
32 | }
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/test/utils/constants.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { OpenSeaAPI } from "../../src/api";
3 | import { Chain } from "../../src/types";
4 |
5 | export const MAINNET_API_KEY = process.env.OPENSEA_API_KEY;
6 | export const WALLET_PRIV_KEY = process.env.WALLET_PRIV_KEY;
7 |
8 | const ALCHEMY_API_KEY_MAINNET = process.env.ALCHEMY_API_KEY;
9 | const ALCHEMY_API_KEY_POLYGON = process.env.ALCHEMY_API_KEY_POLYGON;
10 |
11 | export const RPC_PROVIDER_MAINNET = new ethers.JsonRpcProvider(
12 | `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY_MAINNET}`,
13 | );
14 | export const RPC_PROVIDER_POLYGON = new ethers.JsonRpcProvider(
15 | `https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY_POLYGON}`,
16 | );
17 |
18 | export const OFFER_AMOUNT = process.env.OFFER_AMOUNT ?? "0.004";
19 | export const DAPPER_ADDRESS = "0x4819352bd7fadcCFAA8A2cDA4b2825a9ec51417c";
20 | export const BAYC_CONTRACT_ADDRESS =
21 | "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d";
22 | export const BAYC_TOKEN_IDS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
23 |
24 | export const mainAPI = new OpenSeaAPI(
25 | {
26 | apiKey: MAINNET_API_KEY,
27 | chain: Chain.Mainnet,
28 | },
29 | process.env.DEBUG ? console.log : undefined,
30 | );
31 |
32 | export const testnetAPI = new OpenSeaAPI(
33 | {
34 | chain: Chain.Sepolia,
35 | },
36 | process.env.DEBUG ? console.log : undefined,
37 | );
38 |
--------------------------------------------------------------------------------
/test/utils/setup.ts:
--------------------------------------------------------------------------------
1 | import chai = require("chai");
2 | import chaiAsPromised = require("chai-as-promised");
3 |
4 | chai.should();
5 | chai.use(chaiAsPromised);
6 |
--------------------------------------------------------------------------------
/test/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai";
2 | import { OrderV2 } from "../../src/orders/types";
3 |
4 | export const expectValidOrder = (order: OrderV2) => {
5 | const requiredFields = [
6 | "createdDate",
7 | "closingDate",
8 | "listingTime",
9 | "expirationTime",
10 | "orderHash",
11 | "maker",
12 | "taker",
13 | "protocolData",
14 | "protocolAddress",
15 | "currentPrice",
16 | "makerFees",
17 | "takerFees",
18 | "side",
19 | "orderType",
20 | "cancelled",
21 | "finalized",
22 | "markedInvalid",
23 | "remainingQuantity",
24 | ];
25 | for (const field of requiredFields) {
26 | expect(field in order).to.be.true;
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["./test"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["./src/**/*", "test"],
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "target": "es2020",
6 | "lib": ["dom", "esnext"],
7 | "skipLibCheck": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noImplicitReturns": true,
10 | "noErrorTruncation": true,
11 | "noImplicitThis": false,
12 | "noUnusedParameters": true,
13 | "preserveConstEnums": true,
14 | "removeComments": false,
15 | "sourceMap": true,
16 | "strict": true,
17 | "declaration": true,
18 | "module": "commonjs",
19 | "moduleResolution": "node",
20 | "resolveJsonModule": true,
21 | "isolatedModules": true,
22 | "baseUrl": "."
23 | }
24 | }
25 |
--------------------------------------------------------------------------------