├── .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 | --------------------------------------------------------------------------------