├── .commitlintrc.js ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── pre-commit └── pre-push ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __test__ ├── authenticator.test.ts ├── bulk-listing.test.ts ├── common │ ├── config.ts │ ├── create-env-test-skd.ts │ ├── off-line-processor.ts │ └── utils.ts ├── create-listing.test.ts ├── create-offer.test.ts ├── executor.test.ts ├── fulfilled-listing.test.ts ├── go-trading.test.ts └── order-fetcher.test.ts ├── commitlint.config.js ├── docs ├── asset │ └── bulk_listing_server.png ├── interfaces │ ├── CollectionListingResponse.md │ ├── CollectionListingsParam.md │ ├── ListingNFTParams.md │ ├── SingleAddressListingsResponse.md │ ├── SingleNftListingResponse.md │ ├── TradeAggregatorParams.md │ └── TradeAggregatorResponse.md ├── model_diagram.jpg ├── process.jpg └── tradeAggregator │ ├── BulkBuy.md │ ├── BulkList.md │ ├── BuyByCollectionListings.md │ ├── BuyByNFTListings.md │ ├── BuyByWalletListings.md │ ├── tradeWithSafeMode.md │ └── tradeWithoutSafeMode.md ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── rollup.config.mjs ├── src ├── abi │ ├── crypto-punk.ts │ ├── erc-1155.ts │ ├── erc-721.ts │ └── index.ts ├── common │ ├── index.ts │ ├── rate-limiter.ts │ └── utils │ │ ├── index.ts │ │ └── sleep.ts ├── exceptions │ ├── aggregator.ts │ ├── base.ts │ ├── index.ts │ └── utils.ts ├── http │ ├── client.ts │ └── index.ts ├── index.ts ├── modules │ ├── aggregator │ │ └── index.ts │ ├── config │ │ └── index.ts │ ├── index.ts │ ├── order-fetcher │ │ └── index.ts │ └── utils │ │ ├── action │ │ ├── executor │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── processor │ │ │ ├── common │ │ │ │ ├── index.ts │ │ │ │ ├── sign-info.ts │ │ │ │ └── sign-order-data.ts │ │ │ └── index.ts │ │ ├── readme.md │ │ └── task │ │ │ ├── controller.ts │ │ │ ├── index.ts │ │ │ ├── pass-through.ts │ │ │ ├── signature.ts │ │ │ ├── template.ts │ │ │ └── transaction.ts │ │ ├── blur-auth.ts │ │ ├── consts.ts │ │ ├── ethereum │ │ └── index.ts │ │ ├── index.ts │ │ ├── internal-utils.ts │ │ ├── is-invalid-param.ts │ │ ├── post-order │ │ ├── handler.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ ├── looks-rare-v2 │ │ │ ├── index.ts │ │ │ └── types.ts │ │ │ ├── looks-rare │ │ │ ├── index.ts │ │ │ └── types.ts │ │ │ ├── seaport-v1.5 │ │ │ ├── addresses.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ │ ├── utils.ts │ │ │ └── x2y2 │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── x2y2-auth.ts └── types │ ├── action │ ├── action.ts │ ├── executor.ts │ ├── index.ts │ ├── processor.ts │ └── task.ts │ ├── aggregator │ ├── aggregator.ts │ ├── cancel.ts │ ├── create.ts │ ├── fulfill.ts │ ├── index.ts │ └── response.ts │ ├── authenticator │ └── index.ts │ ├── config.ts │ ├── go-trading.ts │ ├── http.ts │ ├── index.ts │ ├── json.ts │ ├── order-fetcher.ts │ ├── order.ts │ ├── post-order.ts │ ├── safe-any.ts │ └── utils.ts ├── tea.yaml ├── tsconfig.build.json └── tsconfig.json /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | ['build', 'ci', 'chore', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'], 8 | ], 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY=Your_PRIVATE_KEY 2 | ADDRESS=Your_ADDRESS 3 | API_KEY=Your_API_KEY 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | commitlint.config.js 2 | .eslintcache 3 | *.js 4 | *.mjs 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["./node_modules/gts/", "plugin:jest/recommended"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: buildAndPublishLibrary 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | - '!v*beta*' 8 | 9 | jobs: 10 | publish: 11 | name: buildLibrary 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup Node.js v16.14.0 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 'v16.14.0' 21 | 22 | - name: Install 23 | run: npm install -g pnpm && pnpm install --force 24 | 25 | - name: Build 26 | run: npm run build:rollup 27 | 28 | - name: Publish to NPM 29 | run: | 30 | npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN 31 | npm publish --access public 32 | env: 33 | NPM_TOKEN: ${{ secrets.PUBLISH }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_store 3 | package-lock.json 4 | yarn-error.log 5 | 6 | .env 7 | # zero-installs 8 | .yarn/cache 9 | .yarn/versions 10 | .yarn/install-state.gz 11 | .yarn/unplugged 12 | 13 | # intell 14 | .idea 15 | .idea/**/** 16 | 17 | /coverage 18 | 19 | tsconfig.build.tsbuildinfo 20 | 21 | dist/* 22 | 23 | .env 24 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | ./node_modules/.bin/commitlint --edit $1 5 | npx --no-install commitlint --edit 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | ./node_modules/.bin/lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run test 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *ignore 2 | *.json 3 | *.yml 4 | *.md 5 | .husky/* 6 | .vscode/* 7 | node_modules 8 | yarn.lock 9 | .prettierrc 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | bracketSpacing: true, 4 | tabWidth: 2, 5 | singleQuote: true, 6 | trailingComma: 'es5', 7 | arrowParens: 'avoid', 8 | }; 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.editorconfig", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "drKnoxy.eslint-disable-snippets", 7 | "streetsidesoftware.code-spell-checker", 8 | "codezombiech.gitignore", 9 | "aaron-bond.better-comments", 10 | "jasonnutter.search-node-modules", 11 | "andrejunges.handlebars", 12 | "mikestead.dotenv", 13 | "gruntfuggly.todo-tree", 14 | "vscode-icons-team.vscode-icons", 15 | "OBKoro1.koroFileHeader", 16 | "arcanis.vscode-zipfs" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileheader.customMade": { 3 | "autoAdd": false 4 | }, 5 | "fileheader.cursorMode": { 6 | "description": "", 7 | "param": "", 8 | "return": "" 9 | }, 10 | "files.associations": { 11 | ".code-workspace": "jsonc", 12 | ".babelrc": "json", 13 | ".eslintrc": "jsonc", 14 | ".eslintrc*.json": "jsonc", 15 | ".stylelintrc": "jsonc", 16 | "stylelintrc": "jsonc", 17 | ".htmlhintrc": "jsonc", 18 | "htmlhintrc": "jsonc", 19 | "Procfile*": "shellscript", 20 | "README": "markdown" 21 | }, 22 | "search.useIgnoreFiles": true, 23 | "search.exclude": { 24 | "**/dist": true, 25 | "**/*.log": true, 26 | "**/*.pid": true, 27 | "**/.git": true, 28 | "**/node_modules": true, 29 | "**/bower_components": true, 30 | "**/.yarn": true, 31 | "**/.pnp.*": true 32 | }, 33 | // 34 | "editor.rulers": [ 35 | 80, 36 | 120 37 | ], 38 | "files.eol": "\n", 39 | "files.trimTrailingWhitespace": true, 40 | "files.insertFinalNewline": true, 41 | // 42 | "todo-tree.general.tags": [ 43 | "TODO:", 44 | "FIXME:" 45 | ], 46 | "todo-tree.highlights.defaultHighlight": { 47 | "gutterIcon": true 48 | }, 49 | "todo-tree.highlights.customHighlight": { 50 | "TODO:": { 51 | "foreground": "#fff", 52 | "background": "#ffbd2a", 53 | "iconColour": "#ffbd2a" 54 | }, 55 | "FIXME:": { 56 | "foreground": "#fff", 57 | "background": "#f06292", 58 | "icon": "flame", 59 | "iconColour": "#f06292" 60 | } 61 | }, 62 | // 63 | "cSpell.diagnosticLevel": "Hint", 64 | "[javascript]": { 65 | "editor.defaultFormatter": "esbenp.prettier-vscode" 66 | }, 67 | "[javascriptreact]": { 68 | "editor.defaultFormatter": "esbenp.prettier-vscode" 69 | }, 70 | "[typescript]": { 71 | "editor.defaultFormatter": "esbenp.prettier-vscode" 72 | }, 73 | "[typescriptreact]": { 74 | "editor.defaultFormatter": "esbenp.prettier-vscode" 75 | }, 76 | "[vue]": { 77 | "editor.defaultFormatter": "esbenp.prettier-vscode" 78 | }, 79 | "eslint.alwaysShowStatus": true, 80 | "eslint.run": "onType", 81 | "eslint.probe": [ 82 | "javascript", 83 | "javascriptreact", 84 | "typescript", 85 | "typescriptreact", 86 | "vue" 87 | ], 88 | "eslint.format.enable": true, 89 | "eslint.lintTask.enable": true, 90 | "javascript.validate.enable": false, 91 | "typescript.validate.enable": true, 92 | "css.validate": false, 93 | "scss.validate": false, 94 | "less.validate": false, 95 | "[css]": { 96 | "editor.defaultFormatter": "esbenp.prettier-vscode", 97 | "editor.formatOnType": true, 98 | "editor.formatOnPaste": true, 99 | "editor.formatOnSave": true 100 | }, 101 | "[scss]": { 102 | "editor.defaultFormatter": "esbenp.prettier-vscode", 103 | "editor.formatOnType": true, 104 | "editor.formatOnPaste": true, 105 | "editor.formatOnSave": true 106 | }, 107 | "[less]": { 108 | "editor.defaultFormatter": "esbenp.prettier-vscode", 109 | "editor.formatOnType": true, 110 | "editor.formatOnPaste": true, 111 | "editor.formatOnSave": true 112 | }, 113 | "editor.codeActionsOnSave": { 114 | "source.fixAll.eslint": true 115 | }, 116 | "editor.defaultFormatter": "EditorConfig.EditorConfig", 117 | "javascript.format.enable": false, 118 | "typescript.format.enable": false, 119 | // 120 | "json.format.enable": false, 121 | "[json]": { 122 | "editor.defaultFormatter": "esbenp.prettier-vscode", 123 | "editor.tabSize": 2, 124 | "editor.formatOnType": true, 125 | "editor.formatOnPaste": true, 126 | "editor.formatOnSave": true 127 | }, 128 | "[jsonc]": { 129 | "editor.defaultFormatter": "esbenp.prettier-vscode", 130 | "editor.tabSize": 2, 131 | "editor.formatOnType": true, 132 | "editor.formatOnPaste": true, 133 | "editor.formatOnSave": true 134 | }, 135 | "emmet.triggerExpansionOnTab": true, 136 | "typescript.tsdk": "node_modules/typescript/lib", 137 | "explorer.fileNesting.enabled": true, 138 | "explorer.fileNesting.expand": false, 139 | "explorer.fileNesting.patterns": { 140 | "*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map", 141 | "package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, crowdin*, cypress.*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.config.*, webpack*, workspace.json, xo.config.*, yarn*, .babelrc*, .drone.yml*, .gitignore*, dockerfile*, next.*, next-i18*" 142 | }, 143 | "typescript.enablePromptUseWorkspaceTsdk": true 144 | } 145 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NFTGo/GoTrading/523086ea2106784928e86ee5952c9786df2247f4/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, NFTGo 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoTrading-js 2 | [![node](https://img.shields.io/badge/node-%3E%3D%2016.14-brightgreen.svg)](https://nodejs.org/en/) [![Discord][discord-image]][discord-url] [![Twitter][twitter-image]][twitter-url] 3 | 4 | # Table of Contents 5 | - [GoTrading-js](#gotrading-js) 6 | - [Table of Contents](#table-of-contents) 7 | - [Introduction](#introduction) 8 | - [Key Features](#key-features) 9 | - [Supported Marketplaces](#supported-marketplaces) 10 | - [Supported Chains](#supported-chains) 11 | - [Requirements](#requirements) 12 | - [Quickstart](#quickstart) 13 | - [1. Install the SDK.](#1-install-the-sdk) 14 | - [2. Import and init the GoTrading SDK.](#2-import-and-init-the-gotrading-sdk) 15 | - [3. Fulfill Listings](#3-fulfill-listings) 16 | - [SDK Core Methods](#sdk-core-methods) 17 | - [Marketplace](#marketplace) 18 | - [Create Listings](#create-listings) 19 | - [Create Offers](#create-offers) 20 | - [Fulfill Listings](#fulfill-listings) 21 | - [Fulfill Offers](#fulfill-offers) 22 | - [Cancel Orders](#cancel-orders) 23 | - [OrderFetcher](#orderfetcher) 24 | - [Get Orders By Contract](#get-orders-by-contract) 25 | - [Get Orders By NFT](#get-orders-by-nft) 26 | - [Get Orders By Ids](#get-orders-by-ids) 27 | - [Get Orders By Maker](#get-orders-by-maker) 28 | - [Utils](#utils) 29 | - [Questions \& Feedback](#questions--feedback) 30 | - [License](#license) 31 | 32 | ## Introduction 33 | 34 | 35 | 36 | GoTrading is an open-source development kit that enables you to build your own NFT trading aggregator and marketplace. The SDK provides a comprehensive set of tools and APIs that greatly simplify the development process of a general trading aggregator like Gem.xyz or Blur.io, and allows developers to access real-time order feed and NFT transaction data. With the SDK, you can easily aggregate orders and functionality from mainstream marketplaces such as Opensea, Looksrare, Blur, x2y2, Sudoswap, etc, all in your products and communities. 37 | 38 | ## Key Features 39 | - Simple and easy-to-use API 40 | - Real-time market data access 41 | - Code is easy to customize and extend 42 | - Supports Bulk Listing and Bulk Buying 43 | 44 | ## Supported Marketplaces 45 | GoTrading currently aggregates the following marketplaces, and we will continue to add more marketplaces in the future. 46 | 47 | | **Marketplace** | **Create Listings** | **Fulfill Listings** | **Create Offers** | **Fulfill Offers** | **Cancel Listings/Offers** | **Protocol** | 48 | | --------------- | ------------------- | -------------------- | ----------------- | ------------------ | -------------------------- | --------------------------------- | 49 | | OpenSea | Yes | Yes | Yes | Yes | Yes | seaport-v1.5 | 50 | | Blur | Yes | Yes | Yes | Yes | Yes | blur | 51 | | LooksRare | Yes | Yes | Yes | Yes | Yes | looksrare-v2 | 52 | | X2Y2 | Yes | Yes | Yes | Yes | Yes | x2y2 | 53 | | Sudoswap | No | Yes | No | Yes | No | sudoswap/sudoswap-v2 | 54 | | CryptoPunks | No | Yes | No | Yes | Yes | cryptopunks | 55 | | Artblocks | No | Yes | No | Yes | Yes | seaport-v1.5 | 56 | | Reservoir | No | Yes | No | Yes | Yes | seaport-v1.5 | 57 | | ENSVision | No | Yes | No | Yes | Yes | seaport-v1.5 | 58 | | Magically | No | Yes | No | Yes | Yes | seaport-v1.5 | 59 | | Alienswap | No | Yes | No | Yes | Yes | seaport-v1.5 | 60 | | Ordinals | No | Yes | No | Yes | Yes | seaport-v1.5 | 61 | | Sound | No | Yes | No | Yes | Yes | seaport-v1.5 | 62 | | NFTGO | Yes | Yes | Yes | Yes | Yes | seaport-v1.5/payment-processor-v2 | 63 | 64 | 65 | 66 | ## Supported Chains 67 | GoTrading currently supports the following chains: 68 | - Ethereum Mainnet 69 | - Sepolia Testnet ( Comming Soon ) 70 | - Polygon Mainnet ( Comming Soon ) 71 | 72 | 73 | 74 | ## Requirements 75 | - Node.js >= 16.14 76 | - web3 >= 1.8.2 77 | - ethers >= 5.6.9, < 6.1 78 | 79 | You can do this by running the following commands: 80 | ```bash 81 | npm install web3 ethers@5.6.9 82 | ``` 83 | 84 | ## Quickstart 85 | ### 1. Install the SDK. 86 | 87 | > With `npm` : 88 | ```bash 89 | npm install @nftgo/gotrading 90 | ``` 91 | 92 | > With `yarn` : 93 | ```bash 94 | yarn add @nftgo/gotrading 95 | ``` 96 | 97 | ### 2. Import and init the GoTrading SDK. 98 | Instantiate the instance of GoTrading using your etheres provider with API key. 99 | ```ts 100 | import { init, Config } from '@nftgo/gotrading'; 101 | import Web3 from 'web3'; 102 | 103 | // Create a new Web3 Provider to interact with the Ethereum network. 104 | const provider = new Web3.providers.HttpProvider('https://mainnet.infura.io') //Replace with your own provider 105 | 106 | // Configure the necessary parameters for the Trade Aggregator API client. 107 | const configs: Config = { 108 | apiKey: "YOUR-API-KEY", // Replace with your own API Key. 109 | web3Provider: provider, 110 | walletConfig: { 111 | address: "Your wallet address", 112 | privateKey: "Your private key" 113 | }, // Replace with your wallet info. 114 | }; 115 | 116 | // Create a Trade Aggregator client instance and return the utility and aggregator objects of the Trade Aggregator API. 117 | const {aggregator, utils, orderFetcher} = init(configs); 118 | ``` 119 | > If you need to obtain an API key or a custom plan, please contact our support team. You can reach us by submitting a [form](https://forms.monday.com/forms/7fd30cd3cef08cf3b3dbccd1c72892b5), and we will respond to you within 1-2 business days. 120 | > 121 | > Please note that we may need to understand your use case and requirements in order to provide you with the API key and custom plan that best suits your needs. Thank you for your interest in our service, and we look forward to working with you. 122 | ### 3. Fulfill Listings 123 | ```ts 124 | 125 | import { init, Config, FulfillListingsReq } from '@nftgo/gotrading'; 126 | 127 | async function demo() { 128 | const config: Config = {}; 129 | 130 | const { aggregator, utils } = init(config); 131 | 132 | const {listingDTOs} = await orderFetcher.getOrdersByContract({ 133 | contractAddress: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', // bayc contract address 134 | orderType: OrderType.Listing 135 | }) 136 | const req: FulfillListingsReq = { 137 | buyer: 'xxx', // your address 138 | orderIds: listingDTOs.map(listingDTO => listingDTO.orderId), 139 | safeMode: false, 140 | }; 141 | 142 | // get actions, meanwhile we provide executeActions function to deal with actions 143 | const { actions, executeActions } = await aggregator.fulfillListings(req); 144 | 145 | 146 | // case 1 147 | // RECOMMEND: use execute functions we provide 148 | await executeActions({ 149 | onTaskExecuted(task) { 150 | // do something with completed task info 151 | console.log(task.action.name, task.status); 152 | }, 153 | }); 154 | console.log('success'); 155 | 156 | // case 2 157 | // execute actions by yourself 158 | const executor = utils.createActionExecutor(actions); 159 | for (const task of executor) { 160 | await task.execute(); 161 | console.log(task.action.name, task.status); 162 | } 163 | console.log('success'); 164 | } 165 | ``` 166 | 167 | ## SDK Core Methods 168 | To use the GoTrading SDK, you need to initialize the SDK with your API key. After initialization, you can use the following methods to interact with the GoTrading API. 169 | 170 | ```ts 171 | import { init, Config } from '@nftgo/gotrading'; 172 | 173 | const config: Config = { 174 | apiKey: 'YOUR-API-KEY', 175 | openSeaApiKeyConfig: { 176 | apiKey: 'YOUR-OPENSEA-API-KEY', 177 | requestsPerInterval: 10, // 10 requests per interval 178 | interval: 1000, // 1 second 179 | }, 180 | looksRareApiKeyConfig: { 181 | apiKey: 'YOUR-LOOKSRARE-API-KEY', 182 | requestsPerInterval: 10, // 10 requests per interval 183 | interval: 1000, // 1 second 184 | }, 185 | x2y2ApiKeyConfig: { 186 | apiKey: 'YOUR-X2Y2-API-KEY', 187 | requestsPerInterval: 10, // 10 requests per interval 188 | interval: 1000, // 1 second 189 | }, 190 | walletConfig: { 191 | address: 'Your wallet address', 192 | privateKey: 'Your private key', 193 | }, // Replace with your wallet info. 194 | }; 195 | 196 | const goTrading = init(config); 197 | ``` 198 | 199 | ### Marketplace 200 | The Marketplace methods are used to create and fulfill listings and offers across all marketplaces. 201 | 202 | #### Create Listings 203 | ```ts 204 | import { CreateListingsReq, Orderbook, OrderKind } from '@nftgo/gotrading'; 205 | 206 | const req: CreateListingsReq = { 207 | maker: 'xxx', // your address 208 | params: [ 209 | { 210 | token: '0x97a20815a061eae224c4fdf3109731f73743db73:2', 211 | quantity: 1, 212 | weiPrice: '1000', 213 | orderKind: OrderKind.SeaportV15, 214 | orderbook: Orderbook.Opensea, 215 | listingTime: '1688625367', 216 | expirationTime: '1689858225', 217 | currency: '0x0000000000000000000000000000000000000000', 218 | automatedRoyalties: true, 219 | }, 220 | { 221 | token: '0x97a20815a061eae224c4fdf3109731f73743db73:2', 222 | quantity: 1, 223 | weiPrice: '1000', 224 | orderKind: OrderKind.X2Y2, 225 | orderbook: Orderbook.X2Y2, 226 | listingTime: '1688625367', 227 | expirationTime: '1689858225', 228 | currency: '0x0000000000000000000000000000000000000000', 229 | }, 230 | { 231 | token: '0x97a20815a061eae224c4fdf3109731f73743db73:2', 232 | quantity: 1, 233 | weiPrice: '1000', 234 | orderKind: OrderKind.LooksRareV2, 235 | orderbook: Orderbook.LooksRare, 236 | listingTime: '1688625367', 237 | expirationTime: '1689858225', 238 | currency: '0x0000000000000000000000000000000000000000', 239 | }, 240 | { 241 | token: '0x61628d84d0871a38f102d5f16f4e69ee91d6cdd9:7248', 242 | quantity: 1, 243 | weiPrice: '1000', 244 | orderKind: OrderKind.SeaportV15, 245 | orderbook: Orderbook.Opensea, 246 | listingTime: '1688625367', 247 | expirationTime: '1689858225', 248 | currency: '0x0000000000000000000000000000000000000000', 249 | automatedRoyalties: true, 250 | }, 251 | ], 252 | }; 253 | 254 | const response = await goTrading.aggregator.createListings(req); 255 | 256 | await response.executeActions({ 257 | onTaskExecuted(task) { 258 | console.log(task.action.name, task.status); 259 | }, 260 | }); 261 | 262 | 263 | ``` 264 | #### Create Offers 265 | ```ts 266 | import { CreateOffersReq, Orderbook, OrderKind } from '@nftgo/gotrading'; 267 | 268 | const req: CreateOffersReq = { 269 | maker: 'xxx', // your address 270 | params: [ 271 | { 272 | collection: '0x8d04a8c79ceb0889bdd12acdf3fa9d207ed3ff63', 273 | weiPrice: '10000000000', 274 | orderKind: OrderKind.SeaportV15, 275 | orderbook: Orderbook.Opensea, 276 | listingTime: '1689017272', 277 | expirationTime: '1688017272', 278 | quantity: 2, 279 | }, 280 | { 281 | collection: '0x8d04a8c79ceb0889bdd12acdf3fa9d207ed3ff63', 282 | weiPrice: '10000000000', 283 | orderKind: OrderKind.LooksRareV2, 284 | orderbook: Orderbook.Looksrare, 285 | listingTime: '1689017272', 286 | expirationTime: '1688017272', 287 | quantity: 1, 288 | } 289 | ], 290 | }; 291 | 292 | const response = await goTrading.aggregator.createOffers(req); 293 | 294 | await response.executeActions({ 295 | onTaskExecuted(task) { 296 | console.log(task.action.name, task.status); 297 | }, 298 | }); 299 | 300 | ``` 301 | 302 | #### Fulfill Listings 303 | ```ts 304 | import { FulfillListingsReq, Orderbook, OrderKind } from '@nftgo/gotrading'; 305 | 306 | const orderIds = ['xxx', 'yyy']; // pass the listing ids you want to fulfill 307 | 308 | const req: FulfillListingsReq = { 309 | buyer: 'xxx', // your address 310 | orderIds, 311 | }; 312 | 313 | const response = await goTrading.aggregator.fulfillListings(req); 314 | 315 | await response.executeActions({ 316 | onTaskExecuted(task) { 317 | console.log(task.action.name, task.status); 318 | }, 319 | }); 320 | 321 | ``` 322 | 323 | #### Fulfill Offers 324 | ```ts 325 | import { FulfillOffersReq, Orderbook, OrderKind } from '@nftgo/gotrading'; 326 | 327 | const orderIds = ['xxx', 'yyy']; // pass the offer ids you want to fulfill 328 | 329 | const req: FulfillOffersReq = { 330 | sellerAddress: 'xxx', // your address 331 | offerFulfillmentIntentions: [ 332 | { 333 | orderId: orderIds[0], 334 | contractAddress: "0x02d66f9d220553d831b239f00b5841280ddcfaf3", 335 | tokenId: "1", 336 | quantity: 1, 337 | }, 338 | { 339 | orderId: orderIds[1], 340 | contractAddress: "0x02d66f9d220553d831b239f00b5841280ddcfaf3", 341 | tokenId: "2", 342 | quantity: 1, 343 | }, 344 | ], 345 | }; 346 | 347 | const response = await goTrading.aggregator.fulfillOffers(req); 348 | 349 | await response.executeActions({ 350 | onTaskExecuted(task) { 351 | console.log(task.action.name, task.status); 352 | }, 353 | }); 354 | 355 | ``` 356 | 357 | #### Cancel Orders 358 | ```ts 359 | import { CancelOrdersReq, Orderbook, OrderKind } from '@nftgo/gotrading'; 360 | 361 | 362 | const cancelOrdersReq: CancelOrdersReq = { 363 | callerAddress: 'xxx', // your address 364 | orders: [ 365 | { 366 | orderId: 'aaa', 367 | orderType: OrderType.Listing, 368 | }, 369 | { 370 | orderId: 'bbb', 371 | orderType: OrderType.Offer, 372 | }, 373 | ], 374 | }; 375 | 376 | const response = await goTrading.aggregator.cancelOrders(cancelOrdersReq); 377 | 378 | await response.executeActions({ 379 | onTaskExecuted(task) { 380 | console.log(task.action.name, task.status); 381 | }, 382 | }); 383 | 384 | ``` 385 | 386 | ### OrderFetcher 387 | 388 | #### Get Orders By Contract 389 | ```ts 390 | import { OrderType, GetOrdersByContractReq } from '@nftgo/gotrading'; 391 | 392 | // Get listings by contractAddress 393 | const getOrdersByContractReq: GetOrdersByContractReq = { 394 | contractAddress: '0x97a20815a061eae224c4fdf3109731f73743db73', 395 | orderType: OrderType.Listing, 396 | }; 397 | 398 | const { listingDTOs } = await goTrading.orderFetcher.getOrdersByContract(getOrdersByContractReq); 399 | 400 | // Get offers by contractAddress 401 | const getOffersByContractReq: GetOrdersByContractReq = { 402 | contractAddress: '0x97a20815a061eae224c4fdf3109731f73743db73', 403 | orderType: OrderType.Offer, 404 | }; 405 | 406 | const { offerDTOs } = await goTrading.orderFetcher.getOrdersByContract(getOrdersByContractReq); 407 | 408 | 409 | ``` 410 | 411 | #### Get Orders By NFT 412 | ```ts 413 | import { OrderType, GetOrdersByNftsReq } from '@nftgo/gotrading'; 414 | 415 | // Get listings by nft 416 | const getOrdersByNftsReq: GetOrdersByNftsReq = { 417 | contractAddress: '0x8d04a8c79ceb0889bdd12acdf3fa9d207ed3ff63', 418 | tokenId: '1', 419 | orderType: OrderType.Listing, 420 | }; 421 | 422 | const { listingDTOs } = await goTrading.orderFetcher.getOrdersByNFT(getOrdersByNftsReq); 423 | 424 | // Get offers by nft 425 | const getOffersByNftsReq: GetOrdersByNftsReq = { 426 | contractAddress: '0x8d04a8c79ceb0889bdd12acdf3fa9d207ed3ff63', 427 | tokenId: '1', 428 | orderType: OrderType.Offer, 429 | }; 430 | 431 | const { offerDTOs } = await goTrading.orderFetcher.getOrdersByNFT(getOffersByNftsReq); 432 | 433 | ``` 434 | 435 | #### Get Orders By Ids 436 | ```ts 437 | import { OrderType, GetOrdersByIdsReq } from '@nftgo/gotrading'; 438 | 439 | const getOrdersByIdsReq: GetOrdersByIdsReq = { 440 | orders: [ 441 | { 442 | orderId: 'xxx', 443 | orderType: OrderType.Listing, 444 | }, 445 | { 446 | orderId: 'yyy', 447 | orderType: OrderType.Offer, 448 | }, 449 | ], 450 | }; 451 | 452 | const { listingDTOs, offerDTOs } = await goTrading.orderFetcher.getOrdersByIds(getOrdersByIdsReq); 453 | 454 | ``` 455 | 456 | #### Get Orders By Maker 457 | ```ts 458 | import { OrderType, GetOrdersByMakerReq } from '@nftgo/gotrading'; 459 | 460 | // Get listings by maker 461 | const getOrdersByMakerReq: GetOrdersByMakerReq = { 462 | maker: 'xxx', // your address 463 | orderType: OrderType.Listing, 464 | }; 465 | 466 | const { listingDTOs } = await goTrading.orderFetcher.getOrdersByMaker(getOrdersByMakerReq); 467 | 468 | // Get offers by maker 469 | const getOffersByMakerReq: GetOrdersByMakerReq = { 470 | maker: 'xxx', // your address 471 | orderType: OrderType.Offer, 472 | }; 473 | 474 | const { offerDTOs } = await goTrading.orderFetcher.getOrdersByMaker(getOffersByMakerReq); 475 | 476 | ``` 477 | 478 | ### Utils 479 | 480 | ## Questions & Feedback 481 | 482 | > If you have any questions, issues, or feedback, please file an issue on GitHub, or drop us a message on our [Discord][discord-url] channel for the SDK. 483 | 484 | ## License 485 | 486 | This project is licensed under the [BSD-3-Clause license](LICENSE). 487 | 488 | 489 | [discord-image]: https://img.shields.io/discord/1040195071401787413?color=brightgreen&label=Discord&logo=discord&logoColor=blue 490 | [discord-url]: https://discord.gg/nftgo 491 | [twitter-image]: https://img.shields.io/twitter/follow/NFTGo?label=NFTGo&style=social 492 | [twitter-url]: https://twitter.com/NFTGoDevs 493 | -------------------------------------------------------------------------------- /__test__/authenticator.test.ts: -------------------------------------------------------------------------------- 1 | import { createEnvTestSdk } from './common/create-env-test-skd'; 2 | 3 | describe('Test Authenticator', () => { 4 | const { sdk, address } = createEnvTestSdk(); 5 | 6 | it('Test Blur Authenticator', async () => { 7 | const blurAuth = await sdk.utils.blurAuthenticator.authorize({ address }); 8 | expect(typeof blurAuth).toBe('string'); 9 | }); 10 | 11 | it('Test X2Y2 Authenticator', async () => { 12 | const response = await sdk.utils.x2y2Authenticator.authorize({ address }); 13 | 14 | expect(response).toHaveProperty('message'); 15 | expect(typeof response.message).toBe('string'); 16 | expect(response).toHaveProperty('signature'); 17 | expect(typeof response.signature).toHaveProperty('signature'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__test__/common/config.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | import { EVMChain, Config } from '@/types'; 4 | import Web3 from 'web3'; 5 | 6 | const DefaultProviderUrl = 'https://cloudflare-eth.com/'; 7 | const HTTP_PROXY = 'http://127.0.0.1:9999'; 8 | // const HTTP_PROXY = 'http://127.0.0.1:7890'; 9 | const openseaApi = { 10 | apiKey: process.env.OPENSEA_API_KEY || '', 11 | requestsPerInterval: 2, 12 | interval: 1000, 13 | }; 14 | const looksrareApi = { 15 | apiKey: process.env.LOOKSRARE_API_KEY || '', 16 | requestsPerInterval: 2, 17 | interval: 1000, 18 | }; 19 | const x2y2Api = { 20 | apiKey: process.env.X2Y2_API_KEY || '', 21 | requestsPerInterval: 2, 22 | interval: 1000, 23 | }; 24 | 25 | export function initConfig() { 26 | const provider = initWeb3Provider(); 27 | const config: Config = { 28 | apiKey: process.env.API_KEY || '', // Replace with your own API Key. 29 | baseUrl: process.env.BASE_URL || '', 30 | chain: EVMChain.ETHEREUM, 31 | web3Provider: provider, 32 | walletConfig: { 33 | address: process.env.ADDRESS || '', 34 | privateKey: process.env.PRIVATE_KEY || '', 35 | }, 36 | openSeaApiKeyConfig: openseaApi, 37 | looksRareApiKeyConfig: looksrareApi, 38 | x2y2ApiKeyConfig: x2y2Api, 39 | }; 40 | return config; 41 | } 42 | 43 | export function initWeb3Provider(providerUrl = DefaultProviderUrl) { 44 | const web3Provider = new Web3.providers.HttpProvider(providerUrl); 45 | return web3Provider; 46 | } 47 | 48 | export function checkProviderIsValid(provider: any) { 49 | const web3 = new Web3(provider); 50 | web3.eth.getBlockNumber().then(result => { 51 | console.log('Latest Ethereum Block is ', result); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /__test__/common/create-env-test-skd.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import { init } from '../../src'; 3 | 4 | export function createEnvTestSdk() { 5 | const endpoint = process.env.PROVIDER_URL!, 6 | address = process.env.ADDRESS!, 7 | privateKey = process.env.PRIVATE_KEY!, 8 | apiKey = process.env.API_KEY!, 9 | web3Provider = new Web3.providers.HttpProvider(endpoint); 10 | 11 | const sdk = init({ 12 | apiKey, 13 | baseUrl: process.env.BASE_URL, 14 | web3Provider, 15 | walletConfig: { 16 | address, 17 | privateKey, 18 | }, 19 | }); 20 | 21 | return { 22 | sdk, 23 | address, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /__test__/common/off-line-processor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionKind, 3 | ActionName, 4 | ActionProcessor, 5 | AggregatorAction, 6 | PassThroughActionInfo, 7 | ProcessPassThroughActionParams, 8 | SignatureActionInfo, 9 | TransactionActionInfo, 10 | } from '../../src'; 11 | 12 | export class OffLineProcessor implements ActionProcessor { 13 | async processSignatureAction(action: AggregatorAction) { 14 | const { name, data } = action; 15 | if (name === 'order-signature') { 16 | const { sign } = data; 17 | if (!sign) { 18 | throw new Error('sign is required'); 19 | } 20 | const signature = 'signature' + data.orderIndexes.join(','); 21 | return signature; 22 | } 23 | return Promise.reject(new Error('no match action name')); 24 | } 25 | 26 | async processTransactionAction(action: AggregatorAction) { 27 | const { name, data } = action; 28 | const { txData, safeMode } = data; 29 | if (!txData) { 30 | throw new Error('txData is required'); 31 | } 32 | } 33 | 34 | async processPassThroughAction( 35 | action: AggregatorAction, 36 | params: ProcessPassThroughActionParams 37 | ) { 38 | const { name, data } = action; 39 | console.log(data.orderIndexes, params.signature); 40 | } 41 | 42 | async processControllerAction( 43 | action: AggregatorAction 44 | ): Promise[]> { 45 | return []; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /__test__/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { OrderKind, Orderbook } from '@/types'; 2 | 3 | const BigNumber = require('bignumber.js'); 4 | 5 | export function getCurrentTimeStamp(offset: number) { 6 | const now = new Date(); 7 | const cal = now.getTime() + offset; 8 | const timestampInSeconds = Math.floor(cal / 1000); 9 | return timestampInSeconds; 10 | } 11 | 12 | export function getWeiPrice(tokenPrice: number) { 13 | const ethAmount = new BigNumber(tokenPrice); 14 | const weiAmount = ethAmount.times(new BigNumber('10').pow(18)); 15 | return weiAmount.toString(); 16 | } 17 | 18 | export function getOpenSeaOrder(order: any) { 19 | return { 20 | token: order.contract + ':' + order.tokenId, 21 | weiPrice: getWeiPrice(order.ethPrice), 22 | listingTime: getCurrentTimeStamp(0).toString(), 23 | expirationTime: getCurrentTimeStamp(3600000).toString(), 24 | options: { 25 | 'seaport-v1.5': { 26 | useOffChainCancellation: false, 27 | }, 28 | }, 29 | orderbook: Orderbook.Opensea, 30 | orderKind: OrderKind.SeaportV15, 31 | }; 32 | } 33 | 34 | export function getLooksRareOrder(order: any) { 35 | return { 36 | token: order.contract + ':' + order.tokenId, 37 | weiPrice: getWeiPrice(order.ethPrice), 38 | listingTime: getCurrentTimeStamp(0).toString(), 39 | expirationTime: getCurrentTimeStamp(3600000).toString(), 40 | options: {}, 41 | orderbook: Orderbook.LooksRare, 42 | orderKind: OrderKind.LooksRareV2, 43 | }; 44 | } 45 | 46 | export function getX2Y2Order(order: any) { 47 | return { 48 | token: order.contract + ':' + order.tokenId, 49 | weiPrice: getWeiPrice(order.ethPrice), 50 | listingTime: getCurrentTimeStamp(0).toString(), 51 | expirationTime: getCurrentTimeStamp(3600000).toString(), 52 | options: {}, 53 | orderbook: Orderbook.X2Y2, 54 | orderKind: OrderKind.X2Y2, 55 | }; 56 | } 57 | 58 | export function getBlurOrder(order: any) { 59 | return { 60 | token: order.contract + ':' + order.tokenId, 61 | weiPrice: getWeiPrice(order.ethPrice), 62 | listingTime: getCurrentTimeStamp(0).toString(), 63 | expirationTime: getCurrentTimeStamp(3600000).toString(), 64 | options: {}, 65 | orderbook: Orderbook.Blur, 66 | orderKind: OrderKind.Blur, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /__test__/create-listing.test.ts: -------------------------------------------------------------------------------- 1 | import { initConfig } from './common/config'; 2 | import { Aggregator } from '../src/modules/aggregator'; 3 | import { createUtils } from '../src/modules/utils'; 4 | import { HTTPClientStable } from '../src/http'; 5 | import { getX2Y2Order, getOpenSeaOrder, getBlurOrder, getLooksRareOrder } from './common/utils'; 6 | import { setGlobalDispatcher, ProxyAgent } from 'undici'; 7 | import { BlurMarketAuthenticator } from '../src/modules/utils/blur-auth'; 8 | 9 | const HTTP_PROXY = 'http://127.0.0.1:9999'; 10 | 11 | const proxyAgent = new ProxyAgent(HTTP_PROXY); 12 | setGlobalDispatcher(proxyAgent); 13 | 14 | const config = initConfig(); 15 | const utils = createUtils(config); 16 | const httpClient = new HTTPClientStable(); 17 | 18 | const aggregator = new Aggregator(httpClient, config, utils); 19 | 20 | const mock721Order = { 21 | contract: '0xee467844905022d2a6cc1da7a0b555608faae751', 22 | tokenId: '5745', 23 | ethPrice: 1.45, 24 | }; 25 | const mock721Order2 = { 26 | contract: '0xbe9371326f91345777b04394448c23e2bfeaa826', 27 | tokenId: '19516', 28 | ethPrice: 1.45, 29 | }; 30 | 31 | const orders = [ 32 | // getX2Y2Order(mock721Order), 33 | // getBlurOrder(mock721Order), 34 | getOpenSeaOrder(mock721Order), 35 | // getOpenSeaOrder(mock721Order2), 36 | // getLooksRareOrder(mock721Order), 37 | // getLooksRareOrder(mock721Order2), 38 | ]; 39 | 40 | const blurOrders = [getBlurOrder(mock721Order)]; 41 | 42 | // describe('[create listing] interface result test', () => { 43 | // let blurAuthToken = ''; 44 | // const address = config.walletConfig?.address || ''; 45 | // beforeAll(async () => { 46 | // console.log('getting blur auth token...'); 47 | // const authenticator = new BlurMarketAuthenticator(utils, httpClient, config); 48 | // blurAuthToken = await authenticator.authorize(address); 49 | // }, 20000); 50 | // test('[blur order] blur order response', async () => { 51 | // const maker = address; 52 | // console.info('blurAuthToken:', blurAuthToken); 53 | // const res = await aggregator.createListings({ 54 | // maker, 55 | // blurAuth: blurAuthToken, 56 | // params: blurOrders, 57 | // }); 58 | // const { actions, executeActions } = res; 59 | // expect(executeActions).toEqual(expect.any(Function)); 60 | // expect(actions).toEqual(expect.any(Array)); 61 | // }); 62 | 63 | // test('[empty blur token] listing should return error when blur token is empty', async () => { 64 | // const maker = address; 65 | // const func = async () => { 66 | // await aggregator.createListings({ 67 | // maker, 68 | // params: [getBlurOrder(mock721Order)], 69 | // }); 70 | // }; 71 | // await expect(func()).rejects.toThrow(); 72 | // }); 73 | 74 | // test('[incorrect maker] listing should return error when maker is incorrect', async () => { 75 | // const maker = '0x3e24914f74Cd66e3ee7d1F066A880A6c69404E13'; 76 | // const func = async () => { 77 | // await aggregator.createListings({ 78 | // maker, 79 | // params: orders, 80 | // }); 81 | // }; 82 | // await expect(func()).rejects.toThrow(); 83 | // }); 84 | // }); 85 | 86 | describe('[create listing] execute actions test', () => { 87 | test('should return create listing actions', async () => { 88 | const maker = config.walletConfig?.address || ''; 89 | const res = await aggregator.createListings({ 90 | maker, 91 | params: orders, 92 | }); 93 | const { executeActions } = res; 94 | await executeActions({ 95 | onTaskExecuted: task => { 96 | console.info(task); 97 | }, 98 | }); 99 | expect(executeActions).toEqual(expect.any(Function)); 100 | // await executeAllActions(); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /__test__/create-offer.test.ts: -------------------------------------------------------------------------------- 1 | import { OrderKind, Orderbook, init } from '../src'; 2 | import Web3 from 'web3'; 3 | 4 | describe('create offer main process', () => { 5 | const endpoint = process.env.PROVIDER_URL!, 6 | address = process.env.ADDRESS!, 7 | privateKey = process.env.PRIVATE_KEY!, 8 | apiKey = process.env.API_KEY!, 9 | baseUrl = process.env.BASE_URL!, 10 | web3Provider = new Web3.providers.HttpProvider(endpoint); 11 | 12 | const sdk = init({ 13 | apiKey, 14 | web3Provider, 15 | baseUrl, 16 | walletConfig: { 17 | address, 18 | privateKey, 19 | }, 20 | }); 21 | 22 | test('should return buy actions', async () => { 23 | const res = await sdk.aggregator.createOffers({ 24 | maker: process.env.ADDRESS as string, // your address 25 | params: [ 26 | { 27 | collection: '0xed5af388653567af2f388e6224dc7c4b3241c544', 28 | quantity: 1, 29 | weiPrice: '10000000000000000', 30 | orderKind: OrderKind.SeaportV15, 31 | orderbook: Orderbook.Opensea, 32 | listingTime: (Date.now() / 1000).toFixed(0), 33 | expirationTime: (Date.now() / 1000 + 3 * 60 * 60).toFixed(0), 34 | automatedRoyalties: true, 35 | currency: 'WETH', 36 | }, 37 | ], 38 | }); 39 | 40 | const { actions, executeActions } = res; 41 | console.log(actions); 42 | await executeActions({ 43 | onTaskExecuted: task => { 44 | console.log(task.action, task.result); 45 | }, 46 | }); 47 | expect(Array.isArray(actions)).toBe(true); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__test__/executor.test.ts: -------------------------------------------------------------------------------- 1 | import { BrowserActionTaskExecutor } from '../src/modules/utils/action'; 2 | import { ActionKind, ActionProcessor, AggregatorAction } from '../src'; 3 | 4 | describe('Test executor', () => { 5 | it('test controller task', async () => { 6 | const allActions = [ 7 | { 8 | kind: ActionKind.Transaction, 9 | }, 10 | { 11 | kind: ActionKind.Controller, 12 | }, 13 | { 14 | kind: ActionKind.Signature, 15 | }, 16 | { 17 | kind: ActionKind.PassThrough, 18 | }, 19 | ]; 20 | const actions = allActions.slice(0, 2) as unknown as AggregatorAction[]; 21 | 22 | const processor: ActionProcessor = { 23 | async processControllerAction() { 24 | console.log('get next action'); 25 | return allActions.slice(-2); 26 | }, 27 | async processSignatureAction() { 28 | console.log('signature success'); 29 | return 'signature'; 30 | }, 31 | 32 | async processTransactionAction() { 33 | console.log('transaction success'); 34 | }, 35 | 36 | async processPassThroughAction() { 37 | console.log('post order success'); 38 | }, 39 | } as unknown as ActionProcessor; 40 | 41 | const executor = new BrowserActionTaskExecutor(actions, processor); 42 | 43 | let index = 0; 44 | 45 | for (const task of executor) { 46 | await task.execute(); 47 | expect(task.action.kind).toBe(allActions[index].kind); 48 | index++; 49 | } 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__test__/fulfilled-listing.test.ts: -------------------------------------------------------------------------------- 1 | import { initConfig } from './common/config'; 2 | import { Aggregator } from '../src/modules/aggregator'; 3 | import { createUtils } from '../src/modules/utils'; 4 | import { HTTPClientStable } from '../src/http'; 5 | 6 | import { setGlobalDispatcher, ProxyAgent } from 'undici'; 7 | 8 | const HTTP_PROXY = 'http://127.0.0.1:9999'; 9 | 10 | const proxyAgent = new ProxyAgent(HTTP_PROXY); 11 | setGlobalDispatcher(proxyAgent); 12 | 13 | const config = initConfig(); 14 | 15 | const httpClient = new HTTPClientStable(); 16 | const utils = createUtils(config, httpClient); 17 | 18 | const aggregator = new Aggregator(httpClient, config, utils); 19 | 20 | const orders = [ 21 | { 22 | contractAddress: '0xe2e73bc9952142886424631709e382f6bc169e18', 23 | maker: '0xec9e512fe7e90134d8ca7295329ccb0a57c91ecb', 24 | orderId: '64a79a9f1bd6088508d7b331', 25 | tokenId: '2963', 26 | }, 27 | { 28 | contractAddress: '0xe2e73bc9952142886424631709e382f6bc169e18', 29 | maker: '0xd33843650b6e71503f306c177d283c92c002741d', 30 | orderId: '64a717c11bd6088508a6ffe4', 31 | tokenId: '3363', 32 | }, 33 | ]; 34 | describe('fulfill listing main process', () => { 35 | test('should return buy actions', async () => { 36 | const buyer = config.walletConfig?.address || ''; 37 | const res = await aggregator.fulfillListings({ 38 | buyer: buyer, 39 | orderIds: orders.map(order => order.orderId), 40 | safeMode: false, 41 | }); 42 | const { executeActions } = res; 43 | await executeActions({ 44 | onTaskExecuted: task => { 45 | console.info(task); 46 | }, 47 | }); 48 | expect(executeActions).toEqual(expect.any(Function)); 49 | }); 50 | // test('should return safe mode buy actions', async () => { 51 | // const buyer = config.walletConfig?.address || ''; 52 | // const res = await aggregator.fulfillListings({ 53 | // buyer: buyer, 54 | // orderIds: orders.map(order => order.orderId), 55 | // safeMode: true, 56 | // }); 57 | // const { executeActions } = res; 58 | // await executeActions({ 59 | // onTaskExecuted: task => { 60 | // console.info(task); 61 | // }, 62 | // }); 63 | // expect(executeActions).toEqual(expect.any(Function)); 64 | // }); 65 | }); 66 | -------------------------------------------------------------------------------- /__test__/go-trading.test.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | import { OrderType } from '@/types'; 4 | import { init } from '../src'; 5 | import Web3 from 'web3'; 6 | 7 | describe('Test go-trading sdk', () => { 8 | const endpoint = process.env.PROVIDER_URL!, 9 | address = process.env.ADDRESS!, 10 | privateKey = process.env.PRIVATE_KEY!, 11 | apiKey = process.env.API_KEY!, 12 | web3Provider = new Web3.providers.HttpProvider(endpoint); 13 | 14 | const contractAddress = '0xED5AF388653567Af2F388E6224dC7C4b3241C544'; 15 | 16 | const sdk = init({ 17 | apiKey, 18 | baseUrl: process.env.BASE_URL, 19 | web3Provider, 20 | walletConfig: { 21 | address, 22 | privateKey, 23 | }, 24 | }); 25 | 26 | it('fetch Azuki Listing NFT', async () => { 27 | const orders = await sdk.orderFetcher.getOrdersByContract({ 28 | contractAddress, 29 | limit: 1, 30 | offset: 0, 31 | orderType: OrderType.Listing, 32 | }); 33 | expect(orders.listingDTOs.length).toBe(1); 34 | }); 35 | 36 | // it("make azuki's blur bid", async () => { 37 | // //const blurAuth = await sdk.utils. 38 | // const { actions, executeActions } = await sdk.aggregator.createOffers({ 39 | // blurAuth: 'TODO: ', 40 | // maker: address, 41 | // params: [], 42 | // source: 'nftgo.io', 43 | // }); 44 | // console.log({ 45 | // actions, 46 | // }); 47 | // expect(1).toBe(1); 48 | // }); 49 | }); 50 | -------------------------------------------------------------------------------- /__test__/order-fetcher.test.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | import { OrderType } from '@/types'; 4 | import { init } from '../src'; 5 | import Web3 from 'web3'; 6 | 7 | describe('Test order fetcher sdk', () => { 8 | const endpoint = process.env.PROVIDER_URL!, 9 | // address = process.env.ADDRESS!, 10 | // privateKey = process.env.PRIVATE_KEY!, 11 | apiKey = process.env.API_KEY!, 12 | web3Provider = new Web3.providers.HttpProvider(endpoint); 13 | 14 | const contractAddress = '0xED5AF388653567Af2F388E6224dC7C4b3241C544'; 15 | 16 | const sdk = init({ 17 | apiKey, 18 | baseUrl: process.env.BASE_URL, 19 | web3Provider, 20 | // agent: 21 | // walletConfig: { 22 | // address, 23 | // privateKey, 24 | // }, 25 | }); 26 | 27 | it('fetch Azuki Listing NFT', async () => { 28 | const orders = await sdk.orderFetcher.getOrdersByContract({ 29 | contractAddress, 30 | limit: 1, 31 | offset: 0, 32 | orderType: OrderType.Listing, 33 | }); 34 | expect(orders.listingDTOs.length).toBe(1); 35 | expect(typeof orders.listingDTOs?.[0].orderHash).toBe('string'); 36 | }); 37 | 38 | it('fetch orders by ids', async () => { 39 | const orders = await sdk.orderFetcher.getOrdersByIds({ 40 | orders: [ 41 | { 42 | orderId: '650d79013a36395019514b9d', 43 | orderType: OrderType.Listing, 44 | }, 45 | ], 46 | }); 47 | expect(orders.listingDTOs.length).toBe(1); 48 | expect(typeof orders.listingDTOs?.[0].orderHash).toBe('string'); 49 | }); 50 | 51 | it('fetch orders by maker', async () => { 52 | const orders = await sdk.orderFetcher.getOrdersByMaker({ 53 | maker: '0x08c1ae7e46d4a13b766566033b5c47c735e19f6f', 54 | orderType: OrderType.Offer, 55 | includePrivate: true, 56 | }); 57 | console.log(orders, orders.offerDTOs?.[0]?.orderHash); 58 | expect(orders.offerDTOs.length > 0).toBe(true); 59 | expect(typeof orders.offerDTOs?.[0].orderHash).toBe('string'); 60 | }); 61 | 62 | it('fetch orders by NFT', async () => { 63 | const orders = await sdk.orderFetcher.getOrdersByNFT({ 64 | contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', 65 | tokenId: '2632', 66 | orderType: OrderType.Offer, 67 | }); 68 | expect(orders.offerDTOs.length > 0).toBe(true); 69 | expect(typeof orders.offerDTOs?.[0].orderHash).toBe('string'); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/asset/bulk_listing_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NFTGo/GoTrading/523086ea2106784928e86ee5952c9786df2247f4/docs/asset/bulk_listing_server.png -------------------------------------------------------------------------------- /docs/interfaces/CollectionListingResponse.md: -------------------------------------------------------------------------------- 1 | - ***CollectionListingResponse*** 2 | ```ts 3 | export interface CollectionListingResponse { 4 | /** 5 | * Nfts,List of NFTs in the collection 6 | */ 7 | nfts: NFT[]; 8 | /** 9 | * Total,Total number of items 10 | */ 11 | total: number; 12 | } 13 | ``` 14 | 15 | - ***NFT*** 16 | ```ts 17 | export interface NFT { 18 | /** 19 | * Animation Url,The url of animation associated with the NFT 20 | */ 21 | animationUrl?: string; 22 | /** 23 | * Blockchain,Name of the blockchain the NFT belongs to 24 | */ 25 | blockchain: string; 26 | /** 27 | * Collection Name,Name of the collection the NFT belongs to 28 | */ 29 | collectionName?: string; 30 | /** 31 | * Collection Opensea Slug,Opensea Slug of the collection the NFT belongs to 32 | */ 33 | collectionOpenseaSlug?: string; 34 | /** 35 | * Collection Slug,NFTGo Slug of the collection the NFT belongs to 36 | */ 37 | collectionSlug?: string; 38 | /** 39 | * Contract Address,Contract address of the collection the NFT belongs to 40 | */ 41 | contractAddress: string; 42 | /** 43 | * Description,The description of the NFT 44 | */ 45 | description?: string; 46 | /** 47 | * Image,The url or base64 data of image or video associated with the NFT 48 | */ 49 | image?: string; 50 | /** 51 | * Last Sale,Last sale price of the NFT 52 | */ 53 | lastSale?: Sale; 54 | listingData?: ListingOrder; 55 | /** 56 | * Listing Price,Listing price of the NFT 57 | */ 58 | listingPrice?: Price; 59 | /** 60 | * Listing Time,Listing time of the NFT, formatted as timestamp in second. 61 | */ 62 | listingTime?: number; 63 | /** 64 | * Marketplace,Listing marketplace of the NFT 65 | */ 66 | marketplace?: string; 67 | /** 68 | * Marketplace Link,Marketplace link of the NFT 69 | */ 70 | marketplaceLink?: string; 71 | /** 72 | * Name,The name of the NFT 73 | */ 74 | name?: string; 75 | /** 76 | * Owner Addresses,List of owner addresses currently holding the NFT. 77 | * A list of one address if it's an ERC721 NFT. A list of addresses if it's an ERC1155 NFT. 78 | */ 79 | ownerAddresses?: string[]; 80 | /** 81 | * Rarity,NFT Rarity score. Calculation methods can be seen as below: 82 | * https://mirror.xyz/nftgoio.eth/kHWaMtNY6ZOvDzr7PR99D03--VNu6-ZOjYuf6E9-QH0 83 | */ 84 | rarity?: Rarity; 85 | /** 86 | * Token Id,The token ID of the NFT 87 | */ 88 | tokenId: string; 89 | /** 90 | * Traits,The list of NFT traits. Traits consist of a series of types and values, referring 91 | * to the feature of an NFT. For example, if a project has avatar NFTs, the traits may 92 | * include headwear, facial traits, clothes, etc. Traits make each item in an NFT collection 93 | * unique and determine its rarity in a collection. 94 | */ 95 | traits?: Trait[]; 96 | } 97 | ``` 98 | 99 | - ***ListingOrder*** 100 | ```ts 101 | export interface ListingOrder { 102 | /** 103 | * Contract,Address of the contract for this NFT collection, beginning with 0x 104 | */ 105 | contract?: string; 106 | /** 107 | * Eth Price,The price(eth) of the NFT 108 | */ 109 | ethPrice?: number; 110 | /** 111 | * Expired Time,The listing expire time of the NFT 112 | */ 113 | expiredTime?: number; 114 | /** 115 | * Listing Time,The listing time of the NFT 116 | */ 117 | listingTime?: number; 118 | /** 119 | * Market Link,The listing market link the NFT 120 | */ 121 | marketLink?: string; 122 | /** 123 | * Market Name,The listing market name the NFT 124 | */ 125 | marketName?: string; 126 | /** 127 | * Order Id,ID for aggregate 128 | */ 129 | orderId?: string; 130 | /** 131 | * Seller Address,The seller address of the NFT 132 | */ 133 | sellerAddress?: string; 134 | /** 135 | * Token Id,The token ID for this NFT. Each item in an NFT collection will be assigned a 136 | * unique id, the value generally ranges from 0 to N, with N being the total number of 137 | * NFTs in a collection. 138 | */ 139 | tokenId?: string; 140 | /** 141 | * Usd Price,The usd price(usd) of the NFT 142 | */ 143 | usdPrice?: number; 144 | } 145 | ``` 146 | -------------------------------------------------------------------------------- /docs/interfaces/CollectionListingsParam.md: -------------------------------------------------------------------------------- 1 | - ***CollectionListingsParam*** 2 | ```ts 3 | export interface CollectionListingsParam { 4 | /** 5 | * Select specific traits for nft. Use '-' to join trait type and trait value, and ',' to join different traits. For example, 'Eyes-Bored,Fur-Trippy'. Default is None for not selecting traits. 6 | */ 7 | traits?: string; 8 | /** 9 | * Sort by listing_price_low_to_high / listing_price_high_to_low / last_price_low_to_high / last_price_high_to_low / rarity_low_to_high / rarity_high_to_low / sales_time 10 | */ 11 | sortBy?: SortBy; 12 | /** 13 | * The index of data segments. The returned data is divided into many segments. One segment is returned at a time. {offset} parameter indicates the index of data segments. 14 | */ 15 | offset?: number; // default: 0 16 | /** 17 | * The size of a returned data segment 18 | */ 19 | limit?: number; // default: 10 20 | /** 21 | * Queries can be searched with this keyword. 22 | */ 23 | keyWord?: string; 24 | /** 25 | * Queries can be bounded by a Min price and Max Price. 26 | */ 27 | min_price?: number; 28 | /** 29 | * Queries can be bounded by a Min price and Max Price. 30 | */ 31 | max_price?: number; 32 | } 33 | ``` 34 | - SortBy 35 | ```ts 36 | export type SortBy = 37 | | 'listing_price_low_to_high' 38 | | 'listing_price_high_to_low' 39 | | 'last_price_low_to_high' 40 | | 'last_price_high_to_low' 41 | | 'rarity_low_to_high' 42 | | 'rarity_high_to_low' 43 | | 'sales_time'; 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/interfaces/ListingNFTParams.md: -------------------------------------------------------------------------------- 1 | - ***ListingNFTParams*** 2 | ```ts 3 | 4 | export interface NFTBaseInfo { 5 | contract?: string; 6 | tokenId?: string; 7 | } 8 | 9 | export interface NFTInfoForListing extends NFTBaseInfo { 10 | marketplace: Marketplace; 11 | ethPrice: number; 12 | // The meaning of the field is the time when the order was placed, usually either now or at some specific point in time. It is measured in milliseconds. 13 | listingTime: number; 14 | // the expiration time of the order, measured in milliseconds. 15 | expirationTime: number; 16 | // List of fees (formatted as `feeRecipient:feeBps`) to be bundled within the order. Example: `0xF296178d553C8Ec21A2fBD2c5dDa8CA9ac905A00:100` 17 | fees?: string[]; 18 | royaltyBps?: number; 19 | // Only Opensea requires this field to be configured, which means that the royalty set by the exchange will be automatically used. 20 | automatedRoyalties?: boolean; 21 | } 22 | 23 | export interface ErrorListingItem { 24 | // Here the reason for the error will be returned, usually due to signing or errors passed through by the exchange request. 25 | reason: string; 26 | // Refers to which stage in the entire order placing process the error occurred. 27 | reasonStep?: string; 28 | orderIndexes: number[]; 29 | } 30 | 31 | export interface ApprovePolicyOption { 32 | /** 33 | * If true, automatically approves all unapproved NFTs. 34 | * default: false 35 | */ 36 | autoApprove?: boolean; 37 | 38 | /** 39 | * If true, skips unapproved NFTs and proceeds with the approved ones. 40 | * default: false 41 | */ 42 | skipUnapproved?: boolean; 43 | } 44 | 45 | export interface BulkListingOptions extends ApprovePolicyOption { 46 | onFinish?: (successIndexes: number[], failedItems?: ErrorListingItem[]) => void; 47 | onError?: (err: Error) => void; 48 | } 49 | 50 | ``` 51 | 52 | 53 | - ***ListingStepNFTParams*** 54 | ```ts 55 | 56 | // prepare listing response 57 | export interface ListingStepsDetailInfo { 58 | steps: [StepInfo, StepInfo]; 59 | errors: any[]; 60 | } 61 | 62 | interface StepInfo { 63 | id: string; 64 | action: string; 65 | description: string; 66 | kind: 'transaction' | 'signature'; 67 | items: T[]; 68 | } 69 | 70 | export interface ApprovalItem { 71 | status: 'complete' | 'incomplete'; 72 | orderIndexes: number[]; 73 | data?: { 74 | from: string; 75 | to: string; 76 | data: string; 77 | } | null; 78 | } 79 | 80 | 81 | interface ListingAction { 82 | post: PostData | BulkSeaPortPostData; 83 | sign: SignData; 84 | } 85 | 86 | export interface BulkSeaPortPostData { 87 | body: { 88 | items: BulkSeaPortOrder[]; 89 | source: string; 90 | }; 91 | endpoint: '/order/v4'; 92 | method: 'POST'; 93 | } 94 | 95 | export interface PostData { 96 | body: { 97 | order: { 98 | data: Record; 99 | kind: string; 100 | }; 101 | orderbook: string; 102 | source: string; 103 | }; 104 | endpoint: '/order/v3' | '/order/v4'; 105 | method: 'POST'; 106 | } 107 | 108 | export interface SignData { 109 | domain: { 110 | name: string; 111 | version: string; 112 | chainId: number; 113 | verifyingContract: string; 114 | }; 115 | signatureKind: 'eip191' | 'eip712'; 116 | types: Record; 117 | value: Record; 118 | // If it is not seaportv1.5, the following fields will be included. 119 | orderIndex?: number; 120 | status?: 'complete' | 'incomplete'; 121 | message?: string; 122 | } 123 | 124 | 125 | export interface ListingItem { 126 | status: 'complete' | 'incomplete'; 127 | orderIndexes: number[]; 128 | data?: ListingAction; 129 | } 130 | 131 | export interface SignedListingItem { 132 | signature: string; 133 | post: PostData | BulkSeaPortPostData; 134 | orderIndexes: number[]; 135 | } 136 | 137 | ``` 138 | 139 | -------------------------------------------------------------------------------- /docs/interfaces/SingleAddressListingsResponse.md: -------------------------------------------------------------------------------- 1 | - ***SingleAddressListingsResponse*** 2 | ```ts 3 | export interface SingleAddressListingsResponse { 4 | /** 5 | * Last Updated,Last updated timestamp in seconds 6 | */ 7 | lastUpdated: number; 8 | /** 9 | * Nft List 10 | */ 11 | nfts: NFT[]; 12 | } 13 | ``` 14 | - ***NFT*** 15 | ```ts 16 | export interface NFT { 17 | /** 18 | * Animation Url,The url of animation associated with the NFT 19 | */ 20 | animationUrl?: string; 21 | /** 22 | * Blockchain,Name of the blockchain the NFT belongs to 23 | */ 24 | blockchain: string; 25 | /** 26 | * Collection Name,Name of the collection the NFT belongs to 27 | */ 28 | collectionName?: string; 29 | /** 30 | * Collection Opensea Slug,Opensea Slug of the collection the NFT belongs to 31 | */ 32 | collectionOpenseaSlug?: string; 33 | /** 34 | * Collection Slug,NFTGo Slug of the collection the NFT belongs to 35 | */ 36 | collectionSlug?: string; 37 | /** 38 | * Contract Address,Contract address of the collection the NFT belongs to 39 | */ 40 | contractAddress: string; 41 | /** 42 | * Description,The description of the NFT 43 | */ 44 | description?: string; 45 | /** 46 | * Image,The url or base64 data of image or video associated with the NFT 47 | */ 48 | image?: string; 49 | /** 50 | * Last Sale,Last sale price of the NFT 51 | */ 52 | lastSale?: Sale; 53 | listingData?: ListingOrder; 54 | /** 55 | * Listing Price,Listing price of the NFT 56 | */ 57 | listingPrice?: Price; 58 | /** 59 | * Listing Time,Listing time of the NFT, formatted as timestamp in second. 60 | */ 61 | listingTime?: number; 62 | /** 63 | * Marketplace,Listing marketplace of the NFT 64 | */ 65 | marketplace?: string; 66 | /** 67 | * Marketplace Link,Marketplace link of the NFT 68 | */ 69 | marketplaceLink?: string; 70 | /** 71 | * Name,The name of the NFT 72 | */ 73 | name?: string; 74 | /** 75 | * Owner Addresses,List of owner addresses currently holding the NFT.A list of one 76 | * address if it's an ERC721 NFT. A list of addresses if it's an ERC1155 NFT. 77 | */ 78 | ownerAddresses?: string[]; 79 | /** 80 | * Rarity,NFT Rarity score. Calculation methods can be seen as below: 81 | * https://mirror.xyz/nftgoio.eth/kHWaMtNY6ZOvDzr7PR99D03--VNu6-ZOjYuf6E9-QH0 82 | */ 83 | rarity?: Rarity; 84 | /** 85 | * Token Id,The token ID of the NFT 86 | */ 87 | tokenId: string; 88 | /** 89 | * Traits,The list of NFT traits. Traits consist of a series of types and values, referring 90 | * to the feature of an NFT. For example, if a project has avatar NFTs, the traits may 91 | * include headwear, facial traits, clothes, etc. Traits make each item in an NFT collection 92 | * unique and determine its rarity in a collection. 93 | */ 94 | traits?: Trait[]; 95 | } 96 | ``` 97 | 98 | - ***ListingOrder*** 99 | ```ts 100 | export interface ListingOrder { 101 | /** 102 | * Contract,Address of the contract for this NFT collection, beginning with 0x 103 | */ 104 | contract?: string; 105 | /** 106 | * Eth Price,The price(eth) of the NFT 107 | */ 108 | ethPrice?: number; 109 | /** 110 | * Expired Time,The listing expire time of the NFT 111 | */ 112 | expiredTime?: number; 113 | /** 114 | * Listing Time,The listing time of the NFT 115 | */ 116 | listingTime?: number; 117 | /** 118 | * Market Link,The listing market link the NFT 119 | */ 120 | marketLink?: string; 121 | /** 122 | * Market Name,The listing market name the NFT 123 | */ 124 | marketName?: string; 125 | /** 126 | * Order Id,ID for aggregate 127 | */ 128 | orderId?: string; 129 | /** 130 | * Seller Address,The seller address of the NFT 131 | */ 132 | sellerAddress?: string; 133 | /** 134 | * Token Id,The token ID for this NFT. Each item in an NFT collection will be assigned a 135 | * unique id, the value generally ranges from 0 to N, with N being the total number of 136 | * NFTs in a collection. 137 | */ 138 | tokenId?: string; 139 | /** 140 | * Usd Price,The usd price(usd) of the NFT 141 | */ 142 | usdPrice?: number; 143 | } 144 | ``` 145 | -------------------------------------------------------------------------------- /docs/interfaces/SingleNftListingResponse.md: -------------------------------------------------------------------------------- 1 | - ***SingleNFTListingsResponse*** 2 | ```ts 3 | export interface SingleNFTListingsResponse { 4 | /** 5 | * Last Updated,Last updated timestamp in seconds 6 | */ 7 | lastUpdated: number; 8 | /** 9 | * Nft List 10 | */ 11 | listingOrders: ListingOrder[]; 12 | } 13 | ``` 14 | 15 | - ***ListingOrder*** 16 | ```ts 17 | export interface ListingOrder { 18 | /** 19 | * Contract,Address of the contract for this NFT collection, beginning with 0x 20 | */ 21 | contract?: string; 22 | /** 23 | * Eth Price,The price(eth) of the NFT 24 | */ 25 | ethPrice?: number; 26 | /** 27 | * Expired Time,The listing expire time of the NFT 28 | */ 29 | expiredTime?: number; 30 | /** 31 | * Listing Time,The listing time of the NFT 32 | */ 33 | listingTime?: number; 34 | /** 35 | * Market Link,The listing market link the NFT 36 | */ 37 | marketLink?: string; 38 | /** 39 | * Market Name,The listing market name the NFT 40 | */ 41 | marketName?: string; 42 | /** 43 | * Order Id,ID for aggregate 44 | */ 45 | orderId?: string; 46 | /** 47 | * Seller Address,The seller address of the NFT 48 | */ 49 | sellerAddress?: string; 50 | /** 51 | * Token Id,The token ID for this NFT. Each item in an NFT collection will be assigned a 52 | * unique id, the value generally ranges from 0 to N, with N being the total number of 53 | * NFTs in a collection. 54 | */ 55 | tokenId?: string; 56 | /** 57 | * Usd Price,The usd price(usd) of the NFT 58 | */ 59 | usdPrice?: number; 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/interfaces/TradeAggregatorParams.md: -------------------------------------------------------------------------------- 1 | - ***TradeAggregatorParams*** 2 | ```ts 3 | export interface TradeAggregatorParams { 4 | /** 5 | * Buyer Address,Address of buyer. 6 | */ 7 | buyerAddress: string; 8 | /** 9 | * Is Safe,Is it safe mode? true or false 10 | */ 11 | isSafe?: boolean; 12 | /** 13 | * Order Ids,A list of orderIds.order id is from listing API. 14 | */ 15 | orderIds: string[]; 16 | } 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /docs/interfaces/TradeAggregatorResponse.md: -------------------------------------------------------------------------------- 1 | - ***TradeAggregatorResponse*** 2 | ```ts 3 | export interface TradeAggregatorResponse { 4 | /** 5 | * Gas Limit, recommended gas limit as input for this transaction. 6 | */ 7 | gasLimit: number; 8 | /** 9 | * Saving Gas, gas saved by using GoTrading aggregator. 10 | */ 11 | savingGas: number; 12 | txInfo: TXInfo; 13 | /** 14 | * Used Gas,gas used on testnet for this transaction simulation. 15 | */ 16 | usedGas: number; 17 | } 18 | ``` 19 | - ***TXInfo*** 20 | ```ts 21 | export interface TXInfo { 22 | /** 23 | * Data,The data of the transaction. 24 | */ 25 | data: string; 26 | /** 27 | * From Address,The address of the from 28 | */ 29 | fromAddress: string; 30 | /** 31 | * To Address,The address of the to 32 | */ 33 | toAddress: string; 34 | /** 35 | * Value,The price(eth) of the NFT 36 | */ 37 | value: number; 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/model_diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NFTGo/GoTrading/523086ea2106784928e86ee5952c9786df2247f4/docs/model_diagram.jpg -------------------------------------------------------------------------------- /docs/process.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NFTGo/GoTrading/523086ea2106784928e86ee5952c9786df2247f4/docs/process.jpg -------------------------------------------------------------------------------- /docs/tradeAggregator/BulkBuy.md: -------------------------------------------------------------------------------- 1 | # Bulk Buy 2 | ## ***complete example*** 3 | - For server-side: 4 | ```ts 5 | import { init, NFTInfoForTrade } from '@nftgo/gotrading'; 6 | import Web3 from 'web3'; 7 | 8 | const provider = new Web3.providers.HttpProvider('https://mainnet.infura.io') //Replace with your own provider 9 | const configs = { 10 | apiKey: "YOUR-API-KEY", // Replace with your own API Key. 11 | web3Provider: provider, 12 | walletConfig: { 13 | address: "Your wallet address", 14 | privateKey: "Your private key" 15 | }, // Replace with your wallet info. 16 | agent: new HttpsProxyAgent({ // If you have problem connect to our api end point, please config your http agent 17 | host: "your host ip", 18 | port: "your agent port", 19 | }), 20 | }; 21 | 22 | // Create tradeAggregator client 23 | const {aggregator, utils} = init(configs); 24 | 25 | // List some NFTs you want to buy 26 | // We recommend you using our aggregator.getListingsOfNFT method to check whether your nfts have valid listings 27 | 28 | /** 29 | * Note: If you experience a slower response time when placing orders, it might be due to the presence of a Blur order, which requires an additional signature login to support the order purchase. 30 | The signature login process could take 5-15 seconds or even more. 31 | */ 32 | const nfts: NFTInfoForTrade[] = [ // Replace with your own nft list 33 | { 34 | contract: "0xcfff4c8c0df0e2431977eba7df3d3de857f4b76e", 35 | tokenId: "16", 36 | amount: 1 // How many you want to buy. Usually used in ERC1155 nfts 37 | }, 38 | { 39 | contract: "0xcfff4c8c0dF0E2431977EbA7dF3D3De857f4B76e", 40 | tokenId: "18", 41 | amount: 1 42 | } 43 | ] 44 | 45 | // Config your bulk 46 | const bulkBuyConfig = { 47 | ignoreUnListedNFTs: false, // Do you want to ignore unlisted NFTs? 48 | ignoreInvalidOrders: false, // Do you want to ignore invalid orders? 49 | ignoreSuspiciousNFTs: false, // Do you want to ignore suspicious NFTs? 50 | withSafeMode: false, // Use Safe Mode or Without Safe Mode. 51 | }; 52 | 53 | // Buy nfts 54 | aggregator.bulkBuy({ 55 | nfts, 56 | onSendingTransaction: (hash: string) => console.log(hash), // Callback on sending a transaction 57 | onFinishTransaction: ( // Callback on a transaction finished 58 | successNFTs: NFTBaseInfo[], 59 | failNFTs: NFTBaseInfo[], 60 | nftsListingInfo: NftsListingInfo 61 | ) => console.log(successNFTs, failNFTs, nftsListingInfo), 62 | onError: (error: Error, nftsListingInfo?: NftsListingInfo) => 63 | console.log(error, nftsListingInfo), // Callback on any error occurs 64 | config: bulkBuyConfig, 65 | }); 66 | ``` 67 | - For client-side: 68 | ```ts 69 | import { init } from '@nftgo/gotrading'; 70 | import Web3 from 'web3'; 71 | 72 | // For client 73 | const provider = window.ethereum; 74 | const configs = { 75 | apiKey: 'YOUR-API-KEY', // Replace with your own API Key. 76 | }; 77 | 78 | // Create tradeAggregator client 79 | const {aggregator, utils} = init(configs); 80 | // List some NFTs you want to buy 81 | // We recommend you using our aggregator.getListingsOfNFT method to check whether your nfts have valid listings 82 | const nfts: NFTInfoForTrade[] = [ // Replace with your own nft list 83 | { 84 | contract: "0xcfff4c8c0df0e2431977eba7df3d3de857f4b76e", 85 | tokenId: "16", 86 | amount: 1 87 | }, 88 | { 89 | contract: "0xcfff4c8c0dF0E2431977EbA7dF3D3De857f4B76e", 90 | tokenId: "18", 91 | amount: 1 92 | } 93 | ] 94 | 95 | // Config your bulk 96 | const bulkBuyConfig = { 97 | ignoreUnListedNFTs: false, // Do you want to ignore unlisted NFTs? 98 | ignoreInvalidOrders: false, // Do you want to ignore invalid orders? 99 | ignoreSuspiciousNFTs: false, // Do you want to ignore suspicious NFTs? 100 | withSafeMode: false, // Use Safe Mode or Without Safe Mode. 101 | }; 102 | 103 | // Buy nfts 104 | aggregator.bulkBuy({ 105 | nfts, 106 | onSendingTransaction: (hash: string) => console.log(hash), // Callback on sending a transaction 107 | onFinishTransaction: ( // Callback on a transaction finished 108 | successNFTs: NFTBaseInfo[], 109 | failNFTs: NFTBaseInfo[], 110 | nftsListingInfo: NftsListingInfo 111 | ) => console.log(successNFTs, failNFTs, nftsListingInfo), 112 | onError: (error: Error, nftsListingInfo?: NftsListingInfo) => 113 | console.log(error, nftsListingInfo), // Callback on any error occurs 114 | config: bulkBuyConfig, 115 | }); 116 | ``` 117 | -------------------------------------------------------------------------------- /docs/tradeAggregator/BulkList.md: -------------------------------------------------------------------------------- 1 | # Quick Start for ListingIndexerStable 2 | 3 | ## Introduction 4 | 5 | If you need to list NFTs, you can use the `ListingIndexerStable` class for easy operation. It supports single and batch listing, and can be listed on multiple marketplaces such as opensea x2y2 looksrare. In addition, this class also supports setting royalty information and other features. 6 | 7 | ## Usage 8 | 9 | Here are some actual examples of code. 10 | 11 | ## ***complete example*** 12 | - For server-side: 13 | ```ts 14 | // init sdk client 15 | import Web3 from 'web3'; 16 | import { initListingIndexer, NFTInfoForListing } from '@nftgo/gotrading'; 17 | 18 | // server 19 | const provider = new Web3.providers.HttpProvider('https://cloudflare-eth.com/'); 20 | 21 | const openseaApi = { 22 | apiKey: 'apiKey', // replace with your own api key 23 | requestsPerInterval: 2, 24 | interval: 1000, 25 | }; 26 | //Replace with your own provider 27 | const config = { 28 | apiKey: 'api key', // Replace with your own API Key. 29 | web3Provider: provider, // Replace with your provider, 30 | walletConfig: { 31 | address: 'Your wallet address', 32 | privateKey: 'Your private key', 33 | }, // Replace with your wallet info. 34 | openSeaApiKeyConfig: openseaApi, 35 | // looksRareApiKeyConfig: looksrareApi, 36 | // x2y2ApiKeyConfig: x2y2Api, 37 | }; 38 | // create Indexer client 39 | const { listingIndexer } = initListingIndexer(config); 40 | 41 | // Get the listing info of BAYC No.1 42 | const baycContract = '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D'; 43 | 44 | const listingNFTS: NFTInfoForListing[] = [ 45 | { 46 | contract: baycContract, 47 | tokenId: '1', 48 | ethPrice: 60, 49 | marketplace: 'OpenSea', 50 | }, 51 | { 52 | contract: baycContract, 53 | tokenId: '2', 54 | ethPrice: 60, 55 | marketplace: 'OpenSea', 56 | }, 57 | { 58 | contract: baycContract, 59 | tokenId: '2', 60 | ethPrice: 60, 61 | marketplace: 'LooksRare', 62 | }, 63 | ]; 64 | 65 | const listingDataResult = await listingIndexer.bulkListing(listingNFTS, { 66 | autoApprove: true, 67 | onFinish: (successIndexes, failedItems) => { 68 | // successIndexes: [0, 1] 69 | console.log(successIndexes); 70 | }, 71 | onError: (err) => { 72 | console.error(err); 73 | }, 74 | }); 75 | 76 | ``` 77 | 78 | ```ts 79 | // You need to initialize a listingIndexer as shown in the code above. 80 | const listingNFTS: NFTInfoForListing[] = [ 81 | { 82 | contract: baycContract, 83 | tokenId: '1', 84 | ethPrice: 60, 85 | marketplace: 'OpenSea', 86 | }, 87 | ]; 88 | const maker = '0x0000' ?? this.config.walletConfig?.address 89 | 90 | const bulkListing = () => { 91 | /** 92 | * Step 1: Prepare listing: 93 | * This function takes two parameters: a list of NFTs to be listed and the owner's address. 94 | * The prepareListing function returns the specific parameter details required for the subsequent steps of the process 95 | * such as the parameters needed for signing and posting. 96 | */ 97 | const data = await listingIndexer.prepareListing(listingNFTS, maker); 98 | /** 99 | * Then, do some simple data formatting and prepare to hand it over to the next process. 100 | */ 101 | const approvalData = listingIndexer.parseApprovalData(data); 102 | const listingData = listingIndexer.parseListingData(data); 103 | /** 104 | * Step 2: Approve Listing Item with Policy: 105 | * This function will authorize the approvedItems and return the final set of ListingItems. 106 | * Note that NFTs must be authorized before being listed, and only one authorization is required per collection per address. 107 | */ 108 | const approvalResult = await listingIndexer.approveWithPolicy([approvalData, listingData]); 109 | /** 110 | * Step 3: Sign Listing Item: 111 | * This function takes in an array of ListingItem objects that need to be listed. 112 | * The user will sign these items using their configured private key, typically stored in their wallet on the client-side. 113 | * Once signed, the function returns an array containing two elements: 114 | SignedListingItem[]: the successfully signed ListingItems. 115 | ErrorListingItem[]: any ListingItems that failed to be signed. 116 | */ 117 | const [listingResult, errorOrders] = await listingIndexer.signListingOrders(approvalResult); 118 | /** 119 | * Step 4: Post Listing Item: 120 | * This function will post the listing order to the target marketplace. 121 | * It takes as input the SignedListingItem that was previously signed in the previous step. 122 | * This is the final step of the listing process, where a request is made to the market API. 123 | * The function will return information about the final result of the listing. 124 | */ 125 | const [successIndexes, errorItems] = await listingIndexer.bulkPostListingOrders(listingResult); 126 | const errorIndexes = [...errorOrders, ...errorItems]; 127 | ``` 128 | > 129 | 130 | ## ***interface*** 131 | - ***interface*** 132 | - [***ListingNFTParams***](https://github.com/NFTGo/GoTrading-js/blob/feat/draft/docs/interfaces/ListingNFTParams.md) 133 | - [***ListingStepNFTParams***](https://github.com/NFTGo/GoTrading-js/blob/feat/draft/docs/interfaces/ListingNFTParams.md) 134 | -------------------------------------------------------------------------------- /docs/tradeAggregator/BuyByCollectionListings.md: -------------------------------------------------------------------------------- 1 | # Buy by collection listings 2 | ## ***complete example*** 3 | - For server-side: 4 | ```ts 5 | // init sdk client 6 | import Web3 from 'web3'; 7 | import { BigNumber } from "ethers"; 8 | import { CollectionListingsParam, AggregateParams, AggregateResponse, init } from "@nftgo/gotrading"; 9 | 10 | // server 11 | const provider = new Web3.providers.HttpProvider( 12 | "https://rpc.tenderly.co/fork/823ef734-4730-4063-bb00-640c54940021" 13 | ); //Replace with your own provider 14 | const configs = { 15 | apiKey: "api key", // Replace with your own API Key. 16 | web3Provider: "provider", // Replace with your provider, 17 | walletConfig: { 18 | address: "Your wallet address", 19 | privateKey: "Your private key" 20 | }, // Replace with your wallet info. 21 | }; 22 | // create tradeAggregator client 23 | const {aggregator, utils} = init(configs); 24 | 25 | // Get the listing info of BAYC. 26 | const baycContract = '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D'; // Replace with your test collection 27 | 28 | const collectionResponse = await aggregator.getListingsOfCollection(baycContract); 29 | 30 | let orderIds:string[] = []; 31 | for (const nft of collectionResponse.nfts) { 32 | orderIds.push(nft.listingData?.listingOrders[0].orderId as string); 33 | } 34 | const buyerAddress = "0x1234567890123456789012345678901234567890";// Replace with buyer address. 35 | // without safe mode 36 | const params: AggregateParams = ({ 37 | buyerAddress: buyerAddress, 38 | isSafe: false, 39 | orderIds: orderIds, 40 | }); 41 | 42 | const aggregateResponse = await aggregator.getAggregateInfo(params); 43 | 44 | utils?.sendTransaction({ 45 | from: aggregateResponse.txInfo.fromAddress, 46 | to: aggregateResponse.txInfo.toAddress, 47 | data: aggregateResponse.txInfo.data, 48 | value: BigNumber.from(aggregateResponse.txInfo.value.toString()).toHexString() 49 | }).on('transactionHash', (hash)=>{ 50 | console.log(hash); 51 | }).on('receipt', (receipt)=>{ 52 | if (receipt.logs.length) { 53 | for (const log of receipt.logs) { 54 | // not every log with useful info 55 | const decodedLog = utils.decodeLog(log); 56 | } 57 | }else { 58 | console.log('transaction fail for some unknown reason') 59 | } 60 | }).on('error', (error)=>{ 61 | console.log('transaction fail: ', error); 62 | }); 63 | ``` 64 | - For client-side: 65 | 66 | ```ts 67 | // init sdk client 68 | import { BigNumber } from "ethers"; 69 | import { CollectionListingsParam, AggregateParams, AggregateResponse, init } from "@nftgo/gotrading"; 70 | 71 | // client 72 | 73 | const provider = window.ethereum; 74 | const configs = { 75 | apiKey: "api key", // Replace with your own API Key. 76 | }; 77 | // create tradeAggregator client 78 | const {aggregator, utils} = init(configs); 79 | 80 | // Get the listing info of BAYC. 81 | const baycContract = '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D'; // Replace with your test collection 82 | 83 | const collectionResponse = await aggregator.getListingsOfCollection(baycContract); 84 | 85 | let orderIds:string[] = []; 86 | for (const nft of collectionResponse.nfts) { 87 | orderIds.push(nft.listingData?.listingOrders[0].orderId as string); 88 | } 89 | const buyerAddress = "0x1234567890123456789012345678901234567890";// Replace with buyer address. 90 | // without safe mode 91 | const params: AggregateParams = ({ 92 | buyerAddress: buyerAddress, 93 | isSafe: false, 94 | orderIds: orderIds, 95 | }); 96 | 97 | const aggregateResponse = await aggregator.getAggregateInfo(params); 98 | 99 | utils?.sendTransaction({ 100 | from: aggregateResponse.txInfo.fromAddress, 101 | to: aggregateResponse.txInfo.toAddress, 102 | data: aggregateResponse.txInfo.data, 103 | value: BigNumber.from(aggregateResponse.txInfo.value.toString()).toHexString() 104 | }).on('transactionHash', (hash)=>{ 105 | console.log(hash); 106 | }).on('receipt', (receipt)=>{ 107 | if (receipt.logs.length) { 108 | for (const log of receipt.logs) { 109 | // not every log with useful info 110 | const decodedLog = utils.decodeLog(log); 111 | } 112 | }else { 113 | console.log('transaction fail for some unknown reason') 114 | } 115 | }).on('error', (error)=>{ 116 | console.log('transaction fail: ', error); 117 | }); 118 | ``` 119 | > 120 | > This is [Safe Mode Example](https://github.com/NFTGo/GoTrading/blob/master/docs/tradeAggregator/tradeWithSafeMode.md). 121 | 122 | ## ***interface*** 123 | - [***CollectionListingsParam***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/CollectionListingsParam.md) 124 | - [***CollectionListingResponse***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/CollectionListingResponse.md) 125 | - [***AggregateParams***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/TradeAggregatorParams.md) 126 | - [***AggregateResponse***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/TradeAggregatorResponse.md) 127 | -------------------------------------------------------------------------------- /docs/tradeAggregator/BuyByNFTListings.md: -------------------------------------------------------------------------------- 1 | # Buy by NFT listings 2 | ## ***complete example*** 3 | - For server-side: 4 | ```ts 5 | // init sdk client 6 | import Web3 from "web3"; 7 | import { BigNumber } from "ethers"; 8 | import { init, AggregateParams, AggregateResponse, SingleNFTListingsResponse } from "@nftgo/gotrading"; 9 | 10 | // server 11 | const provider = new Web3.providers.HttpProvider( 12 | "https://rpc.tenderly.co/fork/823ef734-4730-4063-bb00-640c54940021" 13 | ); //Replace with your own provider 14 | const configs = { 15 | apiKey: "api key", // Replace with your own API Key. 16 | web3Provider: "provider", // Replace with your provider, 17 | walletConfig: { 18 | address: "Your wallet address", 19 | privateKey: "Your private key" 20 | }, // Replace with your wallet info. 21 | }; 22 | // create tradeAggregator client 23 | const {aggregator, utils} = init(configs); 24 | 25 | // Get the listing info of BAYC No.1 26 | const baycContract = "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D"; 27 | const tokenId = "1"; 28 | 29 | const listingData = await aggregator.getListingsOfNFT(baycContract, tokenId); 30 | let orderIds: string[] = []; 31 | if (listingData?.listingOrders.length > 0) { 32 | orderIds.push(listingData.listingOrders[0].orderId as string); 33 | } 34 | 35 | const buyerAddress = "0x1234567890123456789012345678901234567890";// Replace with buyer address. 36 | // without safe mode 37 | const params: AggregateParams = ({ 38 | buyerAddress: buyerAddress, 39 | isSafe: false, 40 | orderIds: orderIds, 41 | }); 42 | 43 | const aggregateResponse = await aggregator.getAggregateInfo(params); 44 | 45 | utils?.sendTransaction({ 46 | from: aggregateResponse.txInfo.fromAddress, 47 | to: aggregateResponse.txInfo.toAddress, 48 | data: aggregateResponse.txInfo.data, 49 | value: BigNumber.from(aggregateResponse.txInfo.value.toString()).toHexString() 50 | }).on('transactionHash', (hash)=>{ 51 | console.log(hash); 52 | }).on('receipt', (receipt)=>{ 53 | if (receipt.logs.length) { 54 | for (const log of receipt.logs) { 55 | // not every log with useful info 56 | const decodedLog = utils.decodeLog(log); 57 | } 58 | }else { 59 | console.log('transaction fail for some unknown reason') 60 | } 61 | }).on('error', (error)=>{ 62 | console.log('transaction fail: ', error); 63 | }); 64 | ``` 65 | - For client-side: 66 | 67 | ```ts 68 | // init sdk client 69 | import { BigNumber } from "ethers"; 70 | import { init, AggregateParams, AggregateResponse, SingleNFTListingsResponse } from '@nftgo/gotrading'; 71 | 72 | const provider = window.ethereum; 73 | const configs = { 74 | apiKey: "api key", // Replace with your own API Key. 75 | }; 76 | // create tradeAggregator client 77 | const {aggregator, utils} = init(configs); 78 | 79 | // Get the listing info of BAYC No.1 80 | const baycContract = "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D"; 81 | const tokenId = 1; 82 | 83 | const listingOrders = await aggregator.getListingsOfNFT(baycContract, tokenId); 84 | let orderIds: string[] = []; 85 | orderIds.push(listingOrders[0].orderId as string); 86 | 87 | const buyerAddress = "0x1234567890123456789012345678901234567890";// Replace with buyer address. 88 | // without safe mode 89 | const params: AggregateParams = ({ 90 | buyerAddress: buyerAddress, 91 | isSafe: false, 92 | orderIds: orderIds, 93 | }); 94 | 95 | const aggregateResponse = await aggregator.getAggregateInfo(params); 96 | 97 | utils?.sendTransaction({ 98 | from: aggregateResponse.txInfo.fromAddress, 99 | to: aggregateResponse.txInfo.toAddress, 100 | data: aggregateResponse.txInfo.data, 101 | value: BigNumber.from(aggregateResponse.txInfo.value.toString()).toHexString() 102 | }).on('transactionHash', (hash)=>{ 103 | console.log(hash); 104 | }).on('receipt', (receipt)=>{ 105 | if (receipt.logs.length) { 106 | for (const log of receipt.logs) { 107 | // not every log with useful info 108 | const decodedLog = utils.decodeLog(log); 109 | } 110 | }else { 111 | console.log('transaction fail for some unknown reason') 112 | } 113 | }).on('error', (error)=>{ 114 | console.log('transaction fail: ', error); 115 | }); 116 | ``` 117 | > 118 | > This is [Safe Mode Example](https://github.com/NFTGo/GoTrading/blob/master/docs/tradeAggregator/tradeWithSafeMode.md). 119 | 120 | ## ***interface*** 121 | - ***interface*** 122 | - [***SingleNFTListingsResponse***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/SingleNftListingResponse.md) 123 | - [***AggregateParams***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/TradeAggregatorParams.md) 124 | - [***AggregateResponse***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/TradeAggregatorResponse.md) 125 | -------------------------------------------------------------------------------- /docs/tradeAggregator/BuyByWalletListings.md: -------------------------------------------------------------------------------- 1 | # Buy by wallet listings 2 | ## ***complete example*** 3 | - For server-side: 4 | ```ts 5 | // init sdk client 6 | import Web3 from 'web3'; 7 | import { BigNumber } from "ethers"; 8 | import { CollectionListingsParam, AggregateParams, AggregateResponse, init } from "@nftgo/gotrading"; 9 | 10 | // server 11 | const provider = new Web3.providers.HttpProvider( 12 | "https://rpc.tenderly.co/fork/823ef734-4730-4063-bb00-640c54940021" 13 | ); //Replace with your own provider 14 | const configs = { 15 | apiKey: "api key", // Replace with your own API Key. 16 | web3Provider: "provider", // Replace with your provider, 17 | walletConfig: { 18 | address: "Your wallet address", 19 | privateKey: "Your private key" 20 | }, // Replace with your wallet info. 21 | }; 22 | // Create tradeAggregator client 23 | const {aggregator, utils} = init(configs); 24 | 25 | // The wallet address you want to buy from 26 | const walletAddress = "0x8ae57a027c63fca8070d1bf38622321de8004c67"; 27 | 28 | const { nfts: walletNFTList } = await aggregator.getListingsOfWallet(walletAddress); 29 | let orderIds:string[] = []; 30 | for (const nft of walletNFTList) { 31 | orderIds.push(nft.listingData?.listingOrders[0].orderId as string) 32 | } 33 | 34 | const buyerAddress = "0x1234567890123456789012345678901234567890";// Replace with buyer address. 35 | 36 | // without safe mode 37 | const params: AggregateParams = ({ 38 | buyerAddress: buyerAddress, 39 | isSafe: false, 40 | orderIds: orderIds, 41 | }); 42 | 43 | const aggregateResponse = await aggregator.getAggregateInfo(params); 44 | 45 | utils?.sendTransaction({ 46 | from: aggregateResponse.txInfo.fromAddress, 47 | to: aggregateResponse.txInfo.toAddress, 48 | data: aggregateResponse.txInfo.data, 49 | value: BigNumber.from(aggregateResponse.txInfo.value.toString()).toHexString() 50 | }).on('transactionHash', (hash)=>{ 51 | console.log(hash); 52 | }).on('receipt', (receipt)=>{ 53 | if (receipt.logs.length) { 54 | for (const log of receipt.logs) { 55 | // not every log with useful info 56 | const decodedLog = utils.decodeLog(log); 57 | } 58 | }else { 59 | console.log('transaction fail for some unknown reason') 60 | } 61 | }).on('error', (error)=>{ 62 | console.log('transaction fail: ', error); 63 | }); 64 | ``` 65 | - For client-side: 66 | 67 | ```ts 68 | // init sdk client 69 | import { BigNumber } from "ethers"; 70 | import { init, AggregateParams, AggregateResponse, SingleAddressListingsResponse } from '@nftgo/gotrading'; 71 | const configs = { 72 | apiKey: 'YOUR-API-KEY', // Replace with your own API Key. 73 | }; 74 | // create tradeAggregator client 75 | const {aggregator, utils} = init(configs); 76 | 77 | const walletAddress = "0x8ae57a027c63fca8070d1bf38622321de8004c67"; 78 | 79 | const { nfts: walletNFTList } = await aggregator.getListingsOfWallet(walletAddress); 80 | let orderIds:string[] = []; 81 | for (const nft of walletNFTList) { 82 | orderIds.push(nft.listingData?.listingOrders[0].orderId as string) 83 | } 84 | 85 | const buyerAddress = "0x1234567890123456789012345678901234567890";// Replace with buyer address. 86 | 87 | // without safe mode 88 | const params: AggregateParams = ({ 89 | buyerAddress: buyerAddress, 90 | isSafe: false, 91 | orderIds: orderIds, 92 | }); 93 | 94 | const aggregateResponse = await aggregator.getAggregateInfo(params); 95 | 96 | utils?.sendTransaction({ 97 | from: aggregateResponse.txInfo.fromAddress, 98 | to: aggregateResponse.txInfo.toAddress, 99 | data: aggregateResponse.txInfo.data, 100 | value: BigNumber.from(aggregateResponse.txInfo.value.toString()).toHexString() 101 | }).on('transactionHash', (hash)=>{ 102 | console.log(hash); 103 | }).on('receipt', (receipt)=>{ 104 | if (receipt.logs.length) { 105 | for (const log of receipt.logs) { 106 | // not every log with useful info 107 | const decodedLog = utils.decodeLog(log); 108 | } 109 | }else { 110 | console.log('transaction fail for some unknown reason') 111 | } 112 | }).on('error', (error)=>{ 113 | console.log('transaction fail: ', error); 114 | }); 115 | ``` 116 | > 117 | > This is [Safe Mode Example](https://github.com/NFTGo/GoTrading/blob/master/docs/tradeAggregator/tradeWithSafeMode.md). 118 | 119 | ## ***interface*** 120 | - [***SingleAddressListingsResponse***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/SingleAddressListingsResponse.md) 121 | - [***AggregateParams***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/TradeAggregatorParams.md) 122 | - [***AggregateResponse***](https://github.com/NFTGo/GoTrading/blob/master/docs/interfaces/TradeAggregatorResponse.md) 123 | -------------------------------------------------------------------------------- /docs/tradeAggregator/tradeWithSafeMode.md: -------------------------------------------------------------------------------- 1 | # Trade with safe mode 2 | ```ts 3 | const buyerAddress = "0x1234567890123456789012345678901234567890";// Replace with buyer address. 4 | const params: AggregateParams = { 5 | buyerAddress: buyerAddress, 6 | isSafe: true, 7 | orderIds: orderIds, 8 | }; 9 | 10 | const aggregateResponse = await aggregator.getAggregateInfo(params); 11 | 12 | utils 13 | ?.sendSafeModeTransaction({ 14 | from: aggregateResponse.txInfo.fromAddress, 15 | to: aggregateResponse.txInfo.toAddress, 16 | data: aggregateResponse.txInfo.data, 17 | value: BigNumber.from(aggregateResponse.txInfo.value.toString()), 18 | chainId: 1, 19 | gasLimit: BigNumber.from(aggregateResponse.gasLimit.toString()), 20 | }) 21 | .on('transactionHash', (hash)=>{ 22 | console.log(hash); 23 | }) 24 | .on('receipt', (receipt)=>{ 25 | if (receipt.logs.length) { 26 | for (const log of receipt.logs) { 27 | // not every log with useful info 28 | const decodedLog = utils.decodeLog(log); 29 | } 30 | } else { 31 | console.log('transaction fail for some unknown reason'); 32 | } 33 | }) 34 | .on('error', (error)=>{ 35 | console.log('transaction fail: ', error); 36 | }); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/tradeAggregator/tradeWithoutSafeMode.md: -------------------------------------------------------------------------------- 1 | # Trade without safe mode 2 | ```ts 3 | // without safe mode 4 | const buyerAddress = "0x1234567890123456789012345678901234567890";// Replace with buyer address. 5 | const params: AggregateParams = ({ 6 | buyerAddress: buyerAddress, 7 | isSafe: false, 8 | orderIds: orderIds, 9 | }); 10 | 11 | const aggregateResponse = await aggregator.getAggregateInfo(params); 12 | 13 | utils?.sendTransaction({ 14 | from: aggregateResponse.txInfo.fromAddress, 15 | to: aggregateResponse.txInfo.toAddress, 16 | data: aggregateResponse.txInfo.data, 17 | value: BigNumber.from(aggregateResponse.txInfo.value.toString()).toHexString() 18 | }).on('transactionHash', (hash)=>{ 19 | console.log(hash); 20 | }).on('receipt', (receipt)=>{ 21 | if (receipt.logs.length) { 22 | for (const log of receipt.logs) { 23 | // not every log with useful info 24 | const decodedLog = utils.decodeLog(log); 25 | } 26 | }else { 27 | console.log('transaction fail for some unknown reason') 28 | } 29 | }).on('error', (error)=>{ 30 | console.log('transaction fail: ', error); 31 | }); 32 | ``` 33 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | const path = require('path'); 7 | const dotenv = require('dotenv'); 8 | const env = dotenv.config({ path: path.resolve(__dirname, '.env') }); 9 | 10 | module.exports = { 11 | testTimeout: 30e3, 12 | // All imported modules in your tests should be mocked automatically 13 | // automock: false, 14 | 15 | // Stop running tests after `n` failures 16 | // bail: 0, 17 | 18 | // The directory where Jest should store its cached dependency information 19 | // cacheDirectory: "/private/var/folders/40/cnwcs2d14pj4_bg8kgp0x2c80000gn/T/jest_dx", 20 | 21 | // Automatically clear mock calls, instances, contexts and results before every test 22 | clearMocks: true, 23 | 24 | // Indicates whether the coverage information should be collected while executing the test 25 | collectCoverage: true, 26 | 27 | // An array of glob patterns indicating a set of files for which coverage information should be collected 28 | collectCoverageFrom: ['/__test__/**/*.test.ts'], 29 | 30 | // The directory where Jest should output its coverage files 31 | coverageDirectory: 'coverage', 32 | 33 | // An array of regexp pattern strings used to skip coverage collection 34 | // coveragePathIgnorePatterns: [ 35 | // "/node_modules/" 36 | // ], 37 | 38 | // Indicates which provider should be used to instrument code for coverage 39 | coverageProvider: 'v8', 40 | 41 | // A list of reporter names that Jest uses when writing coverage reports 42 | // coverageReporters: [ 43 | // "json", 44 | // "text", 45 | // "lcov", 46 | // "clover" 47 | // ], 48 | 49 | // An object that configures minimum threshold enforcement for coverage results 50 | coverageThreshold: { 51 | global: { 52 | branches: 100, 53 | functions: 100, 54 | lines: 100, 55 | statements: 100, 56 | }, 57 | }, 58 | 59 | // A path to a custom dependency extractor 60 | // dependencyExtractor: undefined, 61 | 62 | // Make calling deprecated APIs throw helpful error messages 63 | // errorOnDeprecated: false, 64 | 65 | // The default configuration for fake timers 66 | // fakeTimers: { 67 | // "enableGlobally": false 68 | // }, 69 | 70 | // Force coverage collection from ignored files using an array of glob patterns 71 | // forceCoverageMatch: [], 72 | 73 | // A path to a module which exports an async function that is triggered once before all test suites 74 | // globalSetup: undefined, 75 | 76 | // A path to a module which exports an async function that is triggered once after all test suites 77 | // globalTeardown: undefined, 78 | 79 | // A set of global variables that need to be available in all test environments 80 | globals: { 81 | ...env.parsed, 82 | }, 83 | 84 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 85 | // maxWorkers: "50%", 86 | 87 | // An array of directory names to be searched recursively up from the requiring module's location 88 | // moduleDirectories: [ 89 | // "node_modules" 90 | // ], 91 | 92 | // An array of file extensions your modules use 93 | // moduleFileExtensions: [ 94 | // "js", 95 | // "mjs", 96 | // "cjs", 97 | // "jsx", 98 | // "ts", 99 | // "tsx", 100 | // "json", 101 | // "node" 102 | // ], 103 | 104 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 105 | moduleNameMapper: { 106 | '^@/types': '/src/types', 107 | '^@/abi': '/src/abi', 108 | '^@/exceptions': '/src/exceptions', 109 | '^@/http': '/src/http', 110 | '^@/common': '/src/common', 111 | }, 112 | 113 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 114 | // modulePathIgnorePatterns: [], 115 | 116 | // Activates notifications for test results 117 | // notify: false, 118 | 119 | // An enum that specifies notification mode. Requires { notify: true } 120 | // notifyMode: "failure-change", 121 | 122 | // A preset that is used as a base for Jest's configuration 123 | preset: 'ts-jest', 124 | 125 | // Run tests from one or more projects 126 | // projects: undefined, 127 | 128 | // Use this configuration option to add custom reporters to Jest 129 | // reporters: undefined, 130 | 131 | // Automatically reset mock state before every test 132 | // resetMocks: false, 133 | 134 | // Reset the module registry before running each individual test 135 | // resetModules: false, 136 | 137 | // A path to a custom resolver 138 | // resolver: undefined, 139 | 140 | // Automatically restore mock state and implementation before every test 141 | // restoreMocks: false, 142 | 143 | // The root directory that Jest should scan for tests and modules within 144 | // rootDir: undefined, 145 | 146 | // A list of paths to directories that Jest should use to search for files in 147 | // roots: [ 148 | // "" 149 | // ], 150 | 151 | // Allows you to use a custom runner instead of Jest's default test runner 152 | // runner: "jest-runner", 153 | 154 | // The paths to modules that run some code to configure or set up the testing environment before each test 155 | setupFiles: [], 156 | 157 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 158 | setupFilesAfterEnv: [], 159 | 160 | // The number of seconds after which a test is considered as slow and reported as such in the results. 161 | // slowTestThreshold: 5, 162 | 163 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 164 | // snapshotSerializers: [], 165 | 166 | // The test environment that will be used for testing 167 | testEnvironment: 'node', 168 | 169 | // Options that will be passed to the testEnvironment 170 | // testEnvironmentOptions: {}, 171 | 172 | // Adds a location field to test results 173 | // testLocationInResults: false, 174 | 175 | // The glob patterns Jest uses to detect test files 176 | // testMatch: [ 177 | // "**/__tests__/**/*.[jt]s?(x)", 178 | // "**/?(*.)+(spec|test).[tj]s?(x)" 179 | // ], 180 | 181 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 182 | // testPathIgnorePatterns: [ 183 | // "/node_modules/" 184 | // ], 185 | 186 | // The regexp pattern or array of patterns that Jest uses to detect test files 187 | // testRegex: [], 188 | 189 | // This option allows the use of a custom results processor 190 | // testResultsProcessor: undefined, 191 | 192 | // This option allows use of a custom test runner 193 | // testRunner: "jest-circus/runner", 194 | 195 | // A map from regular expressions to paths to transformers 196 | // transform: undefined, 197 | 198 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 199 | // transformIgnorePatterns: [ 200 | // "/node_modules/", 201 | // "\\.pnp\\.[^\\/]+$" 202 | // ], 203 | 204 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 205 | // unmockedModulePathPatterns: undefined, 206 | 207 | // Indicates whether each individual test should be reported during the run 208 | // verbose: undefined, 209 | 210 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 211 | // watchPathIgnorePatterns: [], 212 | 213 | // Whether to use watchman for file crawling 214 | // watchman: true, 215 | }; 216 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nftgo/gotrading", 3 | "version": "1.0.15", 4 | "main": "dist/index.js", 5 | "module": "dist/index.esm.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist/index.js", 9 | "dist/index.esm.js", 10 | "dist/index.d.ts" 11 | ], 12 | "engines": { 13 | "node": ">=16.14.0" 14 | }, 15 | "description": "JavaScript SDK for the NFTGo trading aggregator. Let users buy crypto collectibles and other cryptogoods from multi-marketplaces , all on your own site!", 16 | "license": "MIT", 17 | "author": "NFTGo Developers", 18 | "homepage": "https://github.com/NFTGo/GoTrading-js", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/NFTGo/GoTrading-js.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/NFTGo/GoTrading-js/issues" 25 | }, 26 | "scripts": { 27 | "dev": "rimraf dist types && cross-env NODE_ENV=development webpack --config ./config/webpack/webpack.dev.js", 28 | "build": "tsc -p tsconfig.build.json", 29 | "build:rollup": "rollup -c rollup.config.mjs", 30 | "publish:dev": "npm run build && npm publish --registry=https://npm.nftgo.dev/", 31 | "typedoc": "rimraf docs && typedoc", 32 | "test": "jest", 33 | "release": "standard-version", 34 | "release-major": "standard-version --release-as major", 35 | "release-minor": "standard-version --release-as minor", 36 | "release-patch": "standard-version --release-as patch", 37 | "clean": "gts clean", 38 | "fix": "gts fix" 39 | }, 40 | "standard-version": { 41 | "skip": { 42 | "commit": true 43 | } 44 | }, 45 | "devDependencies": { 46 | "@commitlint/cli": "^17.3.0", 47 | "@commitlint/config-conventional": "^17.3.0", 48 | "@ethersproject/abstract-signer": "^5.7.0", 49 | "@types/jest": "^29.2.4", 50 | "@types/node": "^14.11.2", 51 | "@typescript-eslint/eslint-plugin": "latest", 52 | "@typescript-eslint/parser": "^5.46.1", 53 | "bignumber.js": "^9.1.1", 54 | "cross-env": "^7.0.3", 55 | "dotenv": "^16.0.3", 56 | "eslint": "^8.15.0", 57 | "eslint-plugin-jest": "latest", 58 | "gts": "^3.1.1", 59 | "https-proxy-agent": "^7.0.2", 60 | "husky": "^6.0.0", 61 | "jest": "^29.5.0", 62 | "lint-staged": "^11.2.6", 63 | "standard-version": "^9.5.0", 64 | "ts-jest": "^29.0.3", 65 | "types-bn": "^0.0.1", 66 | "typescript": "^5.1.6", 67 | "undici": "^5.22.1", 68 | "web3-core": "^1.8.2", 69 | "web3-utils": "^1.8.2" 70 | }, 71 | "dependencies": { 72 | "@rollup/plugin-alias": "^5.0.0", 73 | "@rollup/plugin-commonjs": "^25.0.2", 74 | "@rollup/plugin-json": "^6.0.0", 75 | "@rollup/plugin-node-resolve": "^15.1.0", 76 | "ethers": "^5.6.9", 77 | "install": "^0.13.0", 78 | "limiter": "^2.1.0", 79 | "rollup": "^3.26.2", 80 | "rollup-plugin-dts": "^5.3.0", 81 | "rollup-plugin-peer-deps-external": "^2.2.4", 82 | "rollup-plugin-typescript2": "^0.35.0", 83 | "tslib": "^2.6.0", 84 | "web3": "^1.8.2" 85 | }, 86 | "packageManager": "pnpm@8.6.0" 87 | } 88 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import nodeResolve from '@rollup/plugin-node-resolve'; 3 | import { defineConfig } from 'rollup'; 4 | import typescript from 'rollup-plugin-typescript2'; 5 | import external from 'rollup-plugin-peer-deps-external'; 6 | import json from '@rollup/plugin-json'; 7 | import dts from 'rollup-plugin-dts'; 8 | import alias from '@rollup/plugin-alias'; 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | import * as url from 'url'; 12 | 13 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); 14 | 15 | const absolute = _path => path.resolve(__dirname, _path); 16 | const entries = { 17 | '@/types': absolute('src/types/index.ts'), 18 | '@/abi': absolute('src/abi/index.ts'), 19 | '@/exceptions': absolute('src/exceptions/index.ts'), 20 | '@/http': absolute('src/http/index.ts'), 21 | '@/common': absolute('src/common/index.ts'), 22 | }; 23 | 24 | export default defineConfig([ 25 | { 26 | input: { 27 | index: 'src/index.ts', 28 | }, 29 | output: [ 30 | { 31 | entryFileNames: 'index.esm.js', 32 | format: 'esm', 33 | dir: 'dist', 34 | }, 35 | { 36 | entryFileNames: 'index.js', 37 | format: 'cjs', 38 | dir: 'dist', 39 | }, 40 | ], 41 | plugins: [ 42 | json(), 43 | alias({ entries }), 44 | external(), 45 | commonjs(), 46 | nodeResolve({}), 47 | typescript({ tsconfig: 'tsconfig.build.json', useTsconfigDeclarationDir: true, exclude: ['__test__'] }), 48 | ], 49 | context: 'globalThis', 50 | }, 51 | { 52 | input: 'dist/src/index.d.ts', 53 | output: [{ file: 'dist/index.d.ts', format: 'es' }], 54 | plugins: [alias({ entries }), dts(), clearTargetPlugin()], 55 | }, 56 | ]); 57 | 58 | function clearTargetPlugin() { 59 | return { 60 | buildEnd() { 61 | fs.rmSync('dist/src', { recursive: true }); 62 | }, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/abi/crypto-punk.ts: -------------------------------------------------------------------------------- 1 | import { AbiItem } from 'web3-utils'; 2 | 3 | export const CryptoPunkABI: Record<'transfer' | 'bought', AbiItem> = { 4 | transfer: { 5 | // punk 6 | anonymous: false, 7 | inputs: [ 8 | { 9 | indexed: true, 10 | name: 'from', 11 | type: 'address', 12 | }, 13 | { 14 | indexed: true, 15 | name: 'to', 16 | type: 'address', 17 | }, 18 | { 19 | indexed: false, 20 | name: 'punkIndex', 21 | type: 'uint256', 22 | }, 23 | ], 24 | name: 'PunkTransfer', 25 | type: 'event', 26 | }, 27 | bought: { 28 | anonymous: false, 29 | inputs: [ 30 | { indexed: true, name: 'punkIndex', type: 'uint256' }, 31 | { indexed: false, name: 'value', type: 'uint256' }, 32 | { indexed: true, name: 'fromAddress', type: 'address' }, 33 | { indexed: true, name: 'toAddress', type: 'address' }, 34 | ], 35 | name: 'PunkBought', 36 | type: 'event', 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/abi/erc-1155.ts: -------------------------------------------------------------------------------- 1 | import { AbiItem } from 'web3-utils'; 2 | 3 | export const ERC1155ABI: Record<'singleTransfer' | 'batchTransfer', AbiItem> = { 4 | singleTransfer: { 5 | // single 1155 6 | anonymous: false, 7 | inputs: [ 8 | { 9 | indexed: true, 10 | internalType: 'address', 11 | name: 'operator', 12 | type: 'address', 13 | }, 14 | { 15 | indexed: true, 16 | internalType: 'address', 17 | name: 'from', 18 | type: 'address', 19 | }, 20 | { 21 | indexed: true, 22 | internalType: 'address', 23 | name: 'to', 24 | type: 'address', 25 | }, 26 | { 27 | indexed: false, 28 | internalType: 'uint256', 29 | name: 'id', 30 | type: 'uint256', 31 | }, 32 | { 33 | indexed: false, 34 | internalType: 'uint256', 35 | name: 'value', 36 | type: 'uint256', 37 | }, 38 | ], 39 | name: 'TransferSingle', 40 | type: 'event', 41 | }, 42 | batchTransfer: { 43 | // multi 1155 44 | anonymous: false, 45 | inputs: [ 46 | { 47 | indexed: true, 48 | internalType: 'address', 49 | name: 'operator', 50 | type: 'address', 51 | }, 52 | { 53 | indexed: true, 54 | internalType: 'address', 55 | name: 'from', 56 | type: 'address', 57 | }, 58 | { 59 | indexed: true, 60 | internalType: 'address', 61 | name: 'to', 62 | type: 'address', 63 | }, 64 | { 65 | indexed: false, 66 | internalType: 'uint256[]', 67 | name: 'ids', 68 | type: 'uint256[]', 69 | }, 70 | { 71 | indexed: false, 72 | internalType: 'uint256[]', 73 | name: 'values', 74 | type: 'uint256[]', 75 | }, 76 | ], 77 | name: 'TransferBatch', 78 | type: 'event', 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/abi/erc-721.ts: -------------------------------------------------------------------------------- 1 | import { AbiItem } from 'web3-utils'; 2 | 3 | export const ERC721ABI: Record<'transfer', AbiItem> = { 4 | transfer: { 5 | anonymous: false, 6 | inputs: [ 7 | { 8 | indexed: true, 9 | internalType: 'address', 10 | name: 'from', 11 | type: 'address', 12 | }, 13 | { 14 | indexed: true, 15 | internalType: 'address', 16 | name: 'to', 17 | type: 'address', 18 | }, 19 | { 20 | indexed: true, 21 | internalType: 'uint256', 22 | name: 'tokenId', 23 | type: 'uint256', 24 | }, 25 | ], 26 | name: 'Transfer', 27 | type: 'event', 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/abi/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crypto-punk'; 2 | export * from './erc-1155'; 3 | export * from './erc-721'; 4 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rate-limiter'; 2 | export * from './utils'; 3 | -------------------------------------------------------------------------------- /src/common/rate-limiter.ts: -------------------------------------------------------------------------------- 1 | import { RateLimiter } from 'limiter'; 2 | export class ExternalServiceRateLimiter { 3 | limiter: RateLimiter; 4 | apiKey: string; 5 | 6 | constructor(apiKey: string, limiter: RateLimiter) { 7 | this.apiKey = apiKey; 8 | this.limiter = limiter; 9 | } 10 | async getAPIKeyWithRateLimiter(): Promise { 11 | await this.limiter.removeTokens(1); 12 | return this.apiKey; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/common/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sleep'; 2 | -------------------------------------------------------------------------------- /src/common/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(timeout: number) { 2 | return new Promise(resolve => { 3 | setTimeout(() => { 4 | resolve(undefined); 5 | }, timeout); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /src/exceptions/aggregator.ts: -------------------------------------------------------------------------------- 1 | import { BaseException, ExceptionType } from './base'; 2 | 3 | enum ApiExceptionType { 4 | API_KEY_ERROR = 'api_key_error', 5 | API_CHAIN_ERROR = 'api_chain_error', 6 | REQUEST_ERROR = 'request_error', 7 | SIGNATURE_ERROR = 'signature_error', 8 | MARKETPLACE_ERROR = 'marketplace_error', 9 | } 10 | 11 | /** 12 | * NFTGo API's wrapper error object 13 | */ 14 | export class AggregatorApiException extends BaseException { 15 | constructor(public code: number | string, public message: string = '', public url?: string) { 16 | super(code, message); 17 | } 18 | 19 | static missApiKeyError() { 20 | return new AggregatorApiException(ApiExceptionType.API_KEY_ERROR, this.missingParam('apiKey')); 21 | } 22 | 23 | static invalidLimitError(url: string, max: number) { 24 | return new AggregatorApiException(ExceptionType.PARAM_ERROR, this.invalidParam('limit', `capped at ${max}`), url); 25 | } 26 | 27 | static requestError(url: string, msg: string) { 28 | return new AggregatorApiException(ApiExceptionType.REQUEST_ERROR, msg, url); 29 | } 30 | 31 | static apiEmptyResponseError(url: string) { 32 | return new AggregatorApiException(ExceptionType.RESPONSE_DATA_EMPTY, 'response is empty', url); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/exceptions/base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NFTGo SDK's base error types 3 | */ 4 | export enum ExceptionType { 5 | PARAM_ERROR = 'param_error', 6 | RESPONSE_DATA_EMPTY = 'response_data_empty', 7 | EXTERNAL_SERVICE_ERROR = 'external_service_error', 8 | } 9 | /** 10 | * NFTGo SDK's base wrapper error object 11 | */ 12 | 13 | export class BaseException extends Error { 14 | constructor(public code: number | string, public message: string = '') { 15 | super(message); 16 | } 17 | 18 | static httpUnsuccessfulResponse(msg: string) { 19 | return new BaseException(ExceptionType.EXTERNAL_SERVICE_ERROR, msg); 20 | } 21 | 22 | static httpRequestError(msg: string) { 23 | return new BaseException(ExceptionType.EXTERNAL_SERVICE_ERROR, msg); 24 | } 25 | 26 | static missingParam(paramName: string) { 27 | return `The param '${paramName}' is required.`; 28 | } 29 | 30 | static invalidParam(paramName: string, extMsg?: string) { 31 | return `The param '${paramName}' is invalid. ${extMsg || ''}`; 32 | } 33 | 34 | static invalidParamError(paramName: string, extMsg?: string) { 35 | return new BaseException(ExceptionType.PARAM_ERROR, this.invalidParam(paramName, extMsg)); 36 | } 37 | 38 | static paramErrorDefault(msg?: string) { 39 | return new BaseException(ExceptionType.PARAM_ERROR, msg); 40 | } 41 | 42 | static missingParamError(paramName: string) { 43 | return new BaseException(ExceptionType.PARAM_ERROR, this.missingParam(paramName)); 44 | } 45 | 46 | static emptyResponseError() { 47 | return new BaseException(ExceptionType.RESPONSE_DATA_EMPTY, 'response is empty'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aggregator'; 2 | export * from './utils'; 3 | export * from './base'; 4 | -------------------------------------------------------------------------------- /src/exceptions/utils.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionType, BaseException } from './base'; 2 | 3 | enum UtilsExceptionType { 4 | DECODE_LOG_ERROR = 'decode_log_error', 5 | } 6 | 7 | export class UtilsException extends BaseException { 8 | constructor(public code: number | string, public message: string = '') { 9 | super(message); 10 | } 11 | static initApiFirst() { 12 | return new UtilsException(ExceptionType.PARAM_ERROR, this.missingParam('api instance')); 13 | } 14 | static provideProviderFirst() { 15 | return new UtilsException(ExceptionType.PARAM_ERROR, this.missingParam('provider')); 16 | } 17 | 18 | static decodeLogError(msg?: string) { 19 | return new UtilsException(UtilsExceptionType.DECODE_LOG_ERROR, msg); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/http/client.ts: -------------------------------------------------------------------------------- 1 | import { AggregatorApiException } from '@/exceptions'; 2 | 3 | import { HTTPClient } from '@/types'; 4 | import { Agent } from 'https'; 5 | import { SafeAny } from 'src/types/safe-any'; 6 | 7 | export class HTTPClientStable implements HTTPClient { 8 | constructor(private agent?: Agent) {} 9 | fetch(input: RequestInfo | URL, init?: RequestInit | undefined) { 10 | const agentOption = this.agent ? { agent: this.agent } : {}; 11 | return new Promise((resolve, reject) => { 12 | fetch(input, { ...init, ...agentOption }) 13 | .then(async res => { 14 | if (!isHttpResponseSuccess(res.status)) { 15 | reject( 16 | new AggregatorApiException( 17 | res.status, 18 | res.statusText?.length > 0 ? JSON.stringify(await res.json()) : res.statusText, 19 | res.url 20 | ) 21 | ); 22 | } 23 | return res.json(); 24 | }) 25 | .catch(e => { 26 | reject(AggregatorApiException.requestError(input?.toString(), e)); 27 | }) 28 | .then(res => { 29 | if (!res) { 30 | reject(AggregatorApiException.apiEmptyResponseError(input?.toString())); 31 | } else { 32 | resolve(res); 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | get(url: string, query: Q | undefined, headers: Record): Promise { 39 | const params = []; 40 | let actualUrl = url; 41 | for (const key in query) { 42 | if (query[key] instanceof Array) { 43 | for (const value of query[key] as unknown as Array) { 44 | value !== null && value !== undefined && params.push(`${key}=${value}`); 45 | } 46 | } else { 47 | query[key] !== null && query[key] !== undefined && params.push(`${key}=${query[key]}`); 48 | } 49 | } 50 | if (params.length !== 0) { 51 | actualUrl = `${url}?${params.join('&')}`; 52 | } 53 | return this.fetch(actualUrl, { headers, method: 'GET' }); 54 | } 55 | 56 | post(url: string, data: P, headers?: Record): Promise { 57 | const body = JSON.stringify(data); 58 | return this.fetch(url, { 59 | method: 'POST', 60 | body: body, 61 | headers: { ...headers, 'Content-Type': 'application/json' }, 62 | }); 63 | } 64 | } 65 | 66 | function isHttpResponseSuccess(status: number): boolean { 67 | return status >= 200 && status < 300; 68 | } 69 | -------------------------------------------------------------------------------- /src/http/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modules'; 2 | export * from '@/types'; 3 | export * from './exceptions'; 4 | -------------------------------------------------------------------------------- /src/modules/aggregator/index.ts: -------------------------------------------------------------------------------- 1 | import { AggregatorApiException } from '@/exceptions'; 2 | import { 3 | Config, 4 | HTTPClient, 5 | Utils, 6 | AggregatorApiResponse, 7 | AggregatorApiStatusResponse, 8 | AggregatorInterface, 9 | AggregatorResponse, 10 | CancelOrdersReq, 11 | CreateListingsReq, 12 | CreateOffersReq, 13 | FulfillListingsReq, 14 | FulfillOffersReq, 15 | } from '@/types'; 16 | 17 | export class Aggregator implements AggregatorInterface { 18 | constructor(private client: HTTPClient, private config: Config, private utils: Utils) {} 19 | 20 | /** 21 | * 22 | * - details: {@link } 23 | * @param params {@link } 24 | * @returns Promise<{@link }> 25 | */ 26 | createOffers = async (params: CreateOffersReq): Promise => { 27 | const res = await this.post('/create-offers', params); 28 | return this.response(res); 29 | }; 30 | 31 | /** 32 | * 33 | * - details: {@link } 34 | * @param params {@link any} 35 | * @returns Promise<{@link any}> 36 | */ 37 | fulfillOffers = async (params: FulfillOffersReq): Promise => { 38 | const res = await this.post('/fulfill-offers', params); 39 | return this.response(res); 40 | }; 41 | 42 | /** 43 | * 44 | * - details: {@link } 45 | * @param params {@link any} 46 | * @returns Promise<{@link any}> 47 | */ 48 | cancelOrders = async (params: CancelOrdersReq): Promise => { 49 | const res = await this.post('/cancel-orders', params); 50 | 51 | return this.response(res); 52 | }; 53 | 54 | /** 55 | * 56 | * - details: {@link } 57 | * @param params {@link any} 58 | * @returns Promise<{@link any}> 59 | */ 60 | createListings = async (params: CreateListingsReq): Promise => { 61 | const data = await this.post('/create-listings', params); 62 | 63 | return this.response(data); 64 | }; 65 | 66 | /** 67 | * buy nfts 68 | * - details: {@link } 69 | * @param params {@link FulfillListingsReq} 70 | * @returns Promise<{@link }> 71 | */ 72 | fulfillListings = async (params: FulfillListingsReq): Promise => { 73 | const data = await this.post('/fulfill-listings', params); 74 | return this.response(data); 75 | }; 76 | 77 | private get headers(): Record { 78 | return { 'X-API-KEY': this.config.apiKey!, 'X-FROM': 'js_sdk' }; 79 | } 80 | 81 | private get url() { 82 | return this.config.baseUrl + '/trade' + '/v1' + '/nft'; 83 | } 84 | 85 | private async post(path: string, params: Req) { 86 | const url = `${this.url}${path}?chain=${this.config.chain}`; 87 | const response = await this.client.post, Req>(url, params, this.headers); 88 | const { code, msg, data } = response; 89 | if (code === 'SUCCESS') { 90 | return data; 91 | } else { 92 | throw new AggregatorApiException(msg, code, path); 93 | } 94 | } 95 | 96 | private response(data: AggregatorApiResponse): AggregatorResponse { 97 | const executor = this.utils.createActionExecutor(data.actions); 98 | const executeActions = executor.execute; 99 | const response: AggregatorResponse = { 100 | ...data, 101 | executeActions, 102 | }; 103 | 104 | return response; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/modules/config/index.ts: -------------------------------------------------------------------------------- 1 | import { Config, EVMChain } from '@/types'; 2 | 3 | export function ensureConfig(option: Partial): Config { 4 | const baseUrl = 'https://data-api.nftgo.io'; 5 | 6 | const chain = EVMChain.ETHEREUM; 7 | 8 | const config: Config = { 9 | baseUrl, 10 | chain, 11 | ...option, 12 | }; 13 | 14 | // TODO: field validation such as chian and base url 15 | // e.g. config.baseUrl.test(/^https:\/\/*/,) 16 | 17 | return config; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { Config, GoTrading } from '@/types'; 2 | import { OrderFetcher } from './order-fetcher'; 3 | import { Aggregator } from './aggregator'; 4 | import { HTTPClientStable } from '@/http'; 5 | import { createUtils } from './utils'; 6 | import { ensureConfig } from './config'; 7 | export type Option = Partial; 8 | export function init(option: Option): GoTrading { 9 | const config = ensureConfig(option); 10 | 11 | const httpClient = new HTTPClientStable(option?.agent); 12 | 13 | const orderFetcher = new OrderFetcher(httpClient, config); 14 | 15 | const utils = createUtils(config, httpClient); 16 | 17 | const aggregator = new Aggregator(httpClient, config, utils); 18 | 19 | const goTrading: GoTrading = Object.freeze({ 20 | orderFetcher, 21 | aggregator, 22 | utils, 23 | config, 24 | }); 25 | 26 | return goTrading; 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/order-fetcher/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetOrdersByContractReq, 3 | GetOrdersByIdsReq, 4 | GetOrdersByMakerReq, 5 | GetOrdersByNftsReq, 6 | OrderFetcherInterface, 7 | OrdersFetcherResp, 8 | HTTPClient, 9 | Config, 10 | OrderFetcherApiResponse, 11 | } from '@/types'; 12 | 13 | export class OrderFetcher implements OrderFetcherInterface { 14 | constructor(private client: HTTPClient, private config: Config) {} 15 | 16 | getOrdersByContract = (params: GetOrdersByContractReq): Promise => { 17 | return this.post('/get-orders-by-contract', params); 18 | }; 19 | 20 | getOrdersByNFT = (params: GetOrdersByNftsReq): Promise => { 21 | return this.post('/get-orders-by-nft', params); 22 | }; 23 | 24 | getOrdersByIds = (params: GetOrdersByIdsReq): Promise => { 25 | return this.post('/get-orders-by-ids', params); 26 | }; 27 | 28 | getOrdersByMaker = (params: GetOrdersByMakerReq): Promise => { 29 | return this.post('/get-orders-by-maker', params); 30 | }; 31 | 32 | private get headers() { 33 | return { 'X-API-KEY': this.config.apiKey!, 'X-FROM': 'js_sdk' }; 34 | } 35 | 36 | private get url() { 37 | return this.config.baseUrl + '/orderbook' + '/v1' + '/orders'; 38 | } 39 | 40 | private async post(path: string, params: P) { 41 | const response = await this.client.post, P>( 42 | `${this.url}${path}?chain=${this.config.chain}`, 43 | params, 44 | this.headers 45 | ); 46 | return response.data; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/utils/action/executor/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionTaskExecutor, ActionTask, ExecuteOptions, AggregatorAction, ActionProcessor, ActionKind } from '@/types'; 2 | import { createTask } from '../task'; 3 | 4 | export class BrowserActionTaskExecutor implements ActionTaskExecutor { 5 | private tasks: ActionTask[] = []; 6 | 7 | constructor(actions: AggregatorAction[], public processor: ActionProcessor) { 8 | this.pushTask(actions); 9 | } 10 | 11 | private pushTask = (actions: AggregatorAction[]) => { 12 | let index = this.tasks.length; 13 | for (const action of actions) { 14 | const task = createTask({ action, index, processor: this.processor, updateTask: this.pushTask }); 15 | if (index !== 0) { 16 | task.pre = this.tasks[index - 1]; 17 | } 18 | this.tasks.push(task); 19 | index++; 20 | } 21 | }; 22 | 23 | execute = async (option?: ExecuteOptions) => { 24 | const handle = option?.onTaskExecuted; 25 | for (const task of this.tasks) { 26 | await task.execute(); 27 | handle?.(task); 28 | } 29 | }; 30 | 31 | *[Symbol.iterator]() { 32 | for (const task of this.tasks) { 33 | yield task; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/utils/action/index.ts: -------------------------------------------------------------------------------- 1 | export * from './executor'; 2 | -------------------------------------------------------------------------------- /src/modules/utils/action/processor/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sign-info'; 2 | export * from './sign-order-data'; 3 | -------------------------------------------------------------------------------- /src/modules/utils/action/processor/common/sign-info.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from '@/types'; 2 | import { SafeAny } from 'src/types/safe-any'; 3 | 4 | type SendTransactionFn = (params: SafeAny) => Transaction; 5 | 6 | export async function signInfo(params: SafeAny, sendTransaction: SendTransactionFn): Promise { 7 | return new Promise((resolve, reject) => { 8 | sendTransaction(params) 9 | .on('error', err => { 10 | reject(new Error(err.message)); 11 | }) 12 | .on('receipt', receipt => { 13 | const error = receipt?.logs.length === 0 || !receipt?.status; 14 | if (error) { 15 | reject(new Error('approved sign failed')); 16 | } else { 17 | resolve(true); 18 | } 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/utils/action/processor/common/sign-order-data.ts: -------------------------------------------------------------------------------- 1 | import { SignData } from '@/types'; 2 | import { arrayify } from 'ethers/lib/utils'; 3 | import { Signer } from 'ethers'; 4 | import { TypedDataSigner } from '@ethersproject/abstract-signer'; 5 | 6 | export async function signOrderData(data: SignData, signer: Signer & TypedDataSigner): Promise { 7 | const { domain, types, value } = data; 8 | 9 | let signature = '0x0000000000000000000000000000000000000000000000000000000000000000'; 10 | if (signer) { 11 | if (data.signatureKind === 'eip191') { 12 | if (data.message?.match(/0x[0-9a-fA-F]{64}/)) { 13 | // If the message represents a hash, we need to convert it to raw bytes first 14 | signature = await signer.signMessage(arrayify(data.message)); 15 | } else { 16 | signature = await signer.signMessage(data.message ?? ''); 17 | } 18 | } else if (data.signatureKind === 'eip712') { 19 | signature = await signer._signTypedData(domain, types, value); 20 | } 21 | } 22 | return signature; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/utils/action/processor/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionKind, 3 | ActionName, 4 | ActionProcessor, 5 | AggregatorAction, 6 | AggregatorApiResponse, 7 | AggregatorApiStatusResponse, 8 | Config, 9 | HTTPClient, 10 | InternalUtils, 11 | ProcessPassThroughActionParams, 12 | } from '@/types'; 13 | import { signInfo, signOrderData } from './common'; 14 | import { PostOrderHandler } from '../../post-order'; 15 | 16 | export class AggregateActionProcessor implements ActionProcessor { 17 | private postOrderHandler: PostOrderHandler; 18 | constructor(private utils: InternalUtils, private config: Config, private client: HTTPClient) { 19 | this.postOrderHandler = new PostOrderHandler(config); 20 | } 21 | 22 | async processSignatureAction(action: AggregatorAction) { 23 | const { name, data } = action; 24 | if (name === 'order-signature') { 25 | const { sign } = data; 26 | if (!sign) { 27 | throw new Error('sign is required'); 28 | } 29 | const signature = await signOrderData(sign, this.utils.getSigner()); 30 | return signature; 31 | } 32 | return Promise.reject(new Error('no match action name')); 33 | } 34 | 35 | async processTransactionAction(action: AggregatorAction) { 36 | const { name, data } = action; 37 | const { txData, safeMode } = data; 38 | if (!txData) { 39 | throw new Error('txData is required'); 40 | } 41 | if (name === 'nft-approval') { 42 | return await signInfo(txData, this.utils.sendTransaction); 43 | } else if (name === 'accept-listing') { 44 | if (safeMode) { 45 | return await signInfo(txData, this.utils.sendSafeModeTransaction); 46 | } else { 47 | return await signInfo(txData, this.utils.sendTransaction); 48 | } 49 | // other name case: currency-wrapping currency-approval 50 | } else { 51 | return await signInfo(txData, this.utils.sendTransaction); 52 | } 53 | } 54 | 55 | async processPassThroughAction( 56 | action: AggregatorAction, 57 | params: ProcessPassThroughActionParams 58 | ) { 59 | const { name, data } = action; 60 | 61 | if (name === ActionName.PostOrderToMarketplace) { 62 | if (!params.signature) { 63 | throw new Error('action signature is required'); 64 | } 65 | const { payload, endpoint } = data; 66 | const postOrderResult = await this.postOrderHandler.handle(payload, params.signature, endpoint); 67 | return postOrderResult; 68 | } 69 | return Promise.resolve({ 70 | status: 'success', 71 | name, 72 | }); 73 | } 74 | 75 | async processControllerAction( 76 | action: AggregatorAction 77 | ): Promise[]> { 78 | const { data } = action; 79 | const { payload, endpoint, method } = data; 80 | 81 | const url = `${this.config.baseUrl}${endpoint}`; 82 | 83 | const clientMethod = method.toLowerCase() as Lowercase; 84 | 85 | const response = await this.client[clientMethod]>(url, payload); 86 | 87 | return response.data.actions; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/modules/utils/action/readme.md: -------------------------------------------------------------------------------- 1 | # .. 2 | 3 | create offer / listing 4 | 5 | accept offer / listing 6 | 7 | cancel orders (both offer & listing) 8 | 9 | 10 | # Usage 11 | ```ts 12 | const { actions, executeAllActions } = await createListing(); 13 | 14 | // way.1 15 | const executor = utils.createActionExecutor(actions); 16 | executor.execute({ 17 | onTaskExecuted(task){ 18 | console.log(task.action, task.index, task.status) 19 | } 20 | }) 21 | 22 | // way.2 23 | for(const task of executor) { 24 | await task.execute(); 25 | console.log(task.action, task.index, task.status) 26 | } 27 | 28 | // way.3 same as way.1 29 | executeAllActions({ 30 | onTaskExecuted(task) { 31 | console.log(task.action, task.index, task.status) 32 | } 33 | }) 34 | 35 | // way.4 same as way.2 36 | const tasks = utils.createActionTasks(actions); 37 | for(const task of tasks) { 38 | await task.execute(); 39 | console.log(task.action, task.index, task.status) 40 | } 41 | 42 | ``` 43 | 44 | for way.2 and way.4 you have more freedom to customize workflow 45 | 46 | such as 47 | 48 | ```ts 49 | 50 | const { actions } = await createListing(); 51 | 52 | const executor = utils.createActionExecutor(actions); 53 | for(const task of executor) { 54 | if(task.action.kind === TradeActionKind.Signature) { 55 | const signature = 'do your own logic to get user signature'; 56 | task.result = { 57 | signature 58 | } 59 | continue; 60 | } 61 | await task.execute(); 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /src/modules/utils/action/task/controller.ts: -------------------------------------------------------------------------------- 1 | import { ActionKind, AggregatorAction } from '@/types'; 2 | import { ActionTaskTemplate } from './template'; 3 | 4 | export class ControllerActionTask extends ActionTaskTemplate { 5 | updateTask?: (action: AggregatorAction[]) => void; 6 | 7 | protected run = async () => { 8 | const actions = await this.processor.processControllerAction(this.action); 9 | this.updateTask?.(actions); 10 | return null; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/utils/action/task/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionTask, AggregatorAction, ActionKind, ActionProcessor } from '@/types'; 2 | import { PassThroughActionTask } from './pass-through'; 3 | import { SignatureActionTask } from './signature'; 4 | import { TransactionActionTask } from './transaction'; 5 | import { ControllerActionTask } from './controller'; 6 | 7 | export type CreateTaskOption = { 8 | action: AggregatorAction; 9 | index: number; 10 | processor: ActionProcessor; 11 | updateTask?: (actions: AggregatorAction[]) => void; 12 | }; 13 | 14 | export function createTask(option: CreateTaskOption): ActionTask { 15 | const { action, index, processor } = option; 16 | switch (action.kind) { 17 | case ActionKind.PassThrough: 18 | return new PassThroughActionTask(action, index, processor); 19 | case ActionKind.Transaction: 20 | return new TransactionActionTask(action, index, processor); 21 | case ActionKind.Signature: 22 | return new SignatureActionTask(action, index, processor); 23 | case ActionKind.Controller: { 24 | const task = new ControllerActionTask(action, index, processor); 25 | task.updateTask = option.updateTask; 26 | return task; 27 | } 28 | default: 29 | throw Error('Unknown action kind'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/utils/action/task/pass-through.ts: -------------------------------------------------------------------------------- 1 | import { ActionKind, ProcessPassThroughActionParams } from '@/types'; 2 | import { ActionTaskTemplate } from './template'; 3 | 4 | export class PassThroughActionTask extends ActionTaskTemplate { 5 | protected run = async () => { 6 | let pre = this.pre; 7 | while (pre) { 8 | // when can we use it for post-order 9 | // condition 1: it's a signature kind action 10 | // condition 2: it's orderIndexes must include passthrough order index 11 | if (pre.action.kind === ActionKind.Signature) { 12 | const { orderIndexes } = pre.action.data; 13 | const result = pre.result as ProcessPassThroughActionParams; 14 | const needThisSignature = this.action.data.orderIndexes.every(index => orderIndexes.includes(index)); 15 | if (needThisSignature) { 16 | return await this.processor.processPassThroughAction(this.action, result); 17 | } 18 | } 19 | pre = pre.pre; 20 | } 21 | throw new Error('Can not found signature for post order'); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/utils/action/task/signature.ts: -------------------------------------------------------------------------------- 1 | import { ActionKind, ProcessPassThroughActionParams } from '@/types'; 2 | import { ActionTaskTemplate } from './template'; 3 | 4 | export class SignatureActionTask extends ActionTaskTemplate { 5 | protected run = async () => { 6 | const signature = await this.processor.processSignatureAction(this.action); 7 | 8 | const result: ProcessPassThroughActionParams = { 9 | signature, 10 | }; 11 | 12 | return result; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/utils/action/task/template.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionTask, 3 | ActionTaskStatus, 4 | AggregatorAction, 5 | ActionKind, 6 | ActionProcessor, 7 | ProcessPassThroughActionParams, 8 | } from '@/types'; 9 | 10 | export abstract class ActionTaskTemplate implements ActionTask { 11 | status: ActionTaskStatus = 'ready'; 12 | pre: ActionTask | null = null; 13 | error: Error | null = null; 14 | result: ProcessPassThroughActionParams | null = null; 15 | constructor(public action: AggregatorAction, public index: number, protected processor: ActionProcessor) {} 16 | execute = async () => { 17 | try { 18 | this.result = await this.run(); 19 | this.status = 'success'; 20 | } catch (e: unknown) { 21 | this.error = e as Error; 22 | this.status = 'fail'; 23 | } 24 | }; 25 | protected abstract run(): Promise; 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/utils/action/task/transaction.ts: -------------------------------------------------------------------------------- 1 | import { ActionKind } from '@/types'; 2 | import { ActionTaskTemplate } from './template'; 3 | 4 | export class TransactionActionTask extends ActionTaskTemplate { 5 | protected run = async () => { 6 | return await this.processor.processTransactionAction(this.action); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/utils/blur-auth.ts: -------------------------------------------------------------------------------- 1 | import { BaseException } from '@/exceptions'; 2 | import { HTTPClient, Config, AggregatorApiStatusResponse, BlurAuthenticator, BlurAuthenticatorParams } from '@/types'; 3 | 4 | interface BlurAuthChallenge { 5 | expiresOn: string; 6 | hmac: string; 7 | message: string; 8 | walletAddress: string; 9 | } 10 | 11 | interface BlurAuthLoginParams extends BlurAuthChallenge { 12 | signature: string; 13 | } 14 | 15 | interface Signer { 16 | signMessage: (message: string) => Promise; 17 | } 18 | 19 | export class BlurMarketAuthenticator implements BlurAuthenticator { 20 | private accessToken: string | undefined; 21 | private signer: Signer; 22 | private httpClient: HTTPClient; 23 | private config: Config; 24 | 25 | constructor(signer: Signer, httpClient: HTTPClient, config: Config) { 26 | this.signer = signer; 27 | this.httpClient = httpClient; 28 | this.config = config; 29 | } 30 | private get headers() { 31 | return { 'X-API-KEY': this.config.apiKey, 'X-FROM': 'js_sdk' } as Record; 32 | } 33 | 34 | private get url() { 35 | return this.config.baseUrl + '/utils/v1/blur'; 36 | } 37 | 38 | private async getAuthSignature(message: string) { 39 | const signature = this.signer.signMessage(message); 40 | return signature; 41 | } 42 | 43 | private async getAuthChallenge(address: string) { 44 | const { code, msg, data } = await this.httpClient.post< 45 | AggregatorApiStatusResponse, 46 | { address: string } 47 | >( 48 | this.url + '/get-auth-challenge', 49 | { 50 | address, 51 | }, 52 | this.headers 53 | ); 54 | if (code !== 'SUCCESS') { 55 | throw new BaseException('get challenge failed:', msg); 56 | } 57 | return data; 58 | } 59 | 60 | private async signBlurAuthChallenge(params: BlurAuthLoginParams): Promise { 61 | const { code, msg, data } = await this.httpClient.post< 62 | AggregatorApiStatusResponse<{ blurAuth: string }>, 63 | BlurAuthLoginParams 64 | >(this.url + '/get-auth', params, this.headers); 65 | if (code !== 'SUCCESS') { 66 | throw new BaseException('get blur auth failed:', msg); 67 | } 68 | return data?.blurAuth; 69 | } 70 | authorize = async ({ address, force }: BlurAuthenticatorParams) => { 71 | if (!address) { 72 | throw new BaseException('address is required'); 73 | } 74 | if (this.accessToken && !force) { 75 | return this.accessToken; 76 | } 77 | const challenge = await this.getAuthChallenge(address); 78 | const { message } = challenge; 79 | const signature = await this.getAuthSignature(message); 80 | const token = await this.signBlurAuthChallenge({ 81 | ...challenge, 82 | signature, 83 | }); 84 | this.accessToken = token; 85 | return token; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/modules/utils/consts.ts: -------------------------------------------------------------------------------- 1 | export const PUNK_CONTRACT_ADDRESS = '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb'; 2 | -------------------------------------------------------------------------------- /src/modules/utils/ethereum/index.ts: -------------------------------------------------------------------------------- 1 | import { UtilsException } from '@/exceptions'; 2 | import { Config } from '@/types'; 3 | import Web3 from 'web3'; 4 | import { ethers } from 'ethers'; 5 | 6 | // TODO: use chain lib instate of specific lib 7 | export class EthereumLib { 8 | web3?: Web3; 9 | ethersWallet?: ethers.providers.Web3Provider; 10 | ethersSigner?: ethers.Signer; 11 | 12 | constructor(private config: Config) { 13 | const provider = this.config.web3Provider ?? Reflect.get(globalThis, 'ethereum'); 14 | const walletConfig = this.config?.walletConfig; 15 | if (provider) { 16 | this.web3 = new Web3(provider); 17 | 18 | this.ethersWallet = new ethers.providers.Web3Provider(provider); 19 | if (walletConfig) { 20 | if (typeof walletConfig.address !== 'string') { 21 | throw UtilsException.invalidParamError('walletConfig.address'); 22 | } 23 | if (typeof walletConfig.privateKey !== 'string') { 24 | throw UtilsException.invalidParamError('walletConfig.privateKey'); 25 | } 26 | this.ethersSigner = new ethers.Wallet( 27 | walletConfig.privateKey, 28 | new ethers.providers.JsonRpcProvider(provider.host) 29 | ); 30 | this.web3.eth.accounts.wallet.add(walletConfig); 31 | } else { 32 | this.ethersSigner = this.ethersWallet.getSigner(0); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionKind, AggregatorAction, Config, HTTPClient, Utils } from '@/types'; 2 | import { InternalAggregatorUtils } from './internal-utils'; 3 | import { BrowserActionTaskExecutor } from './action'; 4 | import { AggregateActionProcessor } from './action/processor'; 5 | 6 | export function createUtils(config: Config, http: HTTPClient): Utils { 7 | const internalUtils = new InternalAggregatorUtils(config, http); 8 | const processor = new AggregateActionProcessor(internalUtils, config, http); 9 | 10 | internalUtils.createActionExecutor = (actions: AggregatorAction[]) => { 11 | return new BrowserActionTaskExecutor(actions, processor); 12 | }; 13 | internalUtils.processor = processor; 14 | const utils = internalUtils as Utils; 15 | return utils; 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/utils/internal-utils.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, ethers } from 'ethers'; 2 | import Web3 from 'web3'; 3 | 4 | import { Log, provider, TransactionConfig } from 'web3-core'; 5 | import { PUNK_CONTRACT_ADDRESS } from './consts'; 6 | 7 | import { ERC721ABI, CryptoPunkABI, ERC1155ABI } from '@/abi'; 8 | import { 9 | WalletConfig, 10 | InspectTransactionParams, 11 | TransactionHashHandler, 12 | ReceiptHandler, 13 | ErrorHandler, 14 | FinallyHandler, 15 | Transaction, 16 | ActionTaskExecutor, 17 | Config, 18 | AggregatorAction, 19 | InternalUtils, 20 | HTTPClient, 21 | BlurAuthenticator, 22 | X2Y2Authenticator, 23 | ActionProcessor, 24 | } from '@/types'; 25 | import { UtilsException } from '@/exceptions'; 26 | import { BlurMarketAuthenticator } from './blur-auth'; 27 | import { X2Y2MarketplaceAuthenticator } from './x2y2-auth'; 28 | import { SafeAny } from 'src/types/safe-any'; 29 | 30 | export class InternalAggregatorUtils implements InternalUtils { 31 | private provider?: provider; 32 | private walletConfig?: WalletConfig; 33 | public _ethersProvider: ethers.providers.Web3Provider; 34 | public _ethersSigner: SafeAny; 35 | public _web3Instance: Web3; 36 | public account: string | undefined = this.walletConfig?.address; 37 | public blurAccessToken: string | undefined; 38 | private TRANSFER_TOPIC: string; 39 | private TRANSFER_BATCH_TOPIC: string; 40 | private TRANSFER_SINGLE_TOPIC: string; 41 | private PUNK_TRANSFER_TOPIC: string; 42 | private PUNK_BOUGHT_TOPIC: string; 43 | public blurAuthenticator: BlurAuthenticator; 44 | public x2y2Authenticator: X2Y2Authenticator; 45 | public processor?: ActionProcessor | undefined; 46 | constructor(config: Config, client: HTTPClient) { 47 | this.provider = config.web3Provider; 48 | this.walletConfig = config.walletConfig; 49 | 50 | this._web3Instance = new Web3(this.provider || (globalThis as SafeAny)?.ethereum); 51 | this.x2y2Authenticator = new X2Y2MarketplaceAuthenticator(this._web3Instance); 52 | 53 | this._ethersProvider = new ethers.providers.Web3Provider(this.provider || (globalThis as SafeAny)?.ethereum); 54 | if (this.walletConfig) { 55 | if (typeof this.walletConfig?.address !== 'string') { 56 | throw UtilsException.invalidParamError('walletConfig.address'); 57 | } 58 | if (typeof this.walletConfig?.privateKey !== 'string') { 59 | throw UtilsException.invalidParamError('walletConfig.privateKey'); 60 | } 61 | 62 | this._ethersSigner = this.provider 63 | ? new ethers.Wallet( 64 | this.walletConfig.privateKey, 65 | new ethers.providers.JsonRpcProvider((this.provider as SafeAny).host) 66 | ) 67 | : this._ethersProvider.getSigner(this.walletConfig?.address); 68 | 69 | this._web3Instance.eth.accounts.wallet.add(this.walletConfig as WalletConfig); 70 | } 71 | this.blurAuthenticator = new BlurMarketAuthenticator(this._ethersSigner, client, config); 72 | this.TRANSFER_TOPIC = this._web3Instance?.eth.abi.encodeEventSignature(ERC721ABI.transfer); 73 | this.TRANSFER_BATCH_TOPIC = this._web3Instance.eth.abi.encodeEventSignature(ERC1155ABI.batchTransfer); 74 | this.TRANSFER_SINGLE_TOPIC = this._web3Instance.eth.abi.encodeEventSignature(ERC1155ABI.singleTransfer); 75 | this.PUNK_TRANSFER_TOPIC = this._web3Instance.eth.abi.encodeEventSignature(CryptoPunkABI.transfer); 76 | this.PUNK_BOUGHT_TOPIC = this._web3Instance.eth.abi.encodeEventSignature(CryptoPunkABI.bought); 77 | } 78 | 79 | createActionExecutor?: (actions: AggregatorAction[]) => ActionTaskExecutor; 80 | 81 | inspectTransaction = ({ hash, interval = 1000 }: InspectTransactionParams) => { 82 | const transactionInstance = new SendTransaction(); 83 | const intervalId = setInterval(async () => { 84 | try { 85 | const res = await this._web3Instance?.eth?.getTransactionReceipt(hash); 86 | if (res === null) { 87 | return; 88 | } 89 | clearInterval(intervalId); 90 | transactionInstance.receiptHandler?.(res); 91 | } catch (error) { 92 | transactionInstance.errorHandler?.(error as Error); 93 | } finally { 94 | transactionInstance.finally?.(); 95 | } 96 | }, interval); 97 | return transactionInstance; 98 | }; 99 | 100 | decodeLog = (log: Log) => { 101 | if (!log) { 102 | throw UtilsException.decodeLogError('log is empty'); 103 | } 104 | let contract; 105 | let tokenId; 106 | let amount = 0; 107 | let is1155 = false; 108 | const topic = log?.topics?.[0]; 109 | let decodedEventLog; 110 | let to; 111 | try { 112 | switch (topic) { 113 | case this.TRANSFER_TOPIC: 114 | if (log.address.toLowerCase() === PUNK_CONTRACT_ADDRESS) { 115 | // A punk sale will trigger both Transfer721 and PunkBought. We take PunkBought into count rather than Transfer721. 116 | break; 117 | } else { 118 | // 721 119 | decodedEventLog = this._web3Instance.eth.abi.decodeLog( 120 | ERC721ABI.transfer.inputs ?? [], 121 | log.data, 122 | log.topics.slice(1) // without the topic[0] if its a non-anonymous event, otherwise with topic[0]. 123 | ); 124 | contract = log.address; 125 | tokenId = decodedEventLog.tokenId.toString(); 126 | to = decodedEventLog.to; 127 | amount = 1; 128 | } 129 | break; 130 | case this.TRANSFER_BATCH_TOPIC: 131 | // batch 1155 132 | decodedEventLog = this._web3Instance.eth.abi.decodeLog( 133 | ERC1155ABI.batchTransfer.inputs ?? [], 134 | log.data, 135 | log.topics.slice(1) 136 | ); 137 | contract = log.address; 138 | tokenId = decodedEventLog.ids[0]; 139 | amount = parseInt(decodedEventLog.value); // value代表成交的数量 140 | to = decodedEventLog.to; 141 | is1155 = true; 142 | break; 143 | case this.TRANSFER_SINGLE_TOPIC: 144 | // single 1155 145 | decodedEventLog = this._web3Instance.eth.abi.decodeLog( 146 | ERC1155ABI.singleTransfer.inputs ?? [], 147 | log.data, 148 | log.topics.slice(1) 149 | ); 150 | contract = log.address; 151 | tokenId = decodedEventLog.id.toString(); 152 | to = decodedEventLog.to; 153 | is1155 = true; 154 | amount = 1; 155 | break; 156 | case this.PUNK_TRANSFER_TOPIC: 157 | // punk 158 | decodedEventLog = this._web3Instance.eth.abi.decodeLog( 159 | CryptoPunkABI.transfer.inputs ?? [], 160 | log.data, 161 | log.topics.slice(1) 162 | ); 163 | contract = log.address; 164 | tokenId = decodedEventLog.punkIndex.toString(); 165 | to = decodedEventLog.to; 166 | amount = 1; 167 | break; 168 | case this.PUNK_BOUGHT_TOPIC: 169 | decodedEventLog = this._web3Instance.eth.abi.decodeLog( 170 | CryptoPunkABI.bought.inputs ?? [], 171 | log.data, 172 | log.topics.slice(1) 173 | ); 174 | contract = log.address; 175 | tokenId = decodedEventLog.punkIndex.toString(); 176 | to = decodedEventLog.toAddress; 177 | break; 178 | default: 179 | amount = 0; 180 | } 181 | return { 182 | contract, 183 | tokenId, 184 | amount, 185 | is1155, 186 | to, 187 | }; 188 | } catch (error) { 189 | throw UtilsException.decodeLogError(error?.toString()); 190 | } 191 | }; 192 | 193 | sendSafeModeTransaction = (transactionConfig: Partial) => { 194 | const transactionInstance = new SendTransaction(); 195 | // safe mode need more transaction detail than normal, including nonce, gasLimit, type and etc. 196 | // https://docs.ethers.io/v5/api/providers/types/#providers-TransactionRequest 197 | this._web3Instance.eth.getTransactionCount(transactionConfig.from as string).then(nonce => { 198 | transactionConfig.value = BigNumber.isBigNumber(transactionConfig.value) 199 | ? transactionConfig.value 200 | : (BigNumber.from(transactionConfig.value) as SafeAny); 201 | transactionConfig.nonce = nonce; 202 | transactionConfig.type = 2; 203 | const priorityFee = BigNumber.from(transactionConfig?.maxPriorityFeePerGas || 2000000000); 204 | this._web3Instance.eth.getGasPrice().then(gasPrice => { 205 | transactionConfig.maxFeePerGas = transactionConfig?.maxFeePerGas || priorityFee.add(BigNumber.from(gasPrice)); 206 | transactionConfig.maxPriorityFeePerGas = transactionConfig?.maxPriorityFeePerGas || priorityFee; 207 | // eth_sign only accetp 32byte data 208 | const unsignedTransactionHash = this._web3Instance.utils.keccak256( 209 | ethers.utils.serializeTransaction(transactionConfig) 210 | ); 211 | const flashBotsSendTx = (signedTrx: string) => { 212 | const trueRpc = 'https://rpc.flashbots.net'; 213 | const flashBots = new Web3(trueRpc); 214 | flashBots.eth 215 | .sendSignedTransaction(signedTrx) 216 | .on('transactionHash', hash => { 217 | transactionInstance.transactionHashHandler?.(hash); 218 | }) 219 | .on('receipt', receipt => { 220 | transactionInstance.receiptHandler?.(receipt); 221 | }) 222 | .on('error', error => { 223 | transactionInstance.errorHandler?.(error); 224 | }); 225 | }; 226 | // Client 227 | if ((globalThis as SafeAny).ethereum) { 228 | this._web3Instance.eth 229 | .sign(unsignedTransactionHash, transactionConfig.from as string) 230 | .then(async signedTransaction => { 231 | const signedTrx = ethers.utils.serializeTransaction(transactionConfig, signedTransaction); 232 | flashBotsSendTx(signedTrx); 233 | }) 234 | .catch(error => { 235 | transactionInstance.errorHandler?.(error); 236 | }) 237 | .finally(() => { 238 | transactionInstance.finallyHandler?.(); 239 | }); 240 | } else { 241 | // Server 242 | try { 243 | const signedTransaction = this._web3Instance.eth.accounts.sign( 244 | unsignedTransactionHash, 245 | this.walletConfig?.privateKey as string 246 | ); 247 | const signedTrx = ethers.utils.serializeTransaction(transactionConfig, signedTransaction.signature); 248 | flashBotsSendTx(signedTrx); 249 | } catch (error) { 250 | transactionInstance.errorHandler?.(error as SafeAny); 251 | } finally { 252 | transactionInstance.finallyHandler?.(); 253 | } 254 | } 255 | }); 256 | }); 257 | return transactionInstance; 258 | }; 259 | 260 | sendTransaction = (transactionConfig: TransactionConfig) => { 261 | if ( 262 | this.walletConfig?.address && 263 | transactionConfig?.from?.toString?.()?.toLowerCase?.() !== this.walletConfig?.address?.toLowerCase?.() 264 | ) { 265 | throw UtilsException.invalidParamError('transactionConfig.from', 'Buyer address must equal to wallet address'); 266 | } 267 | const transactionInstance = new SendTransaction(); 268 | this._web3Instance.eth 269 | .estimateGas({ 270 | data: transactionConfig.data, 271 | value: transactionConfig.value, 272 | from: transactionConfig.from, 273 | to: transactionConfig.to, 274 | }) 275 | .then(estimateGas => { 276 | // some wallet(eg: coinbase wallet) will inject providers object into window, which provide all providers available in current browser 277 | transactionConfig.gas = BigNumber.from(estimateGas).toHexString(); 278 | if (typeof (globalThis as SafeAny)?.ethereum === 'object') { 279 | let finalProvider = (globalThis as SafeAny)?.ethereum as SafeAny; 280 | if (finalProvider?.providers && (this._web3Instance.currentProvider as SafeAny)?.isMetaMask) { 281 | finalProvider = finalProvider?.providers.filter( 282 | (provider: { isMetaMask: boolean }) => provider.isMetaMask 283 | )[0]; 284 | } 285 | finalProvider 286 | ?.request({ 287 | method: 'eth_sendTransaction', 288 | params: [transactionConfig], 289 | }) 290 | .then((hash: string) => { 291 | transactionInstance.transactionHashHandler?.(hash); 292 | this.inspectTransaction({ hash }).on('receipt', receipt => { 293 | transactionInstance.receiptHandler?.(receipt); 294 | }); 295 | }) 296 | .catch((error: Error) => { 297 | transactionInstance.errorHandler?.(error); 298 | }) 299 | .finally(() => { 300 | transactionInstance.finally(); 301 | }); 302 | } else { 303 | this._web3Instance.eth.accounts 304 | .signTransaction(transactionConfig, this.walletConfig?.privateKey as string) 305 | .then(signedTransaction => { 306 | this._web3Instance.eth 307 | .sendSignedTransaction(signedTransaction.rawTransaction as string) 308 | .on('transactionHash', hash => { 309 | transactionInstance.transactionHashHandler?.(hash); 310 | }) 311 | .on('receipt', receipt => { 312 | transactionInstance.receiptHandler?.(receipt); 313 | }) 314 | .on('error', error => { 315 | transactionInstance.errorHandler?.(error); 316 | }) 317 | .finally(() => { 318 | transactionInstance.finallyHandler?.(); 319 | }); 320 | }); 321 | } 322 | }); 323 | return transactionInstance; 324 | }; 325 | 326 | signMessage = async (message: string): Promise => { 327 | if ((globalThis as SafeAny).ethereum) { 328 | const provider = (globalThis as SafeAny).ethereum; 329 | const accounts = await provider.request({ method: 'eth_requestAccounts' }); 330 | const account = accounts[0]; 331 | const signature = await this._web3Instance.eth.personal.sign(message, account, ''); 332 | return signature; 333 | } else { 334 | // server side 335 | const signResult = this._web3Instance.eth.accounts.sign(message, this.walletConfig?.privateKey as string); 336 | return signResult.signature; 337 | } 338 | }; 339 | 340 | getSigner = () => { 341 | return this._ethersSigner; 342 | }; 343 | } 344 | 345 | class SendTransaction implements Transaction { 346 | public transactionHashHandler: TransactionHashHandler = null; 347 | public receiptHandler: ReceiptHandler = null; 348 | public errorHandler: ErrorHandler = null; 349 | public finallyHandler: FinallyHandler = null; 350 | on(type: 'transactionHash', handler: TransactionHashHandler): Transaction; 351 | on(type: 'receipt', handler: ReceiptHandler): Transaction; 352 | on(type: 'error', handler: ErrorHandler): Transaction; 353 | on( 354 | type: 'transactionHash' | 'receipt' | 'error', 355 | handler: TransactionHashHandler & ReceiptHandler & ErrorHandler 356 | ): Transaction { 357 | this[`${type}Handler`] = handler; 358 | return this; 359 | } 360 | finally(onfinally?: FinallyHandler): void { 361 | this.finallyHandler = onfinally; 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/modules/utils/is-invalid-param.ts: -------------------------------------------------------------------------------- 1 | export function isInvalidParam(param: unknown) { 2 | if (typeof param === 'number') { 3 | return false; 4 | } 5 | 6 | if ( 7 | param === undefined || 8 | param === null || 9 | param === 'null' || 10 | param === 'undefined' || 11 | typeof param === 'boolean' 12 | ) { 13 | return true; 14 | } 15 | 16 | const contentStr = typeof param === 'string' ? param : JSON.stringify(param); 17 | 18 | return contentStr.length === 0 || contentStr === '{}' || contentStr === '[]'; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/handler.ts: -------------------------------------------------------------------------------- 1 | import { ApiKeyConfig, HTTPClient, OrderKind, OrderType } from '@/types'; 2 | import * as Models from './utils'; 3 | 4 | import { ExternalServiceRateLimiter } from '@/common'; 5 | import { RateLimiter } from 'limiter'; 6 | import { BaseException } from '@/exceptions'; 7 | import { _TypedDataEncoder, defaultAbiCoder } from 'ethers/lib/utils'; 8 | import { IPostOrderHandler } from './utils'; 9 | import { SafeAny } from 'src/types/safe-any'; 10 | 11 | export class SeaportV1D5Handler implements IPostOrderHandler { 12 | protocol = OrderKind.SeaportV15; 13 | listingUrl = 'https://api.opensea.io/v2/orders/ethereum/seaport/listings'; 14 | offerCollectionUrl = 'https://api.opensea.io/v2/offers'; 15 | offerTokenUrl = 'https://api.opensea.io/v2/orders/ethereum/seaport/offers'; 16 | rateLimiter: ExternalServiceRateLimiter; 17 | constructor(private client: HTTPClient, apiKeyConfig: ApiKeyConfig) { 18 | this.rateLimiter = new ExternalServiceRateLimiter( 19 | apiKeyConfig.apiKey, 20 | new RateLimiter({ 21 | tokensPerInterval: apiKeyConfig.requestsPerInterval, 22 | interval: apiKeyConfig.interval, 23 | }) 24 | ); 25 | } 26 | async handle(payload: SafeAny): Promise { 27 | const order = payload.order; 28 | const orderbook = payload.orderbook; 29 | const orderType = payload.orderType; 30 | if (!['opensea'].includes(orderbook)) { 31 | throw BaseException.invalidParamError('orderbook', `${this.protocol} only supports opensea`); 32 | } 33 | 34 | const seaportOrder: Models.SeaportV1D5.Types.OrderComponents = { 35 | offerer: order.data.offerer, 36 | zone: order.data.zone, 37 | offer: order.data.offer, 38 | consideration: order.data.consideration, 39 | orderType: order.data.orderType, 40 | startTime: order.data.startTime, 41 | endTime: order.data.endTime, 42 | zoneHash: order.data.zoneHash, 43 | salt: order.data.salt, 44 | conduitKey: order.data.conduitKey, 45 | counter: order.data.counter, 46 | signature: order.data.signature, 47 | }; 48 | 49 | const apiKey = await this.rateLimiter.getAPIKeyWithRateLimiter(); 50 | if (orderType === OrderType.Listing) { 51 | const result = await this.client.post( 52 | this.listingUrl, 53 | { 54 | parameters: { 55 | ...seaportOrder, 56 | totalOriginalConsiderationItems: order.data.consideration.length, 57 | }, 58 | signature: order.data.signature, 59 | protocol_address: Models.SeaportV1D5.Addresses.Exchange[Models.Utils.Network.Ethereum], 60 | }, 61 | { 'X-Api-Key': apiKey }, 62 | true 63 | ); 64 | const orderHash = this.hash(seaportOrder); 65 | return { 66 | orderHash, 67 | result, 68 | }; 69 | } else { 70 | // OrderType.Offer 71 | // We'll always have only one of the below cases: 72 | // Only relevant/present for attribute bids 73 | const attribute = payload.attribute; 74 | // Only relevant for collection bids 75 | const slug = payload.slug; 76 | // Only relevant for token set bids 77 | const tokenSetId = payload.tokenSetId; 78 | const isNonFlagged = payload.isNonFlagged; 79 | 80 | let schema; 81 | if (attribute) { 82 | schema = { 83 | kind: 'attribute', 84 | data: { 85 | collection: attribute.slug, 86 | isNonFlagged: isNonFlagged || undefined, 87 | trait: { 88 | key: attribute.key, 89 | value: attribute.value, 90 | }, 91 | }, 92 | }; 93 | } else if (slug && isNonFlagged) { 94 | schema = { 95 | kind: 'collection-non-flagged', 96 | data: { 97 | collection: { 98 | slug, 99 | }, 100 | }, 101 | }; 102 | } else if (slug) { 103 | schema = { 104 | kind: 'collection', 105 | data: { 106 | collection: { 107 | slug, 108 | }, 109 | }, 110 | }; 111 | } else if (tokenSetId) { 112 | schema = { 113 | kind: 'token-set', 114 | data: { 115 | tokenSetId, 116 | }, 117 | }; 118 | } 119 | 120 | if (schema && ['collection', 'collection-non-flagged', 'attribute'].includes(schema.kind)) { 121 | // post collection/trait offer 122 | const result = await this.client.post( 123 | this.offerCollectionUrl, 124 | { 125 | criteria: schema.data, 126 | protocol_data: { 127 | parameters: { 128 | ...seaportOrder, 129 | totalOriginalConsiderationItems: order.data.consideration.length, 130 | }, 131 | signature: order.data.signature, 132 | }, 133 | protocol_address: Models.SeaportV1D5.Addresses.Exchange[Models.Utils.Network.Ethereum], 134 | }, 135 | { 'X-Api-Key': apiKey }, 136 | true 137 | ); 138 | const orderHash = this.hash(seaportOrder); 139 | return { 140 | orderHash, 141 | result, 142 | }; 143 | } else { 144 | // post token offer 145 | const result = await this.client.post( 146 | this.offerTokenUrl, 147 | { 148 | parameters: { 149 | ...seaportOrder, 150 | totalOriginalConsiderationItems: order.data.consideration.length, 151 | }, 152 | signature: order.data.signature, 153 | protocol_address: Models.SeaportV1D5.Addresses.Exchange[Models.Utils.Network.Ethereum], 154 | }, 155 | { 'X-Api-Key': apiKey }, 156 | true 157 | ); 158 | const orderHash = this.hash(seaportOrder); 159 | return { 160 | orderHash, 161 | result, 162 | }; 163 | } 164 | } 165 | } 166 | 167 | hash(order: Models.SeaportV1D5.Types.OrderComponents) { 168 | const ORDER_EIP712_TYPES = { 169 | OrderComponents: [ 170 | { name: 'offerer', type: 'address' }, 171 | { name: 'zone', type: 'address' }, 172 | { name: 'offer', type: 'OfferItem[]' }, 173 | { name: 'consideration', type: 'ConsiderationItem[]' }, 174 | { name: 'orderType', type: 'uint8' }, 175 | { name: 'startTime', type: 'uint256' }, 176 | { name: 'endTime', type: 'uint256' }, 177 | { name: 'zoneHash', type: 'bytes32' }, 178 | { name: 'salt', type: 'uint256' }, 179 | { name: 'conduitKey', type: 'bytes32' }, 180 | { name: 'counter', type: 'uint256' }, 181 | ], 182 | OfferItem: [ 183 | { name: 'itemType', type: 'uint8' }, 184 | { name: 'token', type: 'address' }, 185 | { name: 'identifierOrCriteria', type: 'uint256' }, 186 | { name: 'startAmount', type: 'uint256' }, 187 | { name: 'endAmount', type: 'uint256' }, 188 | ], 189 | ConsiderationItem: [ 190 | { name: 'itemType', type: 'uint8' }, 191 | { name: 'token', type: 'address' }, 192 | { name: 'identifierOrCriteria', type: 'uint256' }, 193 | { name: 'startAmount', type: 'uint256' }, 194 | { name: 'endAmount', type: 'uint256' }, 195 | { name: 'recipient', type: 'address' }, 196 | ], 197 | }; 198 | return _TypedDataEncoder.hashStruct('OrderComponents', ORDER_EIP712_TYPES, order); 199 | } 200 | } 201 | 202 | export class LooksRareV2Handler implements IPostOrderHandler { 203 | protocol = OrderKind.LooksRareV2; 204 | url = 'https://api.looksrare.org/api/v2/orders'; 205 | rateLimiter: ExternalServiceRateLimiter; 206 | constructor(private client: HTTPClient, apiKeyConfig: ApiKeyConfig) { 207 | this.rateLimiter = new ExternalServiceRateLimiter( 208 | apiKeyConfig.apiKey, 209 | new RateLimiter({ 210 | tokensPerInterval: apiKeyConfig.requestsPerInterval, 211 | interval: apiKeyConfig.interval, 212 | }) 213 | ); 214 | } 215 | async handle(payload: SafeAny): Promise { 216 | const order = payload.order; 217 | const orderbook = payload.orderbook; 218 | 219 | if (!['looks-rare'].includes(orderbook)) { 220 | throw BaseException.invalidParamError('orderbook', `${this.protocol} only supports looks-rare`); 221 | } 222 | 223 | const looksrareOrder: Models.LooksRareV2.Types.MakerOrderParams = order.data; 224 | const apiKey = await this.rateLimiter.getAPIKeyWithRateLimiter(); 225 | const result = this.client.post( 226 | this.url, 227 | { 228 | ...looksrareOrder, 229 | }, 230 | { 'X-Api-Key': apiKey }, 231 | true 232 | ); 233 | const orderHash = this.hash(looksrareOrder); 234 | return { 235 | orderHash, 236 | result, 237 | }; 238 | } 239 | hash(order: Models.LooksRareV2.Types.MakerOrderParams) { 240 | const EIP712_TYPES = { 241 | Maker: [ 242 | { name: 'quoteType', type: 'uint8' }, 243 | { name: 'globalNonce', type: 'uint256' }, 244 | { name: 'subsetNonce', type: 'uint256' }, 245 | { name: 'orderNonce', type: 'uint256' }, 246 | { name: 'strategyId', type: 'uint256' }, 247 | { name: 'collectionType', type: 'uint8' }, 248 | { name: 'collection', type: 'address' }, 249 | { name: 'currency', type: 'address' }, 250 | { name: 'signer', type: 'address' }, 251 | { name: 'startTime', type: 'uint256' }, 252 | { name: 'endTime', type: 'uint256' }, 253 | { name: 'price', type: 'uint256' }, 254 | { name: 'itemIds', type: 'uint256[]' }, 255 | { name: 'amounts', type: 'uint256[]' }, 256 | { name: 'additionalParameters', type: 'bytes' }, 257 | ], 258 | }; 259 | return _TypedDataEncoder.hashStruct('Maker', EIP712_TYPES, order); 260 | } 261 | } 262 | 263 | export class X2Y2Handler implements IPostOrderHandler { 264 | protocol = OrderKind.X2Y2; 265 | url = 'https://api.x2y2.org/api/orders/add'; 266 | rateLimiter: ExternalServiceRateLimiter; 267 | 268 | constructor(private client: HTTPClient, apiKeyConfig: ApiKeyConfig) { 269 | this.rateLimiter = new ExternalServiceRateLimiter( 270 | apiKeyConfig.apiKey, 271 | new RateLimiter({ 272 | tokensPerInterval: apiKeyConfig.requestsPerInterval, 273 | interval: apiKeyConfig.interval, 274 | }) 275 | ); 276 | } 277 | 278 | async handle(payload: SafeAny): Promise { 279 | const order = payload.order; 280 | const orderbook = payload.orderbook; 281 | 282 | if (!['x2y2'].includes(orderbook)) { 283 | throw BaseException.invalidParamError('orderbook', `${this.protocol} only supports x2y2`); 284 | } 285 | 286 | const x2y2Order: Models.X2Y2.Types.X2Y2ListingOrderParams = order.data; 287 | const apiKey = await this.rateLimiter.getAPIKeyWithRateLimiter(); 288 | 289 | const orderParams = { 290 | order: defaultAbiCoder.encode( 291 | [ 292 | `( 293 | uint256 salt, 294 | address user, 295 | uint256 network, 296 | uint256 intent, 297 | uint256 delegateType, 298 | uint256 deadline, 299 | address currency, 300 | bytes dataMask, 301 | (uint256 price, bytes data)[] items, 302 | bytes32 r, 303 | bytes32 s, 304 | uint8 v, 305 | uint8 signVersion 306 | )`, 307 | ], 308 | [x2y2Order] 309 | ), 310 | isBundle: false, 311 | bundleName: '', 312 | bundleDesc: '', 313 | orderIds: [], 314 | changePrice: false, 315 | isCollection: order.data.dataMask !== '0x', 316 | }; 317 | const result = this.client.post(this.url, orderParams, { 'X-Api-Key': apiKey }, true); 318 | const orderHash = x2y2Order.itemHash; 319 | return { 320 | orderHash, 321 | result, 322 | }; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/index.ts: -------------------------------------------------------------------------------- 1 | import { splitSignature } from 'ethers/lib/utils'; 2 | 3 | import { AggregatorApiException, BaseException } from '@/exceptions'; 4 | 5 | import * as Models from './utils'; 6 | import { IPostOrderHandler } from './utils'; 7 | 8 | import { 9 | AggregatorApiResponse, 10 | AggregatorApiStatusResponse, 11 | Config, 12 | HTTPClient, 13 | Orderbook, 14 | OrderKind, 15 | PostOrderReq, 16 | } from '@/types'; 17 | import { LooksRareV2Handler, SeaportV1D5Handler, X2Y2Handler } from './handler'; 18 | import { HTTPClientStable } from '@/http'; 19 | import { SafeAny } from 'src/types/safe-any'; 20 | 21 | export class PostOrderHandler { 22 | private handlers = new Map(); 23 | private client: HTTPClient = new HTTPClientStable(); 24 | 25 | constructor(private config: Config) { 26 | const _client = new HTTPClientStable(config?.agent); 27 | this.client = _client; 28 | if (config.openSeaApiKeyConfig) { 29 | this.handlers.set(OrderKind.SeaportV15, new SeaportV1D5Handler(this.client, config.openSeaApiKeyConfig)); 30 | } 31 | if (config.looksRareApiKeyConfig) { 32 | this.handlers.set(OrderKind.LooksRareV2, new LooksRareV2Handler(this.client, config.looksRareApiKeyConfig)); 33 | } 34 | 35 | if (config.x2y2ApiKeyConfig) { 36 | this.handlers.set(OrderKind.X2Y2, new X2Y2Handler(this.client, config.x2y2ApiKeyConfig)); 37 | } 38 | } 39 | 40 | async handle(params: PostOrderReq, signature: string, endpoint: string): Promise { 41 | // given the orderKind, invoke NFTGo developer API or directly post order to marketplace 42 | if (params.order.kind === OrderKind.Blur || params.orderbook === Orderbook.SELF || params.orderbook === Orderbook.NftGo) { 43 | const res = await this.post(endpoint, { 44 | ...params, 45 | signature, 46 | }); 47 | return res; 48 | } else { 49 | const handler = this.handlers.get(params.order.kind); 50 | if (!handler) { 51 | throw BaseException.invalidParamError('order.kind', 'unsupported orderKind ' + params.order.kind); 52 | } 53 | 54 | switch (params.extraArgs.version) { 55 | case 'v3': { 56 | try { 57 | const { v, r, s } = splitSignature(signature); 58 | params.order.data = { 59 | ...params.order.data, 60 | signature, 61 | v, 62 | r, 63 | s, 64 | }; 65 | // TODO: need to await? 66 | } catch (e) { 67 | throw BaseException.invalidParamError('signature', 'invalid signature ' + signature); 68 | } 69 | const res = await handler.handle(params); 70 | return res; 71 | } 72 | case 'v4': { 73 | try { 74 | const { v, r, s } = splitSignature(signature); 75 | if (params.bulkData?.kind === 'seaport-v1.5') { 76 | // Encode the merkle proof of inclusion together with the signature 77 | params.order.data.signature = Models.SeaportV1D5.Utils.encodeBulkOrderProofAndSignature( 78 | params.bulkData.data.orderIndex, 79 | params.bulkData.data.merkleProof, 80 | signature 81 | ); 82 | } else { 83 | // If the signature is provided via query parameters, use it 84 | params.order.data = { 85 | ...params.order.data, 86 | // To cover everything: 87 | // - orders requiring a single signature field 88 | // - orders requiring split signature fields 89 | signature, 90 | v, 91 | r, 92 | s, 93 | }; 94 | } 95 | } catch { 96 | throw BaseException.invalidParamError('signature', 'invalid signature ' + signature); 97 | } 98 | const res = await handler.handle(params); 99 | return res; 100 | } 101 | default: 102 | throw BaseException.invalidParamError('extraArgs.version', 'unsupported version ' + params.extraArgs.version); 103 | } 104 | } 105 | } 106 | 107 | private get headers() { 108 | return { 'X-API-KEY': this.config.apiKey, 'X-FROM': 'js_sdk' } as Record; 109 | } 110 | 111 | private get url() { 112 | return this.config.baseUrl; 113 | } 114 | 115 | private async post(path: string, params: Req) { 116 | const url = `${this.url}${path}?chain=${this.config.chain}`; 117 | const response = await this.client.post, Req>(url, params, this.headers); 118 | const { code, msg, data } = response; 119 | if (code === 'SUCCESS') { 120 | return data; 121 | } else { 122 | throw new AggregatorApiException(msg, code, path); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as SeaportV1D5 from './seaport-v1.5'; 2 | import * as LooksRare from './looks-rare'; 3 | import * as LooksRareV2 from './looks-rare-v2'; 4 | import * as X2Y2 from './x2y2'; 5 | import * as Utils from './utils'; 6 | 7 | import { OrderKind } from '@/types'; 8 | import { ExternalServiceRateLimiter } from '@/common'; 9 | import { SafeAny } from 'src/types/safe-any'; 10 | 11 | export interface IPostOrderHandler { 12 | protocol: OrderKind; 13 | rateLimiter: ExternalServiceRateLimiter; 14 | handle: (payload: SafeAny) => Promise; 15 | } 16 | 17 | export { SeaportV1D5, LooksRare, LooksRareV2, X2Y2, Utils }; 18 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/looks-rare-v2/index.ts: -------------------------------------------------------------------------------- 1 | import * as Types from './types'; 2 | 3 | export { Types }; 4 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/looks-rare-v2/types.ts: -------------------------------------------------------------------------------- 1 | import { TypedDataField } from '@ethersproject/abstract-signer'; 2 | 3 | export type EIP712TypedData = Record>; 4 | 5 | export type OrderKind = 'single-token' | 'contract-wide'; 6 | 7 | export enum QuoteType { 8 | Bid, 9 | Ask, 10 | } 11 | 12 | export enum CollectionType { 13 | ERC721, 14 | ERC1155, 15 | } 16 | 17 | export enum StrategyType { 18 | standard = 0, 19 | collection = 1, 20 | collectionWithMerkleTree = 2, 21 | } 22 | 23 | export enum MerkleTreeNodePosition { 24 | Left, 25 | Right, 26 | } 27 | 28 | export type MerkleTreeNode = { 29 | value: string; 30 | position: MerkleTreeNodePosition; 31 | }; 32 | 33 | export type MerkleTree = { 34 | root: string; 35 | proof: MerkleTreeNode[]; 36 | }; 37 | 38 | export type MakerOrderParams = { 39 | kind?: OrderKind; 40 | 41 | quoteType: QuoteType; 42 | globalNonce: string; 43 | subsetNonce: string; 44 | orderNonce: string; 45 | strategyId: number; 46 | collectionType: CollectionType; 47 | 48 | collection: string; 49 | currency: string; 50 | signer: string; 51 | 52 | startTime: number; 53 | endTime: number; 54 | price: string; 55 | itemIds: string[]; 56 | amounts: string[]; 57 | 58 | additionalParameters: string; 59 | signature?: string; 60 | merkleTree?: MerkleTree; 61 | }; 62 | 63 | export type TakerOrderParams = { 64 | recipient: string; 65 | additionalParameters: string; 66 | }; 67 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/looks-rare/index.ts: -------------------------------------------------------------------------------- 1 | import * as Types from './types'; 2 | 3 | export { Types }; 4 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/looks-rare/types.ts: -------------------------------------------------------------------------------- 1 | export type LooksRareListingOrderParams = { 2 | isOrderAsk: boolean; 3 | signer: string; 4 | collection: string; 5 | price: string; 6 | tokenId: string; 7 | amount: string; 8 | strategy: string; 9 | currency: string; 10 | nonce: string; 11 | startTime: number; 12 | endTime: number; 13 | minPercentageToAsk: number; 14 | params: string; 15 | v?: number; 16 | r?: string; 17 | s?: string; 18 | }; 19 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/seaport-v1.5/addresses.ts: -------------------------------------------------------------------------------- 1 | import { ChainIdToAddress, Network } from '../utils'; 2 | export const Exchange: ChainIdToAddress = { 3 | [Network.Ethereum]: '0x00000000000000adc04c56bf30ac9d3c0aaf14dc', 4 | [Network.EthereumGoerli]: '0x00000000000000adc04c56bf30ac9d3c0aaf14dc', 5 | [Network.Bsc]: '0x00000000000000adc04c56bf30ac9d3c0aaf14dc', 6 | [Network.Optimism]: '0x00000000000000adc04c56bf30ac9d3c0aaf14dc', 7 | [Network.Gnosis]: '0x00000000000000adc04c56bf30ac9d3c0aaf14dc', 8 | [Network.Polygon]: '0x00000000000000adc04c56bf30ac9d3c0aaf14dc', 9 | [Network.Arbitrum]: '0x00000000000000adc04c56bf30ac9d3c0aaf14dc', 10 | [Network.Avalanche]: '0x00000000000000adc04c56bf30ac9d3c0aaf14dc', 11 | [Network.ScrollAlpha]: '0x238285119ad0842051b4a46a9428139d30869b55', 12 | }; 13 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/seaport-v1.5/index.ts: -------------------------------------------------------------------------------- 1 | import * as Types from './types'; 2 | import * as Addresses from './addresses'; 3 | import * as Utils from './utils'; 4 | 5 | export { Types, Addresses, Utils }; 6 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/seaport-v1.5/types.ts: -------------------------------------------------------------------------------- 1 | export enum OrderType { 2 | FULL_OPEN, 3 | PARTIAL_OPEN, 4 | FULL_RESTRICTED, 5 | PARTIAL_RESTRICTED, 6 | CONTRACT, 7 | } 8 | 9 | export enum ItemType { 10 | NATIVE, 11 | ERC20, 12 | ERC721, 13 | ERC1155, 14 | ERC721_WITH_CRITERIA, 15 | ERC1155_WITH_CRITERIA, 16 | } 17 | 18 | export type OfferItem = { 19 | itemType: ItemType; 20 | token: string; 21 | identifierOrCriteria: string; 22 | startAmount: string; 23 | endAmount: string; 24 | }; 25 | 26 | export type ConsiderationItem = { 27 | itemType: ItemType; 28 | token: string; 29 | identifierOrCriteria: string; 30 | startAmount: string; 31 | endAmount: string; 32 | recipient: string; 33 | }; 34 | 35 | export type ListingOrderParams = { 36 | offerer: string; 37 | zone: string; 38 | offer: OfferItem[]; 39 | consideration: ConsiderationItem[]; 40 | orderType: OrderType; 41 | startTime: number; 42 | endTime: number; 43 | zoneHash: string; 44 | salt: string; 45 | conduitKey: string; 46 | counter: string; 47 | signature: string; 48 | }; 49 | 50 | export type OrderComponents = { 51 | offerer: string; 52 | zone: string; 53 | offer: OfferItem[]; 54 | consideration: ConsiderationItem[]; 55 | orderType: OrderType; 56 | startTime: number; 57 | endTime: number; 58 | zoneHash: string; 59 | salt: string; 60 | conduitKey: string; 61 | counter: string; 62 | signature?: string; 63 | totalOriginalConsiderationItems?: number; 64 | }; 65 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/seaport-v1.5/utils.ts: -------------------------------------------------------------------------------- 1 | import { defaultAbiCoder, hexConcat } from 'ethers/lib/utils'; 2 | 3 | export function encodeBulkOrderProofAndSignature(orderIndex: number, merkleProof: string[], signature: string): string { 4 | return hexConcat([ 5 | signature, 6 | `0x${orderIndex.toString(16).padStart(6, '0')}`, 7 | defaultAbiCoder.encode([`uint256[${merkleProof.length}]`], [merkleProof]), 8 | ]); 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export enum Network { 2 | // Ethereum 3 | Ethereum = 1, 4 | EthereumGoerli = 5, 5 | // Optimism 6 | Optimism = 10, 7 | // Bsc 8 | Bsc = 56, 9 | // Gnosis 10 | Gnosis = 100, 11 | // Polygon 12 | Polygon = 137, 13 | PolygonMumbai = 80001, 14 | // Arbitrum 15 | Arbitrum = 42161, 16 | // Avalanche 17 | Avalanche = 43114, 18 | AvalancheFuji = 43113, 19 | // Scroll 20 | ScrollAlpha = 534353, 21 | MantleTestnet = 5001, 22 | } 23 | 24 | export type ChainIdToAddress = { [chainId: number]: string }; 25 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/x2y2/index.ts: -------------------------------------------------------------------------------- 1 | import * as Types from './types'; 2 | 3 | export { Types }; 4 | -------------------------------------------------------------------------------- /src/modules/utils/post-order/utils/x2y2/types.ts: -------------------------------------------------------------------------------- 1 | export type X2Y2ListingOrderParams = { 2 | id: number; 3 | type: string; 4 | currency: string; 5 | price: string; 6 | maker: string; 7 | taker: string; 8 | deadline: number; 9 | itemHash: string; 10 | nft: { 11 | token: string; 12 | tokenId?: string; 13 | }; 14 | royalty_fee: number; 15 | }; 16 | -------------------------------------------------------------------------------- /src/modules/utils/x2y2-auth.ts: -------------------------------------------------------------------------------- 1 | import { X2Y2Authenticator, X2Y2AuthenticatorParams } from '@/types'; 2 | 3 | import Web3 from 'web3'; 4 | 5 | export class X2Y2MarketplaceAuthenticator implements X2Y2Authenticator { 6 | constructor(private web3: Web3) {} 7 | 8 | authorize = async ({ address }: X2Y2AuthenticatorParams) => { 9 | const message = this.web3.utils 10 | .utf8ToHex(`Before canceling the listing, please sign X2Y2 to let us verify that you are the owner of this address: 11 | ${address} 12 | This will not cost you any gas fees. 13 | `); 14 | 15 | const signature = await this.web3.eth.sign(message, address); 16 | 17 | return { 18 | message, 19 | signature, 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/types/action/action.ts: -------------------------------------------------------------------------------- 1 | import { SafeAny } from '../safe-any'; 2 | 3 | /** 4 | * actions witch trade aggregate response for frontend developer to process user interaction 5 | */ 6 | export type AggregatorAction = T extends ActionKind 7 | ? { 8 | name: ActionName; 9 | description: string; 10 | kind: T; 11 | data: ActionDataMap[T]; 12 | } 13 | : never; 14 | 15 | export enum ActionKind { 16 | Transaction = 'transaction', 17 | Signature = 'signature', 18 | PassThrough = 'pass-through', 19 | Controller = 'controller', 20 | } 21 | 22 | export interface ActionDataMap { 23 | [ActionKind.Transaction]: TransactionActionInfo; 24 | [ActionKind.Signature]: SignatureActionInfo; 25 | [ActionKind.PassThrough]: PassThroughActionInfo; 26 | [ActionKind.Controller]: PassThroughActionInfo; 27 | } 28 | 29 | export type TransactionActionInfo = { 30 | orderIndexes?: number[]; 31 | safeMode?: boolean; 32 | txAssociatedOrderIds: string[]; 33 | usdGas?: string; 34 | gasLimit?: string; 35 | txData: TxData; 36 | }; 37 | 38 | export type TxData = { 39 | from: string; 40 | to: string; 41 | data: string; 42 | value: string; 43 | }; 44 | 45 | export interface SignData { 46 | domain: { 47 | name: string; 48 | version: string; 49 | chainId: number; 50 | verifyingContract: string; 51 | }; 52 | signatureKind: 'eip191' | 'eip712'; 53 | types: Record; 54 | value: Record; 55 | message?: string; 56 | } 57 | 58 | export type SignatureActionInfo = { 59 | orderIndexes: number[]; 60 | sign: SignData; 61 | body: object; 62 | }; 63 | 64 | export type PassThroughActionInfo = { 65 | orderIndexes: number[]; 66 | endpoint: string; 67 | method: 'POST' | 'GET'; 68 | payload: SafeAny; 69 | }; 70 | 71 | export enum ActionName { 72 | AcceptOffer = 'accept-offer', 73 | AcceptListing = 'accept-listing', 74 | NftApproval = 'nft-approval', 75 | CurrencyApproval = 'currency-approval', 76 | PassThrough = 'pass-through', 77 | Contoller = 'controller', 78 | PostOrderToMarketplace = 'post-order-to-marketplace', 79 | CurrencyWrapping = 'currency-wrapping', 80 | OrderSignature = 'order-signature', 81 | CancelOrders = 'cancel-orders', 82 | Failed = 'failed', 83 | } 84 | -------------------------------------------------------------------------------- /src/types/action/executor.ts: -------------------------------------------------------------------------------- 1 | import { ActionTask } from './task'; 2 | 3 | export interface ActionTaskExecutor { 4 | execute(options?: ExecuteOptions): Promise; 5 | [Symbol.iterator]: () => Generator; 6 | } 7 | 8 | export interface ExecuteOptions { 9 | onTaskExecuted: TaskExecutedHandle; 10 | } 11 | 12 | export type TaskExecutedHandle = (task: ActionTask) => void; 13 | -------------------------------------------------------------------------------- /src/types/action/index.ts: -------------------------------------------------------------------------------- 1 | export * from './action'; 2 | export * from './executor'; 3 | export * from './task'; 4 | export * from './processor'; 5 | -------------------------------------------------------------------------------- /src/types/action/processor.ts: -------------------------------------------------------------------------------- 1 | import { SafeAny } from '../safe-any'; 2 | import { ActionKind, AggregatorAction } from './action'; 3 | 4 | export interface ActionProcessor { 5 | processSignatureAction: (action: AggregatorAction) => Promise; 6 | 7 | processTransactionAction: (action: AggregatorAction) => Promise; 8 | 9 | processPassThroughAction: ( 10 | action: AggregatorAction, 11 | params: ProcessPassThroughActionParams 12 | ) => Promise; 13 | 14 | processControllerAction(action: AggregatorAction): Promise[]>; 15 | } 16 | 17 | export type ProcessPassThroughActionParams = { 18 | signature: string; 19 | }; 20 | -------------------------------------------------------------------------------- /src/types/action/task.ts: -------------------------------------------------------------------------------- 1 | import { AggregatorAction, ActionKind } from './action'; 2 | 3 | export interface ActionTaskResultMap { 4 | [ActionKind.PassThrough]: unknown; 5 | [ActionKind.Signature]: string; 6 | [ActionKind.Transaction]: unknown; 7 | } 8 | 9 | export type ActionTaskStatus = 'success' | 'fail' | 'ready' | 'pending'; 10 | 11 | export interface ActionTask { 12 | status: ActionTaskStatus; 13 | index: number; 14 | error: Error | null; 15 | action: AggregatorAction; 16 | pre: ActionTask | null; 17 | result: unknown; 18 | execute(): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /src/types/aggregator/aggregator.ts: -------------------------------------------------------------------------------- 1 | import { CancelOrdersReq } from './cancel'; 2 | import { CreateOffersReq, CreateListingsReq } from './create'; 3 | import { FulfillOffersReq, FulfillListingsReq } from './fulfill'; 4 | import { AggregatorResponse } from './response'; 5 | 6 | export interface AggregatorInterface { 7 | /** 8 | * 9 | * - details: {@link } 10 | * @param params {@link } 11 | * @returns Promise<{@link }> 12 | */ 13 | createOffers(params: CreateOffersReq): Promise; 14 | 15 | /** 16 | * 17 | * - details: {@link } 18 | * @param params {@link any} 19 | * @returns Promise<{@link any}> 20 | */ 21 | fulfillOffers(params: FulfillOffersReq): Promise; 22 | 23 | /** 24 | * 25 | * - details: {@link } 26 | * @param params {@link any} 27 | * @returns Promise<{@link any}> 28 | */ 29 | cancelOrders(params: CancelOrdersReq): Promise; 30 | 31 | /** 32 | * 33 | * - details: {@link } 34 | * @param params {@link any} 35 | * @returns Promise<{@link any}> 36 | */ 37 | createListings(params: CreateListingsReq): Promise; 38 | 39 | /** 40 | * buy nfts 41 | * - details: {@link } 42 | * @param params {@link FulfillListingsReq} 43 | * @returns Promise<{@link }> 44 | */ 45 | fulfillListings(params: FulfillListingsReq): Promise; 46 | } 47 | -------------------------------------------------------------------------------- /src/types/aggregator/cancel.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '../order'; 2 | 3 | /** 4 | * cancel orders 5 | */ 6 | export interface CancelOrdersReq { 7 | callerAddress: string; 8 | extraArgs?: { 9 | blurAuth?: string; 10 | sign?: string; 11 | signMessage?: string; 12 | }; 13 | orders: (Order & { orderHash?: string })[]; 14 | } 15 | -------------------------------------------------------------------------------- /src/types/aggregator/create.ts: -------------------------------------------------------------------------------- 1 | import { OrderKind, Orderbook } from '../order'; 2 | 3 | /** 4 | * create listings 5 | */ 6 | /** 7 | * CreateListingV1RequestParam 8 | */ 9 | export interface CreateListingsReq { 10 | /** 11 | * order maker address 12 | */ 13 | maker: string; 14 | params: CreateListingInput[]; 15 | extraArgs?: { 16 | blurAuth?: string; 17 | }; 18 | } 19 | 20 | export interface CreateListingInput { 21 | token: string; 22 | /** 23 | * only applies to ERC1155 24 | */ 25 | quantity?: number; 26 | weiPrice: string; 27 | /** 28 | * order protocol 29 | */ 30 | orderKind: OrderKind; 31 | /** 32 | * marketplace orderbook 33 | */ 34 | orderbook: Orderbook; 35 | /** 36 | * Only applies to seaport orders. If true, royalty amount and recipients will be set automatically. 37 | */ 38 | automatedRoyalties?: boolean; 39 | /** 40 | * Only applies to seaport orders. Set a maximum amount of royalties to pay, rather than the full amount. 41 | * Only relevant when automatedRoyalties is true. 1 BPS = 0.01% Note: OpenSea does not support values below 50 bps. 42 | */ 43 | royaltyBps?: number; 44 | /** 45 | * For self-build marketplaces, include the marketplaceFeeBps within the order to collect marketplace fee. 46 | * Note that 1 Bps stands for 0.01%. For example, using 100 means your marketplace fee address will receive 47 | * 1% of the order's total price. 48 | */ 49 | marketplaceFeeBps?: number; 50 | /** 51 | * Address of the wallet taking the private order, private orders are only supported for seaport-v1.5 and x2y2 52 | */ 53 | taker?: string; 54 | /** 55 | * Unix timestamp (seconds) 56 | */ 57 | listingTime: string; 58 | /** 59 | * Unix timestamp (seconds) 60 | */ 61 | expirationTime: string; 62 | nonce?: string; 63 | salt?: string; 64 | /** 65 | * default to be ethereum 66 | */ 67 | currency?: string; 68 | } 69 | 70 | /** 71 | * create offers 72 | */ 73 | 74 | //CreateOffersV1Request 75 | export interface CreateOffersReq { 76 | maker: string; 77 | params: CreateOfferInput[]; 78 | extraArgs?: { 79 | blurAuth?: string; 80 | }; 81 | } 82 | 83 | export interface CreateOfferInput { 84 | /** 85 | * Bid on a particular token. Example: `0x8d04a8c79ceb0889bdd12acdf3fa9d207ed3ff63:123 86 | */ 87 | token?: string; 88 | /** 89 | * Bid on a particular collection with collection-id. Example: 90 | * `0x8d04a8c79ceb0889bdd12acdf3fa9d207ed3ff63 91 | */ 92 | collection?: string; 93 | /** 94 | * Bid on a particular attribute key. This is case sensitive. Example: `Composition` 95 | */ 96 | attributeKey?: string; 97 | /** 98 | * Bid on a particular attribute value. This is case sensitive. Example: `Teddy (#33)` 99 | */ 100 | attributeValue?: string; 101 | /** 102 | * Quantity of tokens user is buying. Only compatible with ERC1155 tokens. Example: `5` 103 | */ 104 | quantity?: number; 105 | /** 106 | * Amount bidder is willing to offer in wei. Example: `1000000000000000000` 107 | */ 108 | weiPrice: string; 109 | /** 110 | * Exchange protocol used to create order. Example: `seaport-v1.5` 111 | */ 112 | orderKind: OrderKind; 113 | /** 114 | * Orderbook where order is placed. Example: `Reservoir` 115 | */ 116 | orderbook: Orderbook; 117 | orderbookApiKey?: string; 118 | /** 119 | * If true, royalty amounts and recipients will be set automatically. 120 | */ 121 | automatedRoyalties?: boolean; 122 | /** 123 | * Set a maximum amount of royalties to pay, rather than the full amount. Only relevant when 124 | * using automated royalties. 1 BPS = 0.01% Note: OpenSea does not support values below 50 125 | * bps. 126 | */ 127 | royaltyBps?: number; 128 | /** 129 | * For self-build marketplaces, include the marketplaceFeeBps within the order to collect marketplace fee. 130 | * Note that 1 Bps stands for 0.01%. For example, using 100 means your marketplace fee address will receive 131 | * 1% of the order's total price. 132 | */ 133 | marketplaceFeeBps?: number; 134 | /** 135 | * If true flagged tokens will be excluded 136 | */ 137 | excludeFlaggedTokens?: boolean; 138 | /** 139 | * Unix timestamp (seconds) indicating when listing will be listed. Example: `1656080318` 140 | */ 141 | listingTime?: string; 142 | /** 143 | * Unix timestamp (seconds) indicating when listing will expire. Example: `1656080318` 144 | */ 145 | expirationTime?: string; 146 | /** 147 | * Optional. Set a custom nonce 148 | */ 149 | nonce?: string; 150 | /** 151 | * Optional. Random string to make the order unique 152 | */ 153 | salt?: string; 154 | /** 155 | * ERC20 token address that the offer is providing with. Default to be WETH 156 | */ 157 | currency?: string; 158 | } 159 | -------------------------------------------------------------------------------- /src/types/aggregator/fulfill.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AggregateAcceptListingRequest 3 | */ 4 | export interface FulfillListingsReq { 5 | buyer: string; 6 | recipient?: string; 7 | noDirect?: boolean; 8 | orderIds?: string[]; 9 | orderHashes?: string[]; 10 | safeMode: boolean; 11 | extraArgs?: { 12 | blurAuth?: string; 13 | }; 14 | } 15 | 16 | /** 17 | * fulfill offers 18 | */ 19 | export interface FulfillOffersReq { 20 | offerFulfillmentIntentions: OfferFulfillmentIntention[]; 21 | sellerAddress: string; 22 | extraArgs?: { 23 | blurAuth?: string; 24 | }; 25 | } 26 | 27 | export interface OfferFulfillmentIntention { 28 | contractAddress: string; 29 | orderId?: string; 30 | orderHash?: string; 31 | quantity: number; 32 | tokenId: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/types/aggregator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aggregator'; 2 | export * from './cancel'; 3 | export * from './create'; 4 | export * from './fulfill'; 5 | export * from './response'; 6 | -------------------------------------------------------------------------------- /src/types/aggregator/response.ts: -------------------------------------------------------------------------------- 1 | import { ActionKind, ActionTaskExecutor, AggregatorAction } from '../action'; 2 | 3 | export interface AggregatorApiResponse { 4 | actions: AggregatorAction[]; 5 | invalidOrderHashes?: string[]; 6 | invalidOrderIds?: string[]; 7 | } 8 | 9 | export interface AggregatorResponse { 10 | actions: AggregatorAction[]; 11 | invalidOrderHashes?: string[]; 12 | invalidOrderIds?: string[]; 13 | executeActions: ActionTaskExecutor['execute']; 14 | } 15 | 16 | export interface AggregatorApiStatusResponse { 17 | code: 'SUCCESS' | 'SYSTEM_ERROR'; 18 | msg: string; 19 | data: T; 20 | } 21 | -------------------------------------------------------------------------------- /src/types/authenticator/index.ts: -------------------------------------------------------------------------------- 1 | export interface Authenticator

{ 2 | authorize: (params: P) => Promise; 3 | } 4 | 5 | export type BlurAuthenticatorParams = { 6 | address: string; 7 | force?: boolean; 8 | }; 9 | 10 | export type BlurAuthenticator = Authenticator; 11 | 12 | export type X2Y2AuthenticatorParams = { 13 | address: string; 14 | }; 15 | 16 | export type X2Y2AuthenticatorResult = { 17 | message: string; 18 | signature: string; 19 | }; 20 | 21 | export type X2Y2Authenticator = Authenticator; 22 | -------------------------------------------------------------------------------- /src/types/config.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from 'https'; 2 | import { provider } from 'web3-core'; 3 | 4 | // # user-land interface , core should implement this 5 | export enum EVMChain { 6 | ETHEREUM = 'ethereum', 7 | SEPOLIA = 'sepolia', 8 | } 9 | 10 | export interface WalletConfig { 11 | address: string; 12 | privateKey: string; 13 | } 14 | 15 | export interface Config { 16 | baseUrl: string; 17 | chain: EVMChain; 18 | apiKey?: string; 19 | walletConfig?: WalletConfig; 20 | web3Provider?: provider; 21 | openSeaApiKeyConfig?: ApiKeyConfig; 22 | looksRareApiKeyConfig?: ApiKeyConfig; 23 | x2y2ApiKeyConfig?: ApiKeyConfig; 24 | agent?: Agent; 25 | } 26 | 27 | export type ApiKeyConfig = { 28 | apiKey: string; 29 | requestsPerInterval: number; 30 | interval: number; 31 | }; 32 | -------------------------------------------------------------------------------- /src/types/go-trading.ts: -------------------------------------------------------------------------------- 1 | import { AggregatorInterface } from './aggregator'; 2 | import { Config } from './config'; 3 | import { OrderFetcherInterface } from './order-fetcher'; 4 | import { Utils } from './utils'; 5 | 6 | export interface GoTrading { 7 | orderFetcher: OrderFetcherInterface; 8 | utils: Utils; 9 | aggregator: AggregatorInterface; 10 | config: Config; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/http.ts: -------------------------------------------------------------------------------- 1 | export interface HTTPClient { 2 | get(url: string, query: Q | undefined, headers?: Record): Promise; 3 | post( 4 | url: string, 5 | data: P, 6 | headers?: Record, 7 | needOriginResponse?: boolean 8 | ): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './action'; 2 | export * from './aggregator'; 3 | export * from './order-fetcher'; 4 | export * from './utils'; 5 | export * from './order'; 6 | export * from './go-trading'; 7 | export * from './http'; 8 | export * from './config'; 9 | export * from './post-order'; 10 | export * from './authenticator'; 11 | -------------------------------------------------------------------------------- /src/types/json.ts: -------------------------------------------------------------------------------- 1 | export type JSON = string | boolean | number | null | Array | object; 2 | -------------------------------------------------------------------------------- /src/types/order-fetcher.ts: -------------------------------------------------------------------------------- 1 | import { Order, OrderKind, OrderType } from './order'; 2 | 3 | export interface OrderFetcherApiResponse { 4 | code: string; 5 | data: T; 6 | msg: string; 7 | statusCode: number; 8 | } 9 | 10 | /** 11 | * TokenPrice 12 | */ 13 | export interface TokenPrice { 14 | amount: TokenAmount; 15 | currency: TokenCurrency; 16 | } 17 | 18 | /** 19 | * TokenAmount 20 | */ 21 | export interface TokenAmount { 22 | decimal: number; 23 | platform: number | null; 24 | raw: string; 25 | usd: number | null; 26 | } 27 | 28 | /** 29 | * TokenCurrency 30 | */ 31 | export interface TokenCurrency { 32 | chain: string; 33 | contractAddress: string; 34 | decimals: number; 35 | symbol: string; 36 | } 37 | 38 | /** 39 | * OrderStatus 40 | */ 41 | export enum OrderStatus { 42 | Active = 'active', 43 | Cancelled = 'cancelled', 44 | Expired = 'expired', 45 | Filled = 'filled', 46 | Inactive = 'inactive', 47 | } 48 | 49 | export enum MarketId { 50 | Alienswap = 'alienswap', 51 | Artblocks = 'artblocks', 52 | Blur = 'blur', 53 | Ensvision = 'ensvision', 54 | Looksrare = 'looksrare', 55 | Magically = 'magically', 56 | Opensea = 'opensea', 57 | Ordinalsmarket = 'ordinalsmarket', 58 | Reservoir = 'reservoir', 59 | Sound = 'sound', 60 | Sudoswap = 'sudoswap', 61 | X2Y2 = 'x2y2', 62 | } 63 | 64 | /** 65 | * ListingOrderDTO 66 | */ 67 | export interface ListingOrderDTO { 68 | contractAddress: string; 69 | expiration: number; 70 | isPrivate: boolean; 71 | maker: string; 72 | marketId: MarketId; 73 | orderCreateTime?: number; 74 | orderExpirationTime?: number; 75 | orderGeneratedTime: number; 76 | orderId: string; 77 | orderHash: string; 78 | price: TokenPrice; 79 | quantityFilled: number; 80 | quantityRemaining: number; 81 | status: OrderStatus; 82 | taker: string; 83 | tokenId: string; 84 | totalQuantity: number; 85 | } 86 | 87 | /** 88 | * OfferDTO 89 | */ 90 | export interface OfferDTO { 91 | contractAddress: string; 92 | criteria: string; 93 | isPrivate: boolean; 94 | kind: OrderKind; 95 | maker: string; 96 | marketId: string; 97 | orderCreateTime: number; 98 | orderExpirationTime: number; 99 | orderId: string; 100 | orderHash: string; 101 | price: TokenPrice; 102 | quantityRemaining: number; 103 | rawData?: string; 104 | status: OrderStatus; 105 | taker: string; 106 | tokenId?: string; 107 | totalQuantity: number; 108 | } 109 | 110 | export interface GetOrdersByContractReq { 111 | contractAddress: string; 112 | includePrivate?: boolean; 113 | limit?: number; 114 | offset?: number; 115 | orderType: OrderType; 116 | } 117 | 118 | export interface GetOrdersByIdsReq { 119 | orders: Order[]; 120 | } 121 | export interface GetOrdersByMakerReq { 122 | includePrivate?: boolean; 123 | maker: string; 124 | orderType: OrderType; 125 | } 126 | export interface GetOrdersByNftsReq { 127 | contractAddress: string; 128 | includePrivate?: boolean; 129 | limit?: number; 130 | offset?: number; 131 | orderType: OrderType; 132 | tokenId: string; 133 | } 134 | 135 | export interface OrdersFetcherResp { 136 | listingDTOs: ListingOrderDTO[]; 137 | offerDTOs: OfferDTO[]; 138 | } 139 | export interface OrderFetcherInterface { 140 | /** 141 | * Return listings and offers of a contract 142 | * - details: {@link } 143 | * @param params {@link GetOrdersByContractReq} 144 | * @returns Promise<{@link OrdersFetcherResp}> 145 | */ 146 | getOrdersByContract(params: GetOrdersByContractReq): Promise; 147 | 148 | /** 149 | * Return listings and offers of a NFT 150 | * - details: {@link } 151 | * @param params {@link GetOrdersByNftsReq} 152 | * @returns Promise<{@link OrdersFetcherResp}> 153 | */ 154 | getOrdersByNFT(params: GetOrdersByNftsReq): Promise; 155 | 156 | /** 157 | * Return listings and offers of an order 158 | * - details: {@link } 159 | * @param params {@link GetOrdersByIdsReq} 160 | * @returns Promise<{@link OrdersFetcherResp}> 161 | */ 162 | getOrdersByIds(params: GetOrdersByIdsReq): Promise; 163 | 164 | /** 165 | * Return listings and offers of a maker 166 | * - details: {@link } 167 | * @param params {@link GetOrdersByMakerReq} 168 | * @returns Promise<{@link OrdersFetcherResp}> 169 | */ 170 | getOrdersByMaker(params: GetOrdersByMakerReq): Promise; 171 | } 172 | -------------------------------------------------------------------------------- /src/types/order.ts: -------------------------------------------------------------------------------- 1 | export interface Order { 2 | orderId: string; 3 | orderType: OrderType; 4 | } 5 | 6 | export enum OrderType { 7 | Listing = 'listing', 8 | Offer = 'offer', 9 | } 10 | 11 | /** 12 | * Exchange protocol used to create order. Example: `seaport-v1.5` 13 | */ 14 | export enum OrderKind { 15 | Blur = 'blur', 16 | LooksRareV2 = 'looks-rare-v2', 17 | SeaportV15 = 'seaport-v1.5', 18 | Sudoswap = 'sudoswap', 19 | X2Y2 = 'x2y2', 20 | PaymentProcessorV2 = 'payment-processor-v2', 21 | } 22 | 23 | /** 24 | * Orderbook where order is placed. Example: `Reservoir` 25 | */ 26 | export enum Orderbook { 27 | Blur = 'blur', 28 | LooksRare = 'looks-rare', 29 | Opensea = 'opensea', 30 | X2Y2 = 'x2y2', 31 | SELF = 'self', 32 | NftGo = 'nftgo', 33 | } 34 | -------------------------------------------------------------------------------- /src/types/post-order.ts: -------------------------------------------------------------------------------- 1 | import { OrderKind, Orderbook, OrderType } from './order'; 2 | import { SafeAny } from './safe-any'; 3 | 4 | /** 5 | * post order (listings & offers) 6 | */ 7 | export interface PostOrderReq { 8 | attribute?: Attribute; 9 | bulkData?: { 10 | kind: OrderKind; 11 | data: { 12 | orderIndex: number; 13 | merkleProof: string[]; 14 | }; 15 | }; 16 | collection?: string; 17 | extraArgs: { 18 | version: string; 19 | }; 20 | isNonFlagged?: boolean; 21 | order: { 22 | data: SafeAny; 23 | kind: OrderKind; 24 | }; 25 | orderType: OrderType; 26 | slug?: string; 27 | orderbook: Orderbook; 28 | orderbookApiKey?: string; 29 | source?: string; 30 | tokenSetId?: string; 31 | } 32 | 33 | export interface Attribute { 34 | collection: string; 35 | key: string; 36 | value: string; 37 | } 38 | 39 | export interface PostOrderResponse { 40 | message: string; 41 | } 42 | -------------------------------------------------------------------------------- /src/types/safe-any.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export type SafeAny = any; 3 | -------------------------------------------------------------------------------- /src/types/utils.ts: -------------------------------------------------------------------------------- 1 | import { ActionKind, ActionProcessor, ActionTaskExecutor, AggregatorAction } from './action'; 2 | import { ethers } from 'ethers'; 3 | import { Log, TransactionConfig, TransactionReceipt } from 'web3-core'; 4 | import { BlurAuthenticator, X2Y2Authenticator } from './authenticator'; 5 | import { SafeAny } from './safe-any'; 6 | 7 | export interface InternalUtils { 8 | blurAuthenticator: BlurAuthenticator; 9 | x2y2Authenticator: X2Y2Authenticator; 10 | 11 | createActionExecutor?: (actions: AggregatorAction[]) => ActionTaskExecutor; 12 | /** 13 | * Decode transaction log, return contract, token id, trading amount, buyer 14 | * - details: {@link } 15 | * @param log {@link Log} single log returned by send transaction method 16 | * @returns res {@link DecodeLogRes} 17 | */ 18 | decodeLog(log: Log): DecodeLogRes | null; 19 | processor?: ActionProcessor; 20 | /** 21 | * Send transaction with safe mode, using flash bot 22 | * - details: {@link } 23 | * @param transactionConfig {@link https://docs.ethers.io/v5/api/providers/types/#providers-TransactionRequest} transaction config 24 | * @returns transaction {@link Transaction} 25 | */ 26 | sendSafeModeTransaction(transactionConfig: Partial): Transaction; 27 | /** 28 | * Send transaction 29 | * - details: {@link } 30 | * @param transactionConfig {@link https://web3js.readthedocs.io/en/v1.8.2/web3-eth.html#sendtransaction} transaction config 31 | * @returns transaction {@link Transaction} 32 | */ 33 | sendTransaction(transactionConfig: TransactionConfig): Transaction; 34 | /** 35 | * inspect a transaction 36 | * - details: {@link } 37 | * @param params {@link InspectTransactionParams} transaction hash, inspect interval 38 | * @returns transaction {@link Transaction} 39 | */ 40 | inspectTransaction(params: InspectTransactionParams): Transaction; 41 | 42 | // standard sign message 43 | signMessage(message: string): Promise; 44 | 45 | // TODO: signer 46 | getSigner(): SafeAny; 47 | } 48 | 49 | export interface Utils extends InternalUtils { 50 | createActionExecutor(actions: AggregatorAction[]): ActionTaskExecutor; 51 | processor: ActionProcessor; 52 | } 53 | 54 | export interface DecodeLogRes { 55 | contract?: string; 56 | tokenId?: string; 57 | amount?: number; 58 | is1155?: boolean; 59 | to?: string; 60 | } 61 | 62 | export interface Transaction { 63 | on(type: 'transactionHash', handler: TransactionHashHandler): Transaction; 64 | 65 | on(type: 'receipt', handler: ReceiptHandler): Transaction; 66 | 67 | on(type: 'error', handler: ErrorHandler): Transaction; 68 | 69 | on( 70 | type: 'error' | 'receipt' | 'transactionHash', 71 | handler: (receipt: Error | TransactionReceipt | string | object) => void 72 | ): Transaction; 73 | finally(handler: FinallyHandler): void; 74 | } 75 | 76 | export interface DecodeLogRes { 77 | contract?: string; 78 | tokenId?: string; 79 | amount?: number; 80 | is1155?: boolean; 81 | to?: string; 82 | } 83 | 84 | export interface InspectTransactionParams { 85 | hash: string; 86 | interval?: number; // ms, 1000 as default 87 | } 88 | 89 | export type TransactionHashHandler = ((hash: string) => void) | null | undefined; 90 | 91 | export type ReceiptHandler = ((receipt: TransactionReceipt) => void) | null | undefined; 92 | 93 | export type ErrorHandler = ((error: Error) => void) | null | undefined; 94 | 95 | export type FinallyHandler = (() => void) | null | undefined; 96 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0xcEDe200b34F14fBc9eE33e10D41DCf1500e25603' 6 | - '0x361Ee7a1419Bc92cAFB6368544E0F22f21ccBA62' 7 | - '0xc9Df468E49357854B8892aF791fFDd337B2A1D78' 8 | - '0xEe8B5783323A3F9Ac5610AF56710eE18d2892F61' 9 | - '0xB045Bee0E6b5355b419D874BD8a1c7aC4032d1cF' 10 | - '0x888D8ACF5c5b1d6472D7C4A1f4e905197D449cA5' 11 | quorum: 1 12 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "docs", "__tests__", "**/*.test.ts"], 4 | "include": ["src"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "downlevelIteration": true, 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": false, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": false, 15 | "esModuleInterop": true, 16 | "module": "ESNext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": false, 20 | "strictNullChecks": true, 21 | "jsx": "preserve", 22 | "incremental": true, 23 | "declaration": true, 24 | "declarationDir": "dist/src", 25 | "outDir": "dist", 26 | "baseUrl": ".", 27 | "paths": { 28 | "@/types": ["src/types/index.ts"], 29 | "@/abi": ["src/abi/index.ts"], 30 | "@/exceptions": ["src/exceptions/index.ts"], 31 | "@/http": ["src/http/index.ts"], 32 | "@/common": ["src/common/index.ts"], 33 | } 34 | }, 35 | "include": [ 36 | "src", 37 | "__test__", 38 | "./node_modules/types-bn/index.d.ts", 39 | "./node_modules/types-ethereumjs-util/index.d.ts", 40 | ] 41 | } 42 | --------------------------------------------------------------------------------