initial commit, add bitget rest api and websockets connector

This commit is contained in:
Tiago Siebler
2022-10-09 23:01:08 +01:00
commit 0f75ded05c
59 changed files with 15246 additions and 0 deletions

36
.eslintrc.js Normal file
View File

@@ -0,0 +1,36 @@
module.exports = {
env: {
es6: true,
node: true,
},
extends: ['eslint:recommended'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 9
},
plugins: [],
rules: {
'array-bracket-spacing': ['error', 'never'],
indent: ['warn', 2],
'linebreak-style': ['error', 'unix'],
'lines-between-class-members': ['warn', 'always'],
semi: ['error', 'always'],
'new-cap': 'off',
'no-console': 'off',
'no-debugger': 'off',
'no-mixed-spaces-and-tabs': 2,
'no-use-before-define': [2, 'nofunc'],
'no-unreachable': ['warn'],
'no-unused-vars': ['warn'],
'no-extra-parens': ['off'],
'no-mixed-operators': ['off'],
quotes: [2, 'single', 'avoid-escape'],
'block-scoped-var': 2,
'brace-style': [2, '1tbs', { allowSingleLine: true }],
'computed-property-spacing': [2, 'never'],
'keyword-spacing': 2,
'space-unary-ops': 2,
'max-len': ['warn', { 'code': 140 }]
}
};

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [tiagosiebler] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

16
.github/workflow-settings.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"EXCLUDE_MESSAGES": [
"update package version",
"update packages",
"update wp version",
"trigger workflow",
"update TOC"
],
"PROJECT": "Backlog",
"ISSUE_COLUMN": "To do",
"PR_COLUMN": "In progress",
"PR_BODY_TITLE": "## Changes",
"TOC_FOLDING": "1",
"TOC_MAX_HEADER_LEVEL": "3",
"TOC_TITLE": "Summary"
}

36
.github/workflows/integrationtest.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: "Build & Test"
on: [push]
# on:
# # pull_request:
# # branches:
# # - "master"
# push:
# branches:
jobs:
build:
name: "Build & Test"
runs-on: ubuntu-latest
steps:
- name: "Checkout source code"
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm'
- name: Install
run: npm ci --ignore-scripts
- name: Build
run: npm run build
- name: Test
run: npm run test
env:
API_KEY_COM: ${{ secrets.API_KEY_COM }}
API_SECRET_COM: ${{ secrets.API_SECRET_COM }}

60
.github/workflows/npmpublish.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
name: Publish to NPM
on:
push:
branches:
- master
jobs:
publish-npm:
runs-on: ubuntu-latest
steps:
- name: Package Version Updated
uses: MontyD/package-json-updated-action@1.0.1
id: version-updated
with:
path: package.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v2
if: steps.version-updated.outputs.has-updated
- uses: actions/setup-node@v1
if: steps.version-updated.outputs.has-updated
with:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm ci --ignore-scripts
if: steps.version-updated.outputs.has-updated
- run: npm run clean
if: steps.version-updated.outputs.has-updated
- run: npm run build
if: steps.version-updated.outputs.has-updated
- run: npm publish --ignore-scripts
if: steps.version-updated.outputs.has-updated
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
# - name: Create Github Release
# if: steps.version-updated.outputs.has-updated
# id: create_release
# uses: ncipollo/release-action@v1
#publish-gpr:
#needs: build
#runs-on: ubuntu-latest
#steps:
#- uses: actions/checkout@v2
#- uses: actions/setup-node@v1
# with:
# node-version: 12
# registry-url: https://npm.pkg.github.com/
#- run: npm ci
#- run: npm publish
# env:
# NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
!.gitkeep
.DS_STORE
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
pids
*.pid
*.seed
*.pid.lock
node_modules/
.npm
.eslintcache
.node_repl_history
*.tgz
.yarn-integrity
.env
.env.test
.cache
lib
bundleReport.html
.history/

6
.jshintrc Normal file
View File

@@ -0,0 +1,6 @@
{
"esversion": 8,
"asi": true,
"laxbreak": true,
"predef": [ "-Promise" ]
}

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v16.17.1

4
.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"tabWidth": 2,
"singleQuote": true
}

7
LICENSE.md Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2022 Tiago Siebler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

167
README.md Normal file
View File

@@ -0,0 +1,167 @@
# bitget-api
[![Tests](https://circleci.com/gh/tiagosiebler/bitget-api.svg?style=shield)](https://circleci.com/gh/tiagosiebler/bitget-api)
[![npm version](https://img.shields.io/npm/v/bitget-api)][1] [![npm size](https://img.shields.io/bundlephobia/min/bitget-api/latest)][1] [![npm downloads](https://img.shields.io/npm/dt/bitget-api)][1]
[![last commit](https://img.shields.io/github/last-commit/tiagosiebler/bitget-api)][1]
[![CodeFactor](https://www.codefactor.io/repository/github/tiagosiebler/bitget-api/badge)](https://www.codefactor.io/repository/github/tiagosiebler/bitget-api)
[![connector logo](https://github.com/tiagosiebler/bitget-api/blob/master/docs/images/logo1.png?raw=true)][1]
[1]: https://www.npmjs.com/package/bitget-api
Node.js connector for the Bitget APIs and WebSockets:
- Complete integration with all Bitget APIs.
- TypeScript support (with type declarations for most API requests & responses).
- Over 100 integration tests making real API calls & WebSocket connections, validating any changes before they reach npm.
- Robust WebSocket integration with configurable connection heartbeats & automatic reconnect then resubscribe workflows.
- Browser support (via webpack bundle - see "Browser Usage" below).
## Installation
`npm install --save bitget-api`
## Issues & Discussion
- Issues? Check the [issues tab](https://github.com/tiagosiebler/bitget-api/issues).
- Discuss & collaborate with other node devs? Join our [Node.js Algo Traders](https://t.me/nodetraders) engineering community on telegram.
## Related projects
Check out my related projects:
- Try my connectors:
- [ftx-api](https://www.npmjs.com/package/ftx-api)
- [bybit-api](https://www.npmjs.com/package/bybit-api)
- [binance](https://www.npmjs.com/package/binance)
- [bitget-api](https://www.npmjs.com/package/bitget-api)
- [okx-api](https://www.npmjs.com/package/okx-api)
- Try my misc utilities:
- [orderbooks](https://www.npmjs.com/package/orderbooks)
- Check out my examples:
- [awesome-crypto-examples](https://github.com/tiagosiebler/awesome-crypto-examples)
## Documentation
Most methods pass values as-is into HTTP requests. These can be populated using parameters specified by Bitget's API documentation, or check the type definition in each class within this repository (see table below for convenient links to each class).
- [bitget API Documentation](https://www.bitget.com/docs-v5/en/#rest-api).
## Structure
This connector is fully compatible with both TypeScript and pure JavaScript projects, while the connector is written in TypeScript. A pure JavaScript version can be built using `npm run build`, which is also the version published to [npm](https://www.npmjs.com/package/bitget-api).
The version on npm is the output from the `build` command and can be used in projects without TypeScript (although TypeScript is definitely recommended).
- [src](./src) - the whole connector written in TypeScript
- [lib](./lib) - the JavaScript version of the project (built from TypeScript). This should not be edited directly, as it will be overwritten with each release.
- [dist](./dist) - the webpack bundle of the project for use in browser environments (see guidance on webpack below).
- [examples](./examples) - some implementation examples & demonstrations. Contributions are welcome!
---
## REST API Clients
Each REST API group has a dedicated REST client. To avoid confusion, here are the available REST clients and the corresponding API groups:
| Class | Description |
|:------------------------------------: |:---------------------------------------------------------------------------------------------: |
| [SpotClient](src/spot-client.ts) | [Spot APIs](https://bitgetlimited.github.io/apidoc/en/spot/#introduction) |
| [FuturesClient](src/futures-client.ts) | [Futures APIs](https://bitgetlimited.github.io/apidoc/en/mix/#introduction) |
| [BrokerClient](src/broker-client.ts) | [Broker APIs](https://bitgetlimited.github.io/apidoc/en/broker/#introduction) |
| [WebsocketClient](src/websocket-client.ts) | Universal client for all Bitget's Websockets |
Examples for using each client can be found in:
- the [examples](./examples) folder.
- the [awesome-crypto-examples](https://github.com/tiagosiebler/awesome-crypto-examples) repository.
If you're missing an example, you're welcome to request one. Priority will be given to [github sponsors](https://github.com/sponsors/tiagosiebler).
### Usage
First, create API credentials on Bitget's website.
All REST clients have can be used in a similar way. However, method names, parameters and responses may vary depending on the API category you're using!
Not sure which function to call or which parameters to use? Click the class name in the table above to look at all the function names (they are in the same order as the official API docs), and check the API docs for a list of endpoints/paramters/responses.
```javascript
const {
SpotClient,
FuturesClient,
BrokerClient,
} = require('bitget-api');
const API_KEY = 'xxx';
const API_SECRET = 'yyy';
const API_PASS = 'zzz';
const client = new SpotClient({
apiKey: API_KEY,
apiSecret: API_SECRET,
apiPass: API_PASS,
},
// requestLibraryOptions
);
// For public-only API calls, simply don't provide a key & secret or set them to undefined
// const client = new SpotClient();
client.getApiKeyInfo()
.then(result => {
console.log("getApiKeyInfo result: ", result);
})
.catch(err => {
console.error("getApiKeyInfo error: ", err);
});
const symbol = 'BTCUSDT_SPBL';
client.getCandles(symbol, '1min');
.then(result => {
console.log("getCandles result: ", result);
})
.catch(err => {
console.error("getCandles error: ", err);
});
```
#### WebSockets
For more examples, including how to use websockets with bitget, check the [examples](./examples/) and [test](./test/) folders.
---
## Customise Logging
Pass a custom logger which supports the log methods `silly`, `debug`, `notice`, `info`, `warning` and `error`, or override methods from the default logger as desired.
```javascript
const { WebsocketClient, DefaultLogger } = require('bitget-api');
// Disable all logging on the silly level
DefaultLogger.silly = () => {};
const ws = new WebsocketClient(
{
apiKey: 'API_KEY',
apiSecret: 'API_SECRET',
apiPass: 'API_PASS',
},
DefaultLogger
);
```
## Browser Usage
Build a bundle using webpack:
- `npm install`
- `npm build`
- `npm pack`
The bundle can be found in `dist/`. Altough usage should be largely consistent, smaller differences will exist. Documentation is still TODO.
---
## Contributions & Thanks
### Donations
#### tiagosiebler
Support my efforts to make algo trading accessible to all - register with my referral links:
- [Bybit](https://www.bybit.com/en-US/register?affiliate_id=9410&language=en-US&group_id=0&group_type=1)
- [Binance](https://www.binance.com/en/register?ref=20983262)
- [Bitget](https://www.bitget.com/join/18504944)
- [FTX](https://ftx.com/referrals#a=ftxapigithub)
Or buy me a coffee using any of these:
- BTC: `1C6GWZL1XW3jrjpPTS863XtZiXL1aTK7Jk`
- ETH (ERC20): `0xd773d8e6a50758e1ada699bb6c4f98bb4abf82da`
### Contributions & Pull Requests
Contributions are encouraged, I will review any incoming pull requests. See the issues tab for todo items.

BIN
docs/images/logo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

6
examples/README.md Normal file
View File

@@ -0,0 +1,6 @@
# Examples
These samples can be executed using `ts-node`:
```
ts-node ./examples/rest-spot-public.ts
```

View File

@@ -0,0 +1,16 @@
import { SpotClient } from '../src/index';
// or
// import { SpotClient } from 'bitget-api';
const client = new SpotClient();
const symbol = 'BTCUSDT_SPBL';
(async () => {
try {
console.log('getCandles: ', await client.getCandles(symbol, '1min'));
} catch (e) {
console.error('request failed: ', e);
}
})();

73
examples/ws-private.ts Normal file
View File

@@ -0,0 +1,73 @@
import { WebsocketClient, DefaultLogger } from '../src';
// or
// import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from 'bitget-api';
(async () => {
const logger = {
...DefaultLogger,
silly: (...params) => console.log('silly', ...params),
};
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const API_PASS = process.env.API_PASS_COM;
const wsClient = new WebsocketClient(
{
apiKey: API_KEY,
apiSecret: API_SECRET,
apiPass: API_PASS,
// restOptions: {
// optionally provide rest options, e.g. to pass through a proxy
// },
},
logger
);
wsClient.on('update', (data) => {
console.log('WS raw message received ', data);
// console.log('WS raw message received ', JSON.stringify(data, null, 2));
});
wsClient.on('open', (data) => {
console.log('WS connection opened:', data.wsKey);
});
wsClient.on('response', (data) => {
console.log('WS response: ', JSON.stringify(data, null, 2));
});
wsClient.on('reconnect', ({ wsKey }) => {
console.log('WS automatically reconnecting.... ', wsKey);
});
wsClient.on('reconnected', (data) => {
console.log('WS reconnected ', data?.wsKey);
});
// auth happens async after the ws connection opens
wsClient.on('authenticated', (data) => {
console.log('WS authenticated', data);
// wsClient.subscribePublicSpotTickers(['BTCUSDT', 'LTCUSDT']);
});
wsClient.on('exception', (data) => {
console.log('WS error', data);
});
/**
* Private account updates
*/
// spot private
// : account updates
// wsClient.subscribeTopic('SPBL', 'account');
// : order updates
// wsClient.subscribeTopic('SPBL', 'orders');
// futures private
// : account updates
// wsClient.subscribeTopic('UMCBL', 'account');
// // : position updates
// wsClient.subscribeTopic('UMCBL', 'positions');
// // : order updates
// wsClient.subscribeTopic('UMCBL', 'orders');
// // : plan order updates
// wsClient.subscribeTopic('UMCBL', 'ordersAlgo');
})();

76
examples/ws-public.ts Normal file
View File

@@ -0,0 +1,76 @@
import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src';
// or
// import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from 'bitget-api';
(async () => {
const logger = {
...DefaultLogger,
silly: (...params) => console.log('silly', ...params),
};
const wsClient = new WebsocketClient(
{
// restOptions: {
// optionally provide rest options, e.g. to pass through a proxy
// },
},
logger
);
wsClient.on('update', (data) => {
console.log('WS raw message received ', data);
// console.log('WS raw message received ', JSON.stringify(data, null, 2));
});
wsClient.on('open', (data) => {
console.log('WS connection opened:', data.wsKey);
});
wsClient.on('response', (data) => {
console.log('WS response: ', JSON.stringify(data, null, 2));
});
wsClient.on('reconnect', ({ wsKey }) => {
console.log('WS automatically reconnecting.... ', wsKey);
});
wsClient.on('reconnected', (data) => {
console.log('WS reconnected ', data?.wsKey);
});
wsClient.on('exception', (data) => {
console.log('WS error', data);
});
/**
* Public events
*/
const symbol = 'BTCUSDT';
// // Spot public
// // tickers
// wsClient.subscribeTopic('SP', 'ticker', symbol);
// // candles
// wsClient.subscribeTopic('SP', 'candle1m', symbol);
// // orderbook updates
wsClient.subscribeTopic('SP', 'books', symbol);
// // trades
// wsClient.subscribeTopic('SP', 'trade', symbol);
// // Futures public
// // tickers
// wsClient.subscribeTopic('MC', 'ticker', symbol);
// // candles
// wsClient.subscribeTopic('MC', 'candle1m', symbol);
// // orderbook updates
// wsClient.subscribeTopic('MC', 'books', symbol);
// // trades
// wsClient.subscribeTopic('MC', 'trade', symbol);
// Topics are tracked per websocket type
// Get a list of subscribed topics (e.g. for spot topics) (after a 5 second delay)
setTimeout(() => {
const publicSpotTopics = wsClient.getWsStore().getTopics(WS_KEY_MAP.spotv1);
console.log('public spot topics: ', publicSpotTopics);
}, 5 * 1000);
})();

1
index.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require('lib/index');

28
jest.config.js Normal file
View File

@@ -0,0 +1,28 @@
// jest.config.js
module.exports = {
rootDir: './',
globals: {
__DEV__: true,
__PROD__: false
},
testEnvironment: 'node',
preset: "ts-jest",
verbose: true, // report individual test
bail: false, // enable to stop test when an error occur,
detectOpenHandles: false,
moduleDirectories: ['node_modules', 'src', 'test'],
testMatch: ['**/test/**/*.test.ts?(x)'],
testPathIgnorePatterns: ['node_modules/', 'dist/', '.json'],
collectCoverageFrom: [
'src/**/*.ts'
],
coverageThreshold: {
// coverage strategy
global: {
branches: 80,
functions: 80,
lines: 50,
statements: -10
}
}
};

12
jsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs"
},
"exclude": [
"node_modules",
"**/node_modules/*",
"coverage",
"doc"
]
}

9640
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

73
package.json Normal file
View File

@@ -0,0 +1,73 @@
{
"name": "bitget-api",
"version": "0.9.0",
"description": "Node.js connector for Bitget REST APIs and WebSockets, with TypeScript & integration tests.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib/*",
"index.js"
],
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"clean": "rm -rf lib dist",
"build": "tsc",
"build:clean": "npm run clean && npm run build",
"build:watch": "npm run clean && tsc --watch",
"pack": "webpack --config webpack/webpack.config.js",
"prepublish": "npm run build:clean",
"betapublish": "npm publish --tag beta"
},
"author": "Tiago Siebler (https://github.com/tiagosiebler)",
"contributors": [],
"dependencies": {
"axios": "^0.27.2",
"isomorphic-ws": "^5.0.0",
"ws": "^8.9.0"
},
"devDependencies": {
"@types/jest": "^29.0.3",
"@types/node": "^18.7.23",
"eslint": "^8.24.0",
"jest": "^29.1.1",
"source-map-loader": "^4.0.0",
"ts-jest": "^29.0.2",
"ts-loader": "^9.4.1",
"typescript": "^4.8.4",
"webpack": "^5.74.0",
"webpack-bundle-analyzer": "^4.6.1",
"webpack-cli": "^4.10.0"
},
"keywords": [
"bitget",
"bitget api",
"okex",
"okex api",
"api",
"websocket",
"rest",
"rest api",
"usdt",
"trading bots",
"nodejs",
"node",
"trading",
"cryptocurrency",
"bitcoin",
"best"
],
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/tiagosiebler"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/tiagosiebler/bitget-api"
},
"bugs": {
"url": "https://github.com/tiagosiebler/bitget-api/issues"
},
"homepage": "https://github.com/tiagosiebler/bitget-api#readme"
}

155
src/broker-client.ts Normal file
View File

@@ -0,0 +1,155 @@
import {
APIResponse,
BrokerProductType,
BrokerSubWithdrawalRequest,
BrokerSubAPIKeyModifyRequest,
BrokerSubListRequest,
} from './types';
import { REST_CLIENT_TYPE_ENUM } from './util';
import BaseRestClient from './util/BaseRestClient';
/**
* REST API client for broker APIs
*/
export class BrokerClient extends BaseRestClient {
getClientType() {
return REST_CLIENT_TYPE_ENUM.broker;
}
/**
*
* Sub Account Interface
*
*/
/** Get Broker Info */
getBrokerInfo(): Promise<APIResponse<any>> {
return this.getPrivate('/api/broker/v1/account/info');
}
/** Create Sub Account */
createSubAccount(
subName: string,
remark?: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/broker/v1/account/sub-create', {
subName,
remark,
});
}
/** Get Sub List */
getSubAccounts(params?: BrokerSubListRequest): Promise<APIResponse<any>> {
return this.getPrivate('/api/broker/v1/account/sub-list', params);
}
/** Modify Sub Account */
modifySubAccount(
subUid: string,
perm: string,
status: 'normal' | 'freeze' | 'del'
): Promise<APIResponse<any>> {
return this.postPrivate('/api/broker/v1/account/sub-modify', {
subUid,
perm,
status,
});
}
/** Modify Sub Email */
modifySubEmail(subUid: string, subEmail: string): Promise<APIResponse<any>> {
return this.postPrivate('/api/broker/v1/account/sub-modify-email', {
subUid,
subEmail,
});
}
/** Get Sub Email */
getSubEmail(subUid: string): Promise<APIResponse<any>> {
return this.getPrivate('/api/broker/v1/account/sub-email', { subUid });
}
/** Get Sub Spot Assets */
getSubSpotAssets(subUid: string): Promise<APIResponse<any>> {
return this.getPrivate('/api/broker/v1/account/sub-spot-assets', {
subUid,
});
}
/** Get Sub Future Assets */
getSubFutureAssets(
subUid: string,
productType: BrokerProductType
): Promise<APIResponse<any>> {
return this.getPrivate('/api/broker/v1/account/sub-future-assets', {
subUid,
productType,
});
}
/** Get Sub Deposit Address (Only Broker) */
getSubDepositAddress(
subUid: string,
coin: string,
chain?: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/broker/v1/account/sub-address', {
subUid,
coin,
chain,
});
}
/** Sub Withdrawal (Only Broker) */
subWithdrawal(params: BrokerSubWithdrawalRequest): Promise<APIResponse<any>> {
return this.postPrivate('/api/broker/v1/account/sub-withdrawal', params);
}
/** Sub Deposit Auto Transfer (Only Broker) */
setSubDepositAutoTransfer(
subUid: string,
coin: string,
toAccountType: 'spot' | 'mix_usdt' | 'mix_usd' | 'mix_usdc'
): Promise<APIResponse<any>> {
return this.postPrivate('/api/broker/v1/account/sub-auto-transfer', {
subUid,
coin,
toAccountType,
});
}
/**
*
* Sub API Interface
*
*/
/** Create Sub ApiKey (Only Broker) */
createSubAPIKey(
subUid: string,
passphrase: string,
remark: string,
ip: string,
perm?: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/broker/v1/manage/sub-api-create', {
subUid,
passphrase,
remark,
ip,
perm,
});
}
/** Get Sub ApiKey List */
getSubAPIKeys(subUid: string): Promise<APIResponse<any>> {
return this.getPrivate('/api/broker/v1/manage/sub-api-list', { subUid });
}
/** Modify Sub ApiKey (Only Broker) */
modifySubAPIKey(
params: BrokerSubAPIKeyModifyRequest
): Promise<APIResponse<any>> {
return this.postPrivate('/api/broker/v1/manage/sub-api-modify', params);
}
}

17
src/constants/enum.ts Normal file
View File

@@ -0,0 +1,17 @@
export const API_ERROR_CODE = {
SUCCESS: '00000',
INCORRECT_PERMISSIONS: '40014',
ACCOUNT_NOT_COPY_TRADER: '40017',
ACCOUNT_NOT_BROKER: '40029',
FUTURES_ORDER_GET_NOT_FOUND: '40109',
FUTURES_ORDER_CANCEL_NOT_FOUND: '40768',
PLAN_ORDER_NOT_FOUND: '43025',
QTY_LESS_THAN_MINIMUM: '43006',
ORDER_NOT_FOUND: '43001',
/** Parameter verification exception margin mode == FIXED */
PARAMETER_EXCEPTION: '40808',
INSUFFICIENT_BALANCE: '40754',
SERVICE_RETURNED_ERROR: '40725',
FUTURES_POSITION_DIRECTION_EMPTY: '40017',
FUTURES_ORDER_TPSL_NOT_FOUND: '43020',
} as const;

593
src/futures-client.ts Normal file
View File

@@ -0,0 +1,593 @@
import {
APIResponse,
KlineInterval,
FuturesProductType,
FuturesAccountBillRequest,
FuturesBusinessBillRequest,
NewFuturesOrder,
NewBatchFuturesOrder,
FuturesPagination,
NewFuturesPlanOrder,
ModifyFuturesPlanOrder,
ModifyFuturesPlanOrderTPSL,
NewFuturesPlanPositionTPSL,
ModifyFuturesPlanStopOrder,
CancelFuturesPlanTPSL,
HistoricPlanOrderTPSLRequest,
NewFuturesPlanStopOrder,
} from './types';
import { REST_CLIENT_TYPE_ENUM } from './util';
import BaseRestClient from './util/BaseRestClient';
/**
* REST API client
*/
export class FuturesClient extends BaseRestClient {
getClientType() {
return REST_CLIENT_TYPE_ENUM.futures;
}
/**
*
* Market
*
*/
/** Get Symbols : Get basic configuration information of all trading pairs (including rules) */
getSymbols(productType: FuturesProductType): Promise<APIResponse<any[]>> {
return this.get('/api/mix/v1/market/contracts', { productType });
}
/** Get Depth */
getDepth(symbol: string, limit?: string): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/depth', { symbol, limit });
}
/** Get Single Symbol Ticker */
getTicker(symbol: string): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/ticker', { symbol });
}
/** Get All Tickers */
getAllTickers(productType: FuturesProductType): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/tickers', { productType });
}
/** Get Market Trades */
getMarketTrades(symbol: string, limit?: string): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/fills', { symbol, limit });
}
/** Get Candle Data */
getCandles(
symbol: string,
granularity: KlineInterval,
startTime: string,
endTime: string
): Promise<any> {
return this.get('/api/mix/v1/market/candles', {
symbol,
granularity,
startTime,
endTime,
});
}
/** Get symbol index price */
getIndexPrice(symbol: string): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/index', { symbol });
}
/** Get symbol next funding time */
getNextFundingTime(symbol: string): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/funding-time', { symbol });
}
/** Get Withdraw List */
getHistoricFundingRate(
symbol: string,
pageSize?: string,
pageNo?: string,
nextPage?: boolean
): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/history-fundRate', {
symbol,
nextPage,
pageSize,
pageNo,
});
}
/** Get symbol current funding time */
getCurrentFundingRate(symbol: string): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/current-fundRate', { symbol });
}
/** Get symbol open interest */
getOpenInterest(symbol: string): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/open-interest', { symbol });
}
/** Get symbol mark price */
getMarkPrice(symbol: string): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/mark-price', { symbol });
}
/** Get symbol min/max leverage rules */
getLeverageMinMax(symbol: string): Promise<APIResponse<any>> {
return this.get('/api/mix/v1/market/symbol-leverage', { symbol });
}
/**
*
* Account Endpoints
*
*/
/** Get Single Account */
getAccount(symbol: string, marginCoin: string): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/account/account', {
symbol,
marginCoin,
});
}
/** Get Account List */
getAccounts(productType: FuturesProductType): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/account/accounts', { productType });
}
/**
* This interface is only used to calculate the maximum number of positions that can be opened when the user does not hold a position by default.
* The result does not represent the actual number of positions opened.
*/
getOpenCount(
symbol: string,
marginCoin: string,
openPrice: number,
openAmount: number,
leverage?: number
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/account/open-count', {
symbol,
marginCoin,
openPrice,
openAmount,
leverage,
});
}
/** Change Leverage */
setLeverage(
symbol: string,
marginCoin: string,
leverage: string,
holdSide?: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/account/setLeverage', {
symbol,
marginCoin,
leverage,
holdSide,
});
}
/** Change Margin */
setMargin(
symbol: string,
marginCoin: string,
amount: string,
holdSide?: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/account/setMargin', {
symbol,
marginCoin,
amount,
holdSide,
});
}
/** Change Margin Mode */
setMarginMode(
symbol: string,
marginCoin: string,
marginMode: 'fixed' | 'crossed'
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/account/setMarginMode', {
symbol,
marginCoin,
marginMode,
});
}
/** Get Symbol Position */
getPosition(symbol: string, marginCoin?: string): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/position/singlePosition', {
symbol,
marginCoin,
});
}
/** Get All Position */
getPositions(
productType: FuturesProductType,
marginCoin?: string
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/position/allPosition', {
productType,
marginCoin,
});
}
/** Get Account Bill */
getAccountBill(params: FuturesAccountBillRequest): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/account/accountBill', params);
}
/** Get Business Account Bill */
getBusinessBill(
params: FuturesBusinessBillRequest
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/account/accountBusinessBill', params);
}
/**
*
* Trade Endpoints
*
*/
/** Place Order */
submitOrder(params: NewFuturesOrder): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/order/placeOrder', params);
}
/** Batch Order */
batchSubmitOrder(
symbol: string,
marginCoin: string,
orders: NewBatchFuturesOrder[]
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/order/batch-orders', {
symbol,
marginCoin,
orderDataList: orders,
});
}
/** Cancel Order */
cancelOrder(
symbol: string,
marginCoin: string,
orderId: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/order/cancel-order', {
symbol,
marginCoin,
orderId,
});
}
/** Batch Cancel Order */
batchCancelOrder(
symbol: string,
marginCoin: string,
orderIds: string[]
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/order/cancel-batch-orders', {
symbol,
marginCoin,
orderIds,
});
}
/** Cancel All Order */
cancelAllOrders(
productType: FuturesProductType,
marginCoin: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/order/cancel-all-orders', {
productType,
marginCoin,
});
}
/** Get Open Order */
getOpenSymbolOrders(symbol: string): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/order/current', { symbol });
}
/** Get All Open Order */
getOpenOrders(
productType: FuturesProductType,
marginCoin: string
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/order/marginCoinCurrent', {
productType,
marginCoin,
});
}
/** Get History Orders */
getOrderHistory(
symbol: string,
startTime: string,
endTime: string,
pageSize: string,
lastEndId?: string,
isPre?: boolean
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/order/history', {
symbol,
startTime,
endTime,
pageSize,
lastEndId,
isPre,
});
}
/** Get ProductType History Orders */
getProductTypeOrderHistory(
productType: FuturesProductType,
startTime: string,
endTime: string,
pageSize: string,
lastEndId?: string,
isPre?: boolean
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/order/historyProductType', {
productType,
startTime,
endTime,
pageSize,
lastEndId,
isPre,
});
}
/** Get order details */
getOrder(
symbol: string,
orderId?: string,
clientOid?: string
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/order/detail', {
symbol,
orderId,
clientOid,
});
}
/** Get transaction details / history (fills) */
getOrderFills(
symbol: string,
orderId?: string,
pagination?: FuturesPagination
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/order/fills', {
symbol,
orderId,
...pagination,
});
}
/** Get ProductType Order fill detail */
getProductTypeOrderFills(
productType: FuturesProductType,
pagination?: FuturesPagination
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/order/allFills', {
productType: productType.toUpperCase(),
...pagination,
});
}
/** Place Plan order */
submitPlanOrder(params: NewFuturesPlanOrder): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/plan/placePlan', params);
}
/** Modify Plan Order */
modifyPlanOrder(params: ModifyFuturesPlanOrder): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/plan/modifyPlan', params);
}
/** Modify Plan Order TPSL */
modifyPlanOrderTPSL(
params: ModifyFuturesPlanOrderTPSL
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/plan/modifyPlanPreset', params);
}
/** Place Stop order */
submitStopOrder(params: NewFuturesPlanStopOrder): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/plan/placeTPSL', params);
}
/** Place Position TPSL */
submitPositionTPSL(
params: NewFuturesPlanPositionTPSL
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/plan/placePositionsTPSL', params);
}
/** Modify Stop Order */
modifyStopOrder(
params: ModifyFuturesPlanStopOrder
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/plan/modifyTPSLPlan', params);
}
/** Cancel Plan Order TPSL */
cancelPlanOrderTPSL(
params: CancelFuturesPlanTPSL
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/plan/cancelPlan', params);
}
/** Get Plan Order (TPSL) List */
getPlanOrderTPSLs(
symbol: string,
isPlan?: string,
productType?: FuturesProductType
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/plan/currentPlan', {
symbol,
isPlan,
productType,
});
}
/** Get History Plan Orders (TPSL) */
getHistoricPlanOrdersTPSL(
params: HistoricPlanOrderTPSLRequest
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/plan/historyPlan', params);
}
/**
*
* Trade Endpoints
*
*/
/** Get Trader Open order */
getCopyTraderOpenOrder(
symbol: string,
productType: FuturesProductType,
pageSize: number,
pageNo: number
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/trace/currentTrack', {
symbol,
productType,
pageSize,
pageNo,
});
}
/** Get Followers Open Order */
getCopyFollowersOpenOrder(
symbol: string,
productType: FuturesProductType,
pageSize: number,
pageNo: number
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/trace/followerOrder', {
symbol,
productType,
pageSize,
pageNo,
});
}
/** Trader Close Position */
closeCopyTraderPosition(
symbol: string,
trackingNo: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/trace/closeTrackOrder', {
symbol,
trackingNo,
});
}
/** Trader Modify TPSL */
modifyCopyTraderTPSL(
symbol: string,
trackingNo: string,
changes?: {
stopProfitPrice?: number;
stopLossPrice?: number;
}
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/trace/modifyTPSL', {
symbol,
trackingNo,
...changes,
});
}
/** Get Traders History Orders */
getCopyTraderOrderHistory(
startTime: string,
endTime: string,
pageSize: number,
pageNo: number
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/trace/historyTrack', {
startTime,
endTime,
pageSize,
pageNo,
});
}
/** Get Trader Profit Summary */
getCopyTraderProfitSummary(): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/trace/summary');
}
/** Get Trader History Profit Summary (according to settlement currency) */
getCopyTraderHistoricProfitSummary(): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/trace/profitSettleTokenIdGroup');
}
/** Get Trader History Profit Summary (according to settlement currency and date) */
getCopyTraderHistoricProfitSummaryByDate(
marginCoin: string,
dateMs: string,
pageSize: number,
pageNo: number
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/trace/profitDateGroupList', {
marginCoin,
date: dateMs,
pageSize,
pageNo,
});
}
/** Get Trader Histroy Profit Detail */
getCopyTraderHistoricProfitDetail(
marginCoin: string,
dateMs: string,
pageSize: number,
pageNo: number
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/trace/profitDateList', {
marginCoin,
date: dateMs,
pageSize,
pageNo,
});
}
/** Get Trader Profits Details */
getCopyTraderProfitDetails(
pageSize: number,
pageNo: number
): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/trace/waitProfitDateList', {
pageSize,
pageNo,
});
}
/** Get CopyTrade Symbols */
getCopyTraderSymbols(): Promise<APIResponse<any>> {
return this.getPrivate('/api/mix/v1/trace/traderSymbols');
}
/** Trader Change CopyTrade symbol */
setCopyTraderSymbols(
symbol: string,
operation: 'add' | 'delete'
): Promise<APIResponse<any>> {
return this.postPrivate('/api/mix/v1/trace/setUpCopySymbols', {
symbol,
operation,
});
}
}

8
src/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export * from './broker-client';
export * from './futures-client';
export * from './spot-client';
export * from './websocket-client';
export * from './util/logger';
export * from './util';
export * from './types';
export * from './constants/enum';

296
src/spot-client.ts Normal file
View File

@@ -0,0 +1,296 @@
import {
NewBatchSpotOrder,
NewSpotOrder,
NewWalletTransfer,
Pagination,
APIResponse,
KlineInterval,
} from './types';
import { REST_CLIENT_TYPE_ENUM } from './util';
import BaseRestClient from './util/BaseRestClient';
/**
* REST API client
*/
export class SpotClient extends BaseRestClient {
getClientType() {
return REST_CLIENT_TYPE_ENUM.spot;
}
async fetchServerTime(): Promise<number> {
const res = await this.getServerTime();
return Number(res.data);
}
/**
*
* Public
*
*/
/** Get Server Time */
getServerTime(): Promise<APIResponse<string>> {
return this.get('/api/spot/v1/public/time');
}
/** Get Coin List : Get all coins information on the platform */
getCoins(): Promise<APIResponse<any[]>> {
return this.get('/api/spot/v1/public/currencies');
}
/** Get Symbols : Get basic configuration information of all trading pairs (including rules) */
getSymbols(): Promise<APIResponse<any[]>> {
return this.get('/api/spot/v1/public/products');
}
/** Get Single Symbol : Get basic configuration information for one symbol */
getSymbol(symbol: string): Promise<APIResponse<any>> {
return this.get('/api/spot/v1/public/product', { symbol });
}
/**
*
* Market
*
*/
/** Get Single Ticker */
getTicker(symbol: string): Promise<APIResponse<any>> {
return this.get('/api/spot/v1/market/ticker', { symbol });
}
/** Get All Tickers */
getAllTickers(): Promise<APIResponse<any>> {
return this.get('/api/spot/v1/market/tickers');
}
/** Get Market Trades */
getMarketTrades(symbol: string, limit?: string): Promise<APIResponse<any>> {
return this.get('/api/spot/v1/market/fills', { symbol, limit });
}
/** Get Candle Data */
getCandles(
symbol: string,
period: KlineInterval,
pagination?: Pagination
): Promise<APIResponse<any>> {
return this.get('/api/spot/v1/market/candles', {
symbol,
period,
...pagination,
});
}
/** Get Depth */
getDepth(
symbol: string,
type: 'step0' | 'step1' | 'step2' | 'step3' | 'step4' | 'step5',
limit?: string
): Promise<APIResponse<any>> {
return this.get('/api/spot/v1/market/depth', { symbol, type, limit });
}
/**
*
* Wallet Endpoints
*
*/
/** Initiate wallet transfer */
transfer(params: NewWalletTransfer): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/wallet/transfer', params);
}
/** Get Coin Address */
getDepositAddress(coin: string, chain?: string): Promise<APIResponse<any>> {
return this.getPrivate('/api/spot/v1/wallet/deposit-address', {
coin,
chain,
});
}
/** Withdraw Coins On Chain*/
withdraw(params: {
coin: string;
address: string;
chain: string;
tag?: string;
amount: string;
remark?: string;
clientOid?: string;
}): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/wallet/withdrawal', params);
}
/** Inner Withdraw : Internal withdrawal means that both users are on the Bitget platform */
innerWithdraw(
coin: string,
toUid: string,
amount: string,
clientOid?: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/wallet/withdrawal-inner', {
coin,
toUid,
amount,
clientOid,
});
}
/** Get Withdraw List */
getWithdrawals(
coin: string,
startTime: string,
endTime: string,
pageSize?: string,
pageNo?: string
): Promise<APIResponse<any>> {
return this.getPrivate('/api/spot/v1/wallet/withdrawal-list', {
coin,
startTime,
endTime,
pageSize,
pageNo,
});
}
/** Get Deposit List */
getDeposits(
coin: string,
startTime: string,
endTime: string,
pageSize?: string,
pageNo?: string
): Promise<APIResponse<any>> {
return this.getPrivate('/api/spot/v1/wallet/deposit-list', {
coin,
startTime,
endTime,
pageSize,
pageNo,
});
}
/**
*
* Account Endpoints
*
*/
/** Get ApiKey Info */
getApiKeyInfo(): Promise<APIResponse<any>> {
return this.getPrivate('/api/spot/v1/account/getInfo');
}
/** Get Account : get account assets */
getBalance(coin?: string): Promise<APIResponse<any>> {
return this.getPrivate('/api/spot/v1/account/assets', { coin });
}
/** Get Bills : get transaction detail flow */
getTransactionHistory(params?: {
coinId?: number;
groupType?: string;
bizType?: string;
after?: string;
before?: string;
limit?: number;
}): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/account/bills', params);
}
/** Get Transfer List */
getTransferHistory(params?: {
coinId?: number;
fromType?: string;
after?: string;
before?: string;
limit?: number;
}): Promise<APIResponse<any>> {
return this.getPrivate('/api/spot/v1/account/transferRecords', params);
}
/**
*
* Trade Endpoints
*
*/
/** Place order */
submitOrder(params: NewSpotOrder): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/orders', params);
}
/** Place orders in batches, up to 50 at a time */
batchSubmitOrder(
symbol: string,
orderList: NewBatchSpotOrder[]
): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/batch-orders', {
symbol,
orderList,
});
}
/** Cancel order */
cancelOrder(symbol: string, orderId: string): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/cancel-order', {
symbol,
orderId,
});
}
/** Cancel order in batch (per symbol) */
batchCancelOrder(
symbol: string,
orderIds: string[]
): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/cancel-batch-orders', {
symbol,
orderIds,
});
}
/** Get order details */
getOrder(
symbol: string,
orderId: string,
clientOrderId?: string
): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/orderInfo', {
symbol,
orderId,
clientOrderId,
});
}
/** Get order list (open orders) */
getOpenOrders(symbol?: string): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/open-orders', { symbol });
}
/** Get order history for a symbol */
getOrderHistory(
symbol: string,
pagination?: Pagination
): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/history', {
symbol,
...pagination,
});
}
/** Get transaction details / history (fills) for an order */
getOrderFills(
symbol: string,
orderId: string,
pagination?: Pagination
): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/fills', {
symbol,
orderId,
...pagination,
});
}
}

4
src/types/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './response';
export * from './request';
export * from './shared';
export * from './websockets';

View File

@@ -0,0 +1,32 @@
export type BrokerProductType =
| 'umcbl'
| 'usdt'
| 'swap'
| 'dmcbl'
| 'mix'
| 'swap';
export interface BrokerSubListRequest {
pageSize?: string;
lastEndId?: number;
status?: string;
}
export interface BrokerSubWithdrawalRequest {
subUid: string;
coin: string;
address: string;
chain: string;
tag?: string;
amount: string;
remark?: string;
clientOid?: string;
}
export interface BrokerSubAPIKeyModifyRequest {
subUid: string;
apikey: string;
remark?: string;
ip?: string;
perm?: string;
}

View File

@@ -0,0 +1,136 @@
export type FuturesProductType =
| 'umcbl'
| 'dmcbl'
| 'cmcbl'
| 'sumcbl'
| 'sdmcbl'
| 'scmcbl';
export interface FuturesAccountBillRequest {
symbol: string;
marginCoin: string;
startTime: string;
endTime: string;
pageSize?: number;
lastEndId?: string;
next?: boolean;
}
export interface FuturesBusinessBillRequest {
productType: FuturesProductType;
startTime: string;
endTime: string;
pageSize?: number;
lastEndId?: string;
next?: boolean;
}
export type FuturesOrderType = 'limit' | 'market';
export type FuturesOrderSide =
| 'open_long'
| 'open_short'
| 'close_long'
| 'close_short';
export interface NewFuturesOrder {
symbol: string;
marginCoin: string;
size: string;
price?: string;
side: FuturesOrderSide;
orderType: FuturesOrderType;
timeInForceValue?: string;
clientOid?: string;
presetTakeProfitPrice?: string;
presetStopLossPrice?: string;
}
export interface NewBatchFuturesOrder {
size: string;
price?: string;
side: string;
orderType: string;
timeInForceValue?: string;
clientOid?: string;
}
export interface FuturesPagination {
startTime?: string;
endTime?: string;
lastEndId?: string;
}
export interface NewFuturesPlanOrder {
symbol: string;
marginCoin: string;
size: string;
executePrice?: string;
triggerPrice: string;
triggerType: 'fill_price' | 'market_price';
side: FuturesOrderSide;
orderType: FuturesOrderType;
clientOid?: string;
presetTakeProfitPrice?: string;
presetStopLossPrice?: string;
}
export interface ModifyFuturesPlanOrder {
orderId: string;
marginCoin: string;
symbol: string;
executePrice?: string;
triggerPrice: string;
triggerType: string;
orderType: FuturesOrderType;
}
export interface ModifyFuturesPlanOrderTPSL {
orderId: string;
marginCoin: string;
symbol: string;
presetTakeProfitPrice?: string;
presetStopLossPrice?: string;
}
export type FuturesPlanType = 'profit_plan' | 'loss_plan';
export type FuturesHoldSide = 'long' | 'short';
export interface NewFuturesPlanStopOrder {
symbol: string;
marginCoin: string;
planType: FuturesPlanType;
triggerPrice: string;
holdSide?: FuturesHoldSide;
size?: string;
}
export interface NewFuturesPlanPositionTPSL {
symbol: string;
marginCoin: string;
planType: FuturesPlanType;
triggerPrice: string;
holdSide: FuturesHoldSide;
}
export interface ModifyFuturesPlanStopOrder {
orderId: string;
marginCoin: string;
symbol: string;
triggerPrice?: string;
}
export interface CancelFuturesPlanTPSL {
orderId: string;
symbol: string;
marginCoin: string;
planType: FuturesPlanType;
}
export interface HistoricPlanOrderTPSLRequest {
symbol: string;
startTime: string;
endTime: string;
pageSize?: number;
isPre?: boolean;
isPlan?: string;
}

View File

@@ -0,0 +1,4 @@
export * from './broker';
export * from './futures';
export * from './shared';
export * from './spot';

View File

@@ -0,0 +1,9 @@
/** Pagination */
export interface Pagination {
/** Time after */
after?: string;
/** Time before */
before?: string;
/** Elements per page */
limit?: string;
}

26
src/types/request/spot.ts Normal file
View File

@@ -0,0 +1,26 @@
import { numberInString, OrderSide } from '../shared';
export type OrderTypeSpot = 'LIMIT' | 'MARKET' | 'LIMIT_MAKER';
export type OrderTimeInForce = 'GTC' | 'FOK' | 'IOC';
export type WalletType = 'spot' | 'mix_usdt' | 'mix_usd';
export interface NewWalletTransfer {
fromType: WalletType;
toType: WalletType;
amount: string;
coin: string;
clientOid?: string;
}
export interface NewSpotOrder {
symbol: string;
side: string;
orderType: string;
force: string;
price?: string;
quantity: string;
clientOrderId?: string;
}
export type NewBatchSpotOrder = Omit<NewSpotOrder, 'symbol'>;

View File

@@ -0,0 +1 @@
export * from './shared';

View File

@@ -0,0 +1,6 @@
export interface APIResponse<T> {
code: string;
data: T;
msg: 'success' | string;
requestTime: number;
}

27
src/types/shared.ts Normal file
View File

@@ -0,0 +1,27 @@
import { REST_CLIENT_TYPE_ENUM } from '../util';
export type numberInString = string;
export type OrderSide = 'Buy' | 'Sell';
export type KlineInterval =
| '1min'
| '5min'
| '15min'
| '30min'
| '1h'
| '4h'
| '6h'
| '12h'
| '1M'
| '1W'
| '1week'
| '6Hutc'
| '12Hutc'
| '1Dutc'
| '3Dutc'
| '1Wutc'
| '1Mutc';
export type RestClientType =
typeof REST_CLIENT_TYPE_ENUM[keyof typeof REST_CLIENT_TYPE_ENUM];

70
src/types/websockets.ts Normal file
View File

@@ -0,0 +1,70 @@
import { WS_KEY_MAP } from '../util';
export type WsPublicSpotTopic =
| 'ticker'
| 'candle1W'
| 'candle1D'
| 'candle12H'
| 'candle4H'
| 'candle1H'
| 'candle30m'
| 'candle15m'
| 'candle5m'
| 'candle1m'
| 'books'
| 'books5'
| 'trade';
// Futures currently has the same public topics as spot
export type WsPublicFuturesTopic = WsPublicSpotTopic;
export type WsPrivateSpotTopic = 'account' | 'orders';
export type WsPrivateFuturesTopic =
| 'account'
| 'positions'
| 'orders'
| 'ordersAlgo';
export type WsPublicTopic = WsPublicSpotTopic | WsPublicFuturesTopic;
export type WsPrivateTopic = WsPrivateSpotTopic | WsPrivateFuturesTopic;
export type WsTopic = WsPublicTopic | WsPrivateTopic;
/** This is used to differentiate between each of the available websocket streams */
export type WsKey = typeof WS_KEY_MAP[keyof typeof WS_KEY_MAP];
export interface WSClientConfigurableOptions {
/** Your API key */
apiKey?: string;
/** Your API secret */
apiSecret?: string;
/** The passphrase you set when creating the API Key (NOT your account password) */
apiPass?: string;
/** How often to check if the connection is alive */
pingInterval?: number;
/** How long to wait for a pong (heartbeat reply) before assuming the connection is dead */
pongTimeout?: number;
/** Delay in milliseconds before respawning the connection */
reconnectTimeout?: number;
requestOptions?: {
/** override the user agent when opening the websocket connection (some proxies use this) */
agent?: string;
};
wsUrl?: string;
/** Define a recv window when preparing a private websocket signature. This is in milliseconds, so 5000 == 5 seconds */
recvWindow?: number;
}
export interface WebsocketClientOptions extends WSClientConfigurableOptions {
pingInterval: number;
pongTimeout: number;
reconnectTimeout: number;
recvWindow: number;
}

354
src/util/BaseRestClient.ts Normal file
View File

@@ -0,0 +1,354 @@
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import { API_ERROR_CODE } from '../constants/enum';
import { RestClientType } from '../types';
import { signMessage } from './node-support';
import {
RestClientOptions,
serializeParams,
getRestBaseUrl,
} from './requestUtils';
// axios.interceptors.request.use((request) => {
// console.log(
// new Date(),
// 'Starting Request',
// JSON.stringify(
// {
// headers: request.headers,
// url: request.url,
// method: request.method,
// params: request.params,
// data: request.data,
// },
// null,
// 2
// )
// );
// return request;
// });
// axios.interceptors.response.use((response) => {
// console.log(new Date(), 'Response:', JSON.stringify(response, null, 2));
// return response;
// });
interface SignedRequest<T extends object | undefined = {}> {
originalParams: T;
paramsWithSign?: T & { sign: string };
serializedParams: string;
sign: string;
queryParamsWithSign: string;
timestamp: number;
recvWindow: number;
}
interface UnsignedRequest<T extends object | undefined = {}> {
originalParams: T;
paramsWithSign: T;
}
type SignMethod = 'keyInBody' | 'usdc' | 'bitget';
export default abstract class BaseRestClient {
private options: RestClientOptions;
private baseUrl: string;
private globalRequestOptions: AxiosRequestConfig;
private apiKey: string | undefined;
private apiSecret: string | undefined;
private clientType: RestClientType;
private apiPass: string | undefined;
/** Defines the client type (affecting how requests & signatures behave) */
abstract getClientType(): RestClientType;
/**
* Create an instance of the REST client. Pass API credentials in the object in the first parameter.
* @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity
* @param {AxiosRequestConfig} [networkOptions={}] HTTP networking options for axios
*/
constructor(
restOptions: RestClientOptions = {},
networkOptions: AxiosRequestConfig = {}
) {
this.clientType = this.getClientType();
this.options = {
recvWindow: 5000,
/** Throw errors if any request params are empty */
strictParamValidation: false,
...restOptions,
};
this.globalRequestOptions = {
// in ms == 5 minutes by default
timeout: 1000 * 60 * 5,
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config
...networkOptions,
headers: {
'X-CHANNEL-CODE': '3tem',
'Content-Type': 'application/json',
locale: 'en-US',
},
};
this.baseUrl = getRestBaseUrl(false, restOptions);
this.apiKey = this.options.apiKey;
this.apiSecret = this.options.apiSecret;
this.apiPass = this.options.apiPass;
// Throw if one of the 3 values is missing, but at least one of them is set
const credentials = [this.apiKey, this.apiSecret, this.apiPass];
if (
credentials.includes(undefined) &&
credentials.some((v) => typeof v === 'string')
) {
throw new Error(
'API Key, Secret & Passphrase are ALL required to use the authenticated REST client'
);
}
}
get(endpoint: string, params?: any) {
return this._call('GET', endpoint, params, true);
}
getPrivate(endpoint: string, params?: any) {
return this._call('GET', endpoint, params, false);
}
post(endpoint: string, params?: any) {
return this._call('POST', endpoint, params, true);
}
postPrivate(endpoint: string, params?: any) {
return this._call('POST', endpoint, params, false);
}
deletePrivate(endpoint: string, params?: any) {
return this._call('DELETE', endpoint, params, false);
}
/**
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
*/
private async _call(
method: Method,
endpoint: string,
params?: any,
isPublicApi?: boolean
): Promise<any> {
// Sanity check to make sure it's only ever prefixed by one forward slash
const requestUrl = [this.baseUrl, endpoint].join(
endpoint.startsWith('/') ? '' : '/'
);
// Build a request and handle signature process
const options = await this.buildRequest(
method,
endpoint,
requestUrl,
params,
isPublicApi
);
// console.log('full request: ', options);
// Dispatch request
return axios(options)
.then((response) => {
// console.log('response: ', response.data);
// console.error('res: ', response);
// if (response.data && response.data?.code !== API_ERROR_CODE.SUCCESS) {
// throw response.data;
// }
if (response.status == 200) {
if (
typeof response.data?.code === 'string' &&
response.data?.code !== '00000'
) {
throw { response };
}
return response.data;
}
throw { response };
})
.catch((e) => this.parseException(e));
}
/**
* @private generic handler to parse request exceptions
*/
parseException(e: any): unknown {
if (this.options.parseExceptions === false) {
throw e;
}
// Something happened in setting up the request that triggered an Error
if (!e.response) {
if (!e.request) {
throw e.message;
}
// request made but no response received
throw e;
}
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
const response: AxiosResponse = e.response;
// console.error('err: ', response?.data);
throw {
code: response.status,
message: response.statusText,
body: response.data,
headers: response.headers,
requestOptions: {
...this.options,
apiPass: 'omittedFromError',
apiSecret: 'omittedFromError',
},
};
}
/**
* @private sign request and set recv window
*/
private async signRequest<T extends object | undefined = {}>(
data: T,
endpoint: string,
method: Method,
signMethod: SignMethod
): Promise<SignedRequest<T>> {
const timestamp = Date.now();
const res: SignedRequest<T> = {
originalParams: {
...data,
},
sign: '',
timestamp,
recvWindow: 0,
serializedParams: '',
queryParamsWithSign: '',
};
if (!this.apiKey || !this.apiSecret) {
return res;
}
// It's possible to override the recv window on a per rquest level
const strictParamValidation = this.options.strictParamValidation;
if (signMethod === 'bitget') {
const signRequestParams =
method === 'GET'
? serializeParams(data, strictParamValidation, '?')
: JSON.stringify(data) || '';
const paramsStr =
timestamp + method.toUpperCase() + endpoint + signRequestParams;
// console.log('sign params: ', paramsStr);
res.sign = await signMessage(paramsStr, this.apiSecret, 'base64');
res.queryParamsWithSign = signRequestParams;
return res;
}
return res;
}
private async prepareSignParams<TParams extends object | undefined>(
method: Method,
endpoint: string,
signMethod: SignMethod,
params?: TParams,
isPublicApi?: true
): Promise<UnsignedRequest<TParams>>;
private async prepareSignParams<TParams extends object | undefined>(
method: Method,
endpoint: string,
signMethod: SignMethod,
params?: TParams,
isPublicApi?: false | undefined
): Promise<SignedRequest<TParams>>;
private async prepareSignParams<TParams extends object | undefined>(
method: Method,
endpoint: string,
signMethod: SignMethod,
params?: TParams,
isPublicApi?: boolean
) {
if (isPublicApi) {
return {
originalParams: params,
paramsWithSign: params,
};
}
if (!this.apiKey || !this.apiSecret) {
throw new Error('Private endpoints require api and private keys set');
}
return this.signRequest(params, endpoint, method, signMethod);
}
/** Returns an axios request object. Handles signing process automatically if this is a private API call */
private async buildRequest(
method: Method,
endpoint: string,
url: string,
params?: any,
isPublicApi?: boolean
): Promise<AxiosRequestConfig> {
const options: AxiosRequestConfig = {
...this.globalRequestOptions,
url: url,
method: method,
};
for (const key in params) {
if (typeof params[key] === 'undefined') {
delete params[key];
}
}
if (isPublicApi || !this.apiKey || !this.apiPass) {
return {
...options,
params: params,
};
}
const signResult = await this.prepareSignParams(
method,
endpoint,
'bitget',
params,
isPublicApi
);
if (!options.headers) {
options.headers = {};
}
options.headers['ACCESS-KEY'] = this.apiKey;
options.headers['ACCESS-PASSPHRASE'] = this.apiPass;
options.headers['ACCESS-TIMESTAMP'] = signResult.timestamp;
options.headers['ACCESS-SIGN'] = signResult.sign;
options.headers['Content-Type'] = 'application/json';
if (method === 'GET') {
return {
...options,
url: options.url + signResult.queryParamsWithSign,
};
}
return {
...options,
data: params,
};
}
}

205
src/util/WsStore.ts Normal file
View File

@@ -0,0 +1,205 @@
import WebSocket from 'isomorphic-ws';
import { WsPrivateTopic, WsTopic } from '../types';
import { DefaultLogger } from './logger';
export enum WsConnectionStateEnum {
INITIAL = 0,
CONNECTING = 1,
CONNECTED = 2,
CLOSING = 3,
RECONNECTING = 4,
// ERROR = 5,
}
/** A "topic" is always a string */
export type BitgetInstType = 'SP' | 'SPBL' | 'MC' | 'UMCBL' | 'DMCBL';
// TODO: generalise so this can be made a reusable module for other clients
export interface WsTopicSubscribeEventArgs {
instType: BitgetInstType;
channel: WsTopic;
/** The symbol, e.g. "BTCUSDT" */
instId: string;
}
type WsTopicList = Set<WsTopicSubscribeEventArgs>;
interface WsStoredState {
/** The currently active websocket connection */
ws?: WebSocket;
/** The current lifecycle state of the connection (enum) */
connectionState?: WsConnectionStateEnum;
/** A timer that will send an upstream heartbeat (ping) when it expires */
activePingTimer?: ReturnType<typeof setTimeout> | undefined;
/** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */
activePongTimer?: ReturnType<typeof setTimeout> | undefined;
/** If a reconnection is in progress, this will have the timer for the delayed reconnect */
activeReconnectTimer?: ReturnType<typeof setTimeout> | undefined;
/**
* All the topics we are expected to be subscribed to (and we automatically resubscribe to if the connection drops)
*
* A "Set" and a deep object match are used to ensure we only subscribe to a topic once (tracking a list of unique topics we're expected to be connected to)
*/
subscribedTopics: WsTopicList;
isAuthenticated?: boolean;
}
function isDeepObjectMatch(object1: any, object2: any) {
for (const key in object1) {
if (object1[key] !== object2[key]) {
return false;
}
}
return true;
}
export default class WsStore<WsKey extends string> {
private wsState: Record<string, WsStoredState> = {};
private logger: typeof DefaultLogger;
constructor(logger: typeof DefaultLogger) {
this.logger = logger || DefaultLogger;
}
/** Get WS stored state for key, optionally create if missing */
get(key: WsKey, createIfMissing?: true): WsStoredState;
get(key: WsKey, createIfMissing?: false): WsStoredState | undefined;
get(key: WsKey, createIfMissing?: boolean): WsStoredState | undefined {
if (this.wsState[key]) {
return this.wsState[key];
}
if (createIfMissing) {
return this.create(key);
}
}
getKeys(): WsKey[] {
return Object.keys(this.wsState) as WsKey[];
}
create(key: WsKey): WsStoredState | undefined {
if (this.hasExistingActiveConnection(key)) {
this.logger.warning(
'WsStore setConnection() overwriting existing open connection: ',
this.getWs(key)
);
}
this.wsState[key] = {
subscribedTopics: new Set(),
connectionState: WsConnectionStateEnum.INITIAL,
};
return this.get(key);
}
delete(key: WsKey): void {
// TODO: should we allow this at all? Perhaps block this from happening...
if (this.hasExistingActiveConnection(key)) {
const ws = this.getWs(key);
this.logger.warning(
'WsStore deleting state for connection still open: ',
ws
);
ws?.close();
}
delete this.wsState[key];
}
/* connection websocket */
hasExistingActiveConnection(key: WsKey): boolean {
return this.get(key) && this.isWsOpen(key);
}
getWs(key: WsKey): WebSocket | undefined {
return this.get(key)?.ws;
}
setWs(key: WsKey, wsConnection: WebSocket): WebSocket {
if (this.isWsOpen(key)) {
this.logger.warning(
'WsStore setConnection() overwriting existing open connection: ',
this.getWs(key)
);
}
this.get(key, true).ws = wsConnection;
return wsConnection;
}
/* connection state */
isWsOpen(key: WsKey): boolean {
const existingConnection = this.getWs(key);
return (
!!existingConnection &&
existingConnection.readyState === existingConnection.OPEN
);
}
getConnectionState(key: WsKey): WsConnectionStateEnum {
return this.get(key, true).connectionState!;
}
setConnectionState(key: WsKey, state: WsConnectionStateEnum) {
this.get(key, true).connectionState = state;
}
isConnectionState(key: WsKey, state: WsConnectionStateEnum): boolean {
return this.getConnectionState(key) === state;
}
/* subscribed topics */
getTopics(key: WsKey): WsTopicList {
return this.get(key, true).subscribedTopics;
}
getTopicsByKey(): Record<string, WsTopicList> {
const result = {};
for (const refKey in this.wsState) {
result[refKey] = this.getTopics(refKey as WsKey);
}
return result;
}
// Since topics are objects we can't rely on the set to detect duplicates
getMatchingTopic(key: WsKey, topic: WsTopicSubscribeEventArgs) {
// if (typeof topic === 'string') {
// return this.getMatchingTopic(key, { channel: topic });
// }
const allTopics = this.getTopics(key).values();
for (const storedTopic of allTopics) {
if (isDeepObjectMatch(topic, storedTopic)) {
return storedTopic;
}
}
}
addTopic(key: WsKey, topic: WsTopicSubscribeEventArgs) {
// if (typeof topic === 'string') {
// return this.addTopic(key, {
// instType: 'sp',
// channel: topic,
// instId: 'default',
// };
// }
// Check for duplicate topic. If already tracked, don't store this one
const existingTopic = this.getMatchingTopic(key, topic);
if (existingTopic) {
return this.getTopics(key);
}
return this.getTopics(key).add(topic);
}
deleteTopic(key: WsKey, topic: WsTopicSubscribeEventArgs) {
// Check if we're subscribed to a topic like this
const storedTopic = this.getMatchingTopic(key, topic);
if (storedTopic) {
this.getTopics(key).delete(storedTopic);
}
return this.getTopics(key);
}
}

View File

@@ -0,0 +1,47 @@
function _arrayBufferToBase64(buffer: ArrayBuffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
export async function signMessage(
message: string,
secret: string,
method: 'hex' | 'base64'
): Promise<string> {
const encoder = new TextEncoder();
const key = await window.crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign']
);
const signature = await window.crypto.subtle.sign(
'HMAC',
key,
encoder.encode(message)
);
switch (method) {
case 'hex': {
return Array.prototype.map
.call(new Uint8Array(signature), (x: any) =>
('00' + x.toString(16)).slice(-2)
)
.join('');
}
case 'base64': {
return _arrayBufferToBase64(signature);
}
default: {
((x: never) => {})(method);
throw new Error(`Unhandled sign method: ${method}`);
}
}
}

5
src/util/index.ts Normal file
View File

@@ -0,0 +1,5 @@
export * from './BaseRestClient';
export * from './requestUtils';
export * from './WsStore';
export * from './logger';
export * from './websocket-util';

22
src/util/logger.ts Normal file
View File

@@ -0,0 +1,22 @@
export type LogParams = null | any;
export const DefaultLogger = {
silly: (...params: LogParams): void => {
// console.log(params);
},
debug: (...params: LogParams): void => {
console.log(params);
},
notice: (...params: LogParams): void => {
console.log(params);
},
info: (...params: LogParams): void => {
console.info(params);
},
warning: (...params: LogParams): void => {
console.error(params);
},
error: (...params: LogParams): void => {
console.error(params);
},
};

23
src/util/node-support.ts Normal file
View File

@@ -0,0 +1,23 @@
import { createHmac } from 'crypto';
/** This is async because the browser version uses a promise (browser-support) */
export async function signMessage(
message: string,
secret: string,
method: 'hex' | 'base64'
): Promise<string> {
const hmac = createHmac('sha256', secret).update(message);
switch (method) {
case 'hex': {
return hmac.digest('hex');
}
case 'base64': {
return hmac.digest().toString('base64');
}
default: {
((x: never) => {})(method);
throw new Error(`Unhandled sign method: ${method}`);
}
}
}

92
src/util/requestUtils.ts Normal file
View File

@@ -0,0 +1,92 @@
export interface RestClientOptions {
/** Your API key */
apiKey?: string;
/** Your API secret */
apiSecret?: string;
/** The passphrase you set when creating the API Key (NOT your account password) */
apiPass?: string;
/** Set to `true` to connect to testnet. Uses the live environment by default. */
// testnet?: boolean;
/** Override the max size of the request window (in ms) */
recvWindow?: number;
/** Default: false. If true, we'll throw errors if any params are undefined */
strictParamValidation?: boolean;
/**
* Optionally override API protocol + domain
* e.g baseUrl: 'https://api.bitget.com'
**/
baseUrl?: string;
/** Default: true. whether to try and post-process request exceptions (and throw them). */
parseExceptions?: boolean;
}
export function serializeParams<T extends object | undefined = {}>(
params: T,
strict_validation = false,
prefixWith: string = ''
): string {
if (!params) {
return '';
}
const queryString = Object.keys(params)
.sort()
.map((key) => {
const value = params[key];
if (strict_validation === true && typeof value === 'undefined') {
throw new Error(
'Failed to sign API request due to undefined parameter'
);
}
return `${key}=${value}`;
})
.join('&');
// Only prefix if there's a value
return queryString ? prefixWith + queryString : queryString;
}
export function getRestBaseUrl(
useTestnet: boolean,
restInverseOptions: RestClientOptions
): string {
const exchangeBaseUrls = {
livenet: 'https://api.bitget.com',
livenet2: 'https://capi.bitget.com',
testnet: 'https://noTestnet',
};
if (restInverseOptions.baseUrl) {
return restInverseOptions.baseUrl;
}
if (useTestnet) {
return exchangeBaseUrls.testnet;
}
return exchangeBaseUrls.livenet;
}
export function isWsPong(msg: any): boolean {
// bitget
if (msg?.data === 'pong') {
return true;
}
return false;
}
/**
* Used to switch how authentication/requests work under the hood (primarily for SPOT since it's different there)
*/
export const REST_CLIENT_TYPE_ENUM = {
spot: 'spot',
futures: 'futures',
broker: 'broker',
} as const;

135
src/util/websocket-util.ts Normal file
View File

@@ -0,0 +1,135 @@
import { WsKey } from '../types';
import { signMessage } from './node-support';
import { BitgetInstType, WsTopicSubscribeEventArgs } from './WsStore';
/**
* Some exchanges have two livenet environments, some have test environments, some dont. This allows easy flexibility for different exchanges.
* Examples:
* - One livenet and one testnet: NetworkMap<'livenet' | 'testnet'>
* - One livenet, sometimes two, one testnet: NetworkMap<'livenet' | 'testnet', 'livenet2'>
* - Only one livenet, no other networks: NetworkMap<'livenet'>
*/
type NetworkMap<
TRequiredKeys extends string,
TOptionalKeys extends string | undefined = undefined
> = Record<TRequiredKeys, string> &
(TOptionalKeys extends string
? Record<TOptionalKeys, string | undefined>
: Record<TRequiredKeys, string>);
export const WS_BASE_URL_MAP: Record<
WsKey,
Record<'all', NetworkMap<'livenet'>>
> = {
mixv1: {
all: {
livenet: 'wss://ws.bitget.com/mix/v1/stream',
},
},
spotv1: {
all: {
livenet: 'wss://ws.bitget.com/spot/v1/stream',
},
},
};
/** Should be one WS key per unique URL */
export const WS_KEY_MAP = {
spotv1: 'spotv1',
mixv1: 'mixv1',
} as const;
/** Any WS keys in this list will trigger auth on connect, if credentials are available */
export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [
WS_KEY_MAP.spotv1,
WS_KEY_MAP.mixv1,
];
/** Any WS keys in this list will ALWAYS skip the authentication process, even if credentials are available */
export const PUBLIC_WS_KEYS = [] as WsKey[];
/**
* Used to automatically determine if a sub request should be to a public or private ws (when there's two separate connections).
* Unnecessary if there's only one connection to handle both public & private topics.
*/
export const PRIVATE_TOPICS = ['account', 'orders', 'positions', 'ordersAlgo'];
export function isPrivateChannel<TChannel extends string>(
channel: TChannel
): boolean {
return PRIVATE_TOPICS.includes(channel);
}
export function getWsKeyForTopic(
subscribeEvent: WsTopicSubscribeEventArgs,
isPrivate?: boolean
): WsKey {
const instType = subscribeEvent.instType.toUpperCase() as BitgetInstType;
switch (instType) {
case 'SP':
case 'SPBL': {
return WS_KEY_MAP.spotv1;
}
case 'MC':
case 'UMCBL':
case 'DMCBL': {
return WS_KEY_MAP.mixv1;
}
default: {
throw neverGuard(
instType,
`getWsKeyForTopic(): Unhandled market ${'instrumentId'}`
);
}
}
}
/** Force subscription requests to be sent in smaller batches, if a number is returned */
export function getMaxTopicsPerSubscribeEvent(wsKey: WsKey): number | null {
switch (wsKey) {
case 'mixv1':
case 'spotv1': {
// Technically there doesn't seem to be a documented cap, but there is a size limit per request. Doesn't hurt to batch requests.
return 15;
}
default: {
throw neverGuard(wsKey, `getWsKeyForTopic(): Unhandled wsKey`);
}
}
}
export const WS_ERROR_ENUM = {
INVALID_ACCESS_KEY: 30011,
};
export function neverGuard(x: never, msg: string): Error {
return new Error(`Unhandled value exception "${x}", ${msg}`);
}
export async function getWsAuthSignature(
apiKey: string | undefined,
apiSecret: string | undefined,
apiPass: string | undefined,
recvWindow: number = 0
): Promise<{
expiresAt: number;
signature: string;
}> {
if (!apiKey || !apiSecret || !apiPass) {
throw new Error(
`Cannot auth - missing api key, secret or passcode in config`
);
}
const signatureExpiresAt = ((Date.now() + recvWindow) / 1000).toFixed(0);
const signature = await signMessage(
signatureExpiresAt + 'GET' + '/user/verify',
apiSecret,
'base64'
);
return {
expiresAt: Number(signatureExpiresAt),
signature,
};
}

694
src/websocket-client.ts Normal file
View File

@@ -0,0 +1,694 @@
import { EventEmitter } from 'events';
import WebSocket from 'isomorphic-ws';
import WsStore, {
BitgetInstType,
WsTopicSubscribeEventArgs,
} from './util/WsStore';
import {
WebsocketClientOptions,
WSClientConfigurableOptions,
WsKey,
WsTopic,
} from './types';
import {
isWsPong,
WsConnectionStateEnum,
WS_AUTH_ON_CONNECT_KEYS,
WS_KEY_MAP,
DefaultLogger,
WS_BASE_URL_MAP,
getWsKeyForTopic,
neverGuard,
getMaxTopicsPerSubscribeEvent,
isPrivateChannel,
getWsAuthSignature,
} from './util';
const LOGGER_CATEGORY = { category: 'bitget-ws' };
export type WsClientEvent =
| 'open'
| 'update'
| 'close'
| 'exception'
| 'reconnect'
| 'reconnected'
| 'response';
interface WebsocketClientEvents {
/** Connection opened. If this connection was previously opened and reconnected, expect the reconnected event instead */
open: (evt: { wsKey: WsKey; event: any }) => void;
/** Reconnecting a dropped connection */
reconnect: (evt: { wsKey: WsKey; event: any }) => void;
/** Successfully reconnected a connection that dropped */
reconnected: (evt: { wsKey: WsKey; event: any }) => void;
/** Connection closed */
close: (evt: { wsKey: WsKey; event: any }) => void;
/** Received reply to websocket command (e.g. after subscribing to topics) */
response: (response: any & { wsKey: WsKey }) => void;
/** Received data for topic */
update: (response: any & { wsKey: WsKey }) => void;
/** Exception from ws client OR custom listeners (e.g. if you throw inside your event handler) */
exception: (response: any & { wsKey: WsKey }) => void;
/** Confirmation that a connection successfully authenticated */
authenticated: (event: { wsKey: WsKey; event: any }) => void;
}
// Type safety for on and emit handlers: https://stackoverflow.com/a/61609010/880837
export declare interface WebsocketClient {
on<U extends keyof WebsocketClientEvents>(
event: U,
listener: WebsocketClientEvents[U]
): this;
emit<U extends keyof WebsocketClientEvents>(
event: U,
...args: Parameters<WebsocketClientEvents[U]>
): boolean;
}
export class WebsocketClient extends EventEmitter {
private logger: typeof DefaultLogger;
private options: WebsocketClientOptions;
private wsStore: WsStore<WsKey>;
constructor(
options: WSClientConfigurableOptions,
logger?: typeof DefaultLogger
) {
super();
this.logger = logger || DefaultLogger;
this.wsStore = new WsStore(this.logger);
this.options = {
pongTimeout: 1000,
pingInterval: 10000,
reconnectTimeout: 500,
recvWindow: 0,
...options,
};
}
/**
* Subscribe to topics & track/persist them. They will be automatically resubscribed to if the connection drops/reconnects.
* @param wsTopics topic or list of topics
* @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
*/
public subscribe(
wsTopics: WsTopicSubscribeEventArgs[] | WsTopicSubscribeEventArgs,
isPrivateTopic?: boolean
) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach((topic) => {
const wsKey = getWsKeyForTopic(topic, isPrivateTopic);
// Persist this topic to the expected topics list
this.wsStore.addTopic(wsKey, topic);
// TODO: tidy up unsubscribe too, also in other connectors
// if connected, send subscription request
if (
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED)
) {
// if not authenticated, dont sub to private topics yet.
// This'll happen automatically once authenticated
const isAuthenticated = this.wsStore.get(wsKey)?.isAuthenticated;
if (!isAuthenticated) {
return this.requestSubscribeTopics(
wsKey,
topics.filter((topic) => !isPrivateChannel(topic.channel))
);
}
return this.requestSubscribeTopics(wsKey, topics);
}
// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
if (
!this.wsStore.isConnectionState(
wsKey,
WsConnectionStateEnum.CONNECTING
) &&
!this.wsStore.isConnectionState(
wsKey,
WsConnectionStateEnum.RECONNECTING
)
) {
return this.connect(wsKey);
}
});
}
/**
* Unsubscribe from topics & remove them from memory. They won't be re-subscribed to if the connection reconnects.
* @param wsTopics topic or list of topics
* @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
*/
public unsubscribe(
wsTopics: WsTopicSubscribeEventArgs[] | WsTopicSubscribeEventArgs,
isPrivateTopic?: boolean
) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach((topic) =>
this.wsStore.deleteTopic(getWsKeyForTopic(topic, isPrivateTopic), topic)
);
// TODO: should this really happen on each wsKey?? seems weird
this.wsStore.getKeys().forEach((wsKey: WsKey) => {
// unsubscribe request only necessary if active connection exists
if (
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED)
) {
this.requestUnsubscribeTopics(wsKey, topics);
}
});
}
/** Get the WsStore that tracks websockets & topics */
public getWsStore(): typeof this.wsStore {
return this.wsStore;
}
public close(wsKey: WsKey, force?: boolean) {
this.logger.info('Closing connection', { ...LOGGER_CATEGORY, wsKey });
this.setWsState(wsKey, WsConnectionStateEnum.CLOSING);
this.clearTimers(wsKey);
const ws = this.getWs(wsKey);
ws?.close();
if (force) {
ws?.terminate();
}
}
public closeAll(force?: boolean) {
this.wsStore.getKeys().forEach((key: WsKey) => {
this.close(key, force);
});
}
/**
* Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library
*/
public connectAll(): Promise<WebSocket | undefined>[] {
return [this.connect(WS_KEY_MAP.spotv1), this.connect(WS_KEY_MAP.mixv1)];
}
/**
* Request connection to a specific websocket, instead of waiting for automatic connection.
*/
private async connect(wsKey: WsKey): Promise<WebSocket | undefined> {
try {
if (this.wsStore.isWsOpen(wsKey)) {
this.logger.error(
'Refused to connect to ws with existing active connection',
{ ...LOGGER_CATEGORY, wsKey }
);
return this.wsStore.getWs(wsKey);
}
if (
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)
) {
this.logger.error(
'Refused to connect to ws, connection attempt already active',
{ ...LOGGER_CATEGORY, wsKey }
);
return;
}
if (
!this.wsStore.getConnectionState(wsKey) ||
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.INITIAL)
) {
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTING);
}
const url = this.getWsUrl(wsKey); // + authParams;
const ws = this.connectToWsUrl(url, wsKey);
return this.wsStore.setWs(wsKey, ws);
} catch (err) {
this.parseWsError('Connection failed', err, wsKey);
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
}
}
private parseWsError(context: string, error: any, wsKey: WsKey) {
if (!error.message) {
this.logger.error(`${context} due to unexpected error: `, error);
this.emit('response', { ...error, wsKey });
this.emit('exception', { ...error, wsKey });
return;
}
switch (error.message) {
case 'Unexpected server response: 401':
this.logger.error(`${context} due to 401 authorization failure.`, {
...LOGGER_CATEGORY,
wsKey,
});
break;
default:
this.logger.error(
`${context} due to unexpected response error: "${
error?.msg || error?.message || error
}"`,
{ ...LOGGER_CATEGORY, wsKey, error }
);
break;
}
this.emit('response', { ...error, wsKey });
this.emit('exception', { ...error, wsKey });
}
/** Get a signature, build the auth request and send it */
private async sendAuthRequest(wsKey: WsKey): Promise<void> {
try {
const { apiKey, apiSecret, apiPass, recvWindow } = this.options;
const { signature, expiresAt } = await getWsAuthSignature(
apiKey,
apiSecret,
apiPass,
recvWindow
);
this.logger.info(`Sending auth request...`, {
...LOGGER_CATEGORY,
wsKey,
});
const request = {
op: 'login',
args: [
{
apiKey: this.options.apiKey,
passphrase: this.options.apiPass,
timestamp: expiresAt,
sign: signature,
},
],
};
// console.log('ws auth req', request);
return this.tryWsSend(wsKey, JSON.stringify(request));
} catch (e) {
this.logger.silly(e, { ...LOGGER_CATEGORY, wsKey });
}
}
private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) {
this.clearTimers(wsKey);
if (
this.wsStore.getConnectionState(wsKey) !==
WsConnectionStateEnum.CONNECTING
) {
this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING);
}
this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => {
this.logger.info('Reconnecting to websocket', {
...LOGGER_CATEGORY,
wsKey,
});
this.connect(wsKey);
}, connectionDelayMs);
}
private ping(wsKey: WsKey) {
if (this.wsStore.get(wsKey, true).activePongTimer) {
return;
}
this.clearPongTimer(wsKey);
this.logger.silly('Sending ping', { ...LOGGER_CATEGORY, wsKey });
this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' }));
this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => {
this.logger.info('Pong timeout - closing socket to reconnect', {
...LOGGER_CATEGORY,
wsKey,
});
this.getWs(wsKey)?.terminate();
delete this.wsStore.get(wsKey, true).activePongTimer;
}, this.options.pongTimeout);
}
private clearTimers(wsKey: WsKey) {
this.clearPingTimer(wsKey);
this.clearPongTimer(wsKey);
const wsState = this.wsStore.get(wsKey);
if (wsState?.activeReconnectTimer) {
clearTimeout(wsState.activeReconnectTimer);
}
}
// Send a ping at intervals
private clearPingTimer(wsKey: WsKey) {
const wsState = this.wsStore.get(wsKey);
if (wsState?.activePingTimer) {
clearInterval(wsState.activePingTimer);
wsState.activePingTimer = undefined;
}
}
// Expect a pong within a time limit
private clearPongTimer(wsKey: WsKey) {
const wsState = this.wsStore.get(wsKey);
if (wsState?.activePongTimer) {
clearTimeout(wsState.activePongTimer);
wsState.activePongTimer = undefined;
}
}
/**
* @private Use the `subscribe(topics)` method to subscribe to topics. Send WS message to subscribe to topics.
*/
private requestSubscribeTopics(
wsKey: WsKey,
topics: WsTopicSubscribeEventArgs[]
) {
if (!topics.length) {
return;
}
const maxTopicsPerEvent = getMaxTopicsPerSubscribeEvent(wsKey);
if (maxTopicsPerEvent && topics.length > maxTopicsPerEvent) {
this.logger.silly(
`Subscribing to topics in batches of ${maxTopicsPerEvent}`
);
for (var i = 0; i < topics.length; i += maxTopicsPerEvent) {
const batch = topics.slice(i, i + maxTopicsPerEvent);
this.logger.silly(`Subscribing to batch of ${batch.length}`);
this.requestSubscribeTopics(wsKey, batch);
}
this.logger.silly(
`Finished batch subscribing to ${topics.length} topics`
);
return;
}
const wsMessage = JSON.stringify({
op: 'subscribe',
args: topics,
});
this.tryWsSend(wsKey, wsMessage);
}
/**
* @private Use the `unsubscribe(topics)` method to unsubscribe from topics. Send WS message to unsubscribe from topics.
*/
private requestUnsubscribeTopics(
wsKey: WsKey,
topics: WsTopicSubscribeEventArgs[]
) {
if (!topics.length) {
return;
}
const maxTopicsPerEvent = getMaxTopicsPerSubscribeEvent(wsKey);
if (maxTopicsPerEvent && topics.length > maxTopicsPerEvent) {
this.logger.silly(
`Unsubscribing to topics in batches of ${maxTopicsPerEvent}`
);
for (var i = 0; i < topics.length; i += maxTopicsPerEvent) {
const batch = topics.slice(i, i + maxTopicsPerEvent);
this.logger.silly(`Unsubscribing to batch of ${batch.length}`);
this.requestUnsubscribeTopics(wsKey, batch);
}
this.logger.silly(
`Finished batch unsubscribing to ${topics.length} topics`
);
return;
}
const wsMessage = JSON.stringify({
op: 'unsubscribe',
args: topics,
});
this.tryWsSend(wsKey, wsMessage);
}
public tryWsSend(wsKey: WsKey, wsMessage: string) {
try {
this.logger.silly(`Sending upstream ws message: `, {
...LOGGER_CATEGORY,
wsMessage,
wsKey,
});
if (!wsKey) {
throw new Error(
'Cannot send message due to no known websocket for this wsKey'
);
}
const ws = this.getWs(wsKey);
if (!ws) {
throw new Error(
`${wsKey} socket not connected yet, call "connectAll()" first then try again when the "open" event arrives`
);
}
ws.send(wsMessage);
} catch (e) {
this.logger.error(`Failed to send WS message`, {
...LOGGER_CATEGORY,
wsMessage,
wsKey,
exception: e,
});
}
}
private connectToWsUrl(url: string, wsKey: WsKey): WebSocket {
this.logger.silly(`Opening WS connection to URL: ${url}`, {
...LOGGER_CATEGORY,
wsKey,
});
const agent = this.options.requestOptions?.agent;
const ws = new WebSocket(url, undefined, agent ? { agent } : undefined);
ws.onopen = (event) => this.onWsOpen(event, wsKey);
ws.onmessage = (event) => this.onWsMessage(event, wsKey);
ws.onerror = (event) => this.parseWsError('websocket error', event, wsKey);
ws.onclose = (event) => this.onWsClose(event, wsKey);
return ws;
}
private async onWsOpen(event, wsKey: WsKey) {
if (
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)
) {
this.logger.info('Websocket connected', {
...LOGGER_CATEGORY,
wsKey,
});
this.emit('open', { wsKey, event });
} else if (
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.RECONNECTING)
) {
this.logger.info('Websocket reconnected', { ...LOGGER_CATEGORY, wsKey });
this.emit('reconnected', { wsKey, event });
}
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED);
// Some websockets require an auth packet to be sent after opening the connection
if (WS_AUTH_ON_CONNECT_KEYS.includes(wsKey)) {
await this.sendAuthRequest(wsKey);
}
// Reconnect to topics known before it connected
// Private topics will be resubscribed to once reconnected
const topics = [...this.wsStore.getTopics(wsKey)];
const publicTopics = topics.filter(
(topic) => !isPrivateChannel(topic.channel)
);
this.requestSubscribeTopics(wsKey, publicTopics);
this.wsStore.get(wsKey, true)!.activePingTimer = setInterval(
() => this.ping(wsKey),
this.options.pingInterval
);
}
/** Handle subscription to private topics _after_ authentication successfully completes asynchronously */
private onWsAuthenticated(wsKey: WsKey) {
const wsState = this.wsStore.get(wsKey, true);
wsState.isAuthenticated = true;
const topics = [...this.wsStore.getTopics(wsKey)];
const privateTopics = topics.filter((topic) =>
isPrivateChannel(topic.channel)
);
if (privateTopics.length) {
this.subscribe(privateTopics, true);
}
}
private onWsMessage(event: unknown, wsKey: WsKey) {
try {
// any message can clear the pong timer - wouldn't get a message if the ws wasn't working
this.clearPongTimer(wsKey);
if (isWsPong(event)) {
this.logger.silly('Received pong', { ...LOGGER_CATEGORY, wsKey });
return;
}
const msg = JSON.parse((event && event['data']) || event);
const emittableEvent = { ...msg, wsKey };
if (typeof msg === 'object') {
if (typeof msg['code'] === 'number') {
if (msg.event === 'login' && msg.code === 0) {
this.logger.info(`Successfully authenticated WS client`, {
...LOGGER_CATEGORY,
wsKey,
});
this.emit('response', emittableEvent);
this.emit('authenticated', emittableEvent);
this.onWsAuthenticated(wsKey);
return;
}
}
if (msg['event']) {
if (msg.event === 'error') {
this.logger.error(`WS Error received`, {
...LOGGER_CATEGORY,
wsKey,
message: msg || 'no message',
// messageType: typeof msg,
// messageString: JSON.stringify(msg),
event,
});
this.emit('exception', emittableEvent);
this.emit('response', emittableEvent);
return;
}
return this.emit('response', emittableEvent);
}
if (msg['arg']) {
return this.emit('update', emittableEvent);
}
}
this.logger.warning('Unhandled/unrecognised ws event message', {
...LOGGER_CATEGORY,
message: msg || 'no message',
// messageType: typeof msg,
// messageString: JSON.stringify(msg),
event,
wsKey,
});
// fallback emit anyway
return this.emit('update', emittableEvent);
} catch (e) {
this.logger.error('Failed to parse ws event message', {
...LOGGER_CATEGORY,
error: e,
event,
wsKey,
});
}
}
private onWsClose(event: unknown, wsKey: WsKey) {
this.logger.info('Websocket connection closed', {
...LOGGER_CATEGORY,
wsKey,
});
if (
this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CLOSING
) {
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
this.emit('reconnect', { wsKey, event });
} else {
this.setWsState(wsKey, WsConnectionStateEnum.INITIAL);
this.emit('close', { wsKey, event });
}
}
private getWs(wsKey: WsKey) {
return this.wsStore.getWs(wsKey);
}
private setWsState(wsKey: WsKey, state: WsConnectionStateEnum) {
this.wsStore.setConnectionState(wsKey, state);
}
private getWsUrl(wsKey: WsKey): string {
if (this.options.wsUrl) {
return this.options.wsUrl;
}
const networkKey = 'livenet';
switch (wsKey) {
case WS_KEY_MAP.spotv1: {
return WS_BASE_URL_MAP.spotv1.all[networkKey];
}
case WS_KEY_MAP.mixv1: {
return WS_BASE_URL_MAP.mixv1.all[networkKey];
}
default: {
this.logger.error('getWsUrl(): Unhandled wsKey: ', {
...LOGGER_CATEGORY,
wsKey,
});
throw neverGuard(wsKey, `getWsUrl(): Unhandled wsKey`);
}
}
}
/**
* Subscribe to a topic
* @param instType instrument type (refer to API docs).
* @param topic topic name (e.g. "ticker").
* @param instId instrument ID (e.g. "BTCUSDT"). Use "default" for private topics.
*/
public subscribeTopic(
instType: BitgetInstType,
topic: WsTopic,
instId: string = 'default'
) {
return this.subscribe({
instType,
instId,
channel: topic,
});
}
/**
* Unsubscribe from a topic
* @param instType instrument type (refer to API docs).
* @param topic topic name (e.g. "ticker").
* @param instId instrument ID (e.g. "BTCUSDT"). Use "default" for private topics to get all symbols.
*/
public unsubscribeTopic(
instType: BitgetInstType,
topic: WsTopic,
instId: string = 'default'
) {
return this.unsubscribe({
instType,
instId,
channel: topic,
});
}
}

View File

@@ -0,0 +1,111 @@
import { API_ERROR_CODE, BrokerClient } from '../../src';
import { sucessEmptyResponseObject } from '../response.util';
describe('Private Broker REST API GET Endpoints', () => {
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const API_PASS = process.env.API_PASS_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
expect(API_PASS).toStrictEqual(expect.any(String));
});
const api = new BrokerClient({
apiKey: API_KEY,
apiSecret: API_SECRET,
apiPass: API_PASS,
});
const coin = 'BTC';
const subUid = '123456';
const timestampOneHourAgo = new Date().getTime() - 1000 * 60 * 60;
const from = timestampOneHourAgo.toFixed(0);
const to = String(Number(from) + 1000 * 60 * 30); // 30 minutes
it('getBrokerInfo()', async () => {
try {
expect(await api.getBrokerInfo()).toMatchObject(
sucessEmptyResponseObject()
);
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('getSubAccounts()', async () => {
try {
expect(await api.getSubAccounts()).toMatchObject(
sucessEmptyResponseObject()
);
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('getSubEmail()', async () => {
try {
expect(await api.getSubEmail(subUid)).toMatchObject(
sucessEmptyResponseObject()
);
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('getSubSpotAssets()', async () => {
try {
expect(await api.getSubSpotAssets(subUid)).toMatchObject(
sucessEmptyResponseObject()
);
} catch (e) {
// expect(e.body).toBeNull();
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('getSubFutureAssets()', async () => {
try {
expect(await api.getSubFutureAssets(subUid, 'usdt')).toMatchObject(
sucessEmptyResponseObject()
);
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('getSubDepositAddress()', async () => {
try {
expect(await api.getSubDepositAddress(subUid, coin)).toMatchObject(
sucessEmptyResponseObject()
);
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('getSubAPIKeys()', async () => {
try {
expect(await api.getSubAPIKeys(subUid)).toMatchObject(
sucessEmptyResponseObject()
);
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
});

View File

@@ -0,0 +1,125 @@
import { API_ERROR_CODE, BrokerClient } from '../../src';
import { sucessEmptyResponseObject } from '../response.util';
describe('Private Broker REST API POST Endpoints', () => {
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const API_PASS = process.env.API_PASS_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
expect(API_PASS).toStrictEqual(expect.any(String));
});
const api = new BrokerClient({
apiKey: API_KEY,
apiSecret: API_SECRET,
apiPass: API_PASS,
});
const coin = 'BTC';
const subUid = '123456';
const timestampOneHourAgo = new Date().getTime() - 1000 * 60 * 60;
const from = timestampOneHourAgo.toFixed(0);
const to = String(Number(from) + 1000 * 60 * 30); // 30 minutes
it('createSubAccount()', async () => {
try {
expect(await api.createSubAccount('test1')).toMatchObject(
sucessEmptyResponseObject()
);
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('modifySubAccount()', async () => {
try {
expect(
await api.modifySubAccount('test1', 'spot_trade,transfer', 'normal')
).toMatchObject(sucessEmptyResponseObject());
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('modifySubEmail()', async () => {
try {
expect(
await api.modifySubEmail('test1', 'ASDFASDF@LKMASDF.COM')
).toMatchObject(sucessEmptyResponseObject());
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('subWithdrawal()', async () => {
try {
expect(
await api.subWithdrawal({
address: '123455',
amount: '12345',
chain: 'TRC20',
coin: 'USDT',
subUid,
})
).toMatchObject(sucessEmptyResponseObject());
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('setSubDepositAutoTransfer()', async () => {
try {
expect(
await api.setSubDepositAutoTransfer(subUid, 'USDT', 'spot')
).toMatchObject(sucessEmptyResponseObject());
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('createSubAPIKey()', async () => {
try {
expect(
await api.createSubAPIKey(
subUid,
'passphrase12345',
'remark',
'10.0.0.1'
)
).toMatchObject(sucessEmptyResponseObject());
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_BROKER,
});
}
});
it('modifySubAPIKey()', async () => {
try {
expect(
await api.modifySubAPIKey({
apikey: '12345',
subUid,
remark: 'test',
})
).toMatchObject(sucessEmptyResponseObject());
} catch (e) {
expect(e.body).toMatchObject({
code: '40017',
});
}
});
});

View File

@@ -0,0 +1,398 @@
import { API_ERROR_CODE, FuturesClient } from '../../src';
import { sucessEmptyResponseObject } from '../response.util';
describe('Private Futures REST API GET Endpoints', () => {
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const API_PASS = process.env.API_PASS_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
expect(API_PASS).toStrictEqual(expect.any(String));
});
const api = new FuturesClient({
apiKey: API_KEY,
apiSecret: API_SECRET,
apiPass: API_PASS,
});
const symbol = 'BTCUSDT_UMCBL';
const marginCoin = 'USDT';
const timestampOneHourAgo = new Date().getTime() - 1000 * 60 * 60;
const from = timestampOneHourAgo.toFixed(0);
const to = String(Number(from) + 1000 * 60 * 30); // 30 minutes
it('getAccount()', async () => {
try {
expect(await api.getAccount(symbol, marginCoin)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
available: expect.any(String),
btcEquity: expect.any(String),
equity: expect.any(String),
marginCoin: expect.any(String),
marginMode: expect.any(String),
},
});
} catch (e) {
console.error('getAccount: ', e);
expect(e).toBeNull();
}
});
it('getAccounts()', async () => {
try {
expect(await api.getAccounts('umcbl')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getAccounts: ', e);
expect(e).toBeNull();
}
});
it('getOpenCount()', async () => {
try {
expect(
await api.getOpenCount(symbol, marginCoin, 20000, 1)
).toMatchObject({
...sucessEmptyResponseObject(),
data: {
openCount: expect.any(Number),
},
});
} catch (e) {
console.error('getOpenCount: ', e);
expect(e).toBeNull();
}
});
it('getPosition()', async () => {
try {
expect(await api.getPosition(symbol, marginCoin)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getPosition: ', e);
expect(e).toBeNull();
}
});
it('getPositions()', async () => {
try {
expect(await api.getPositions('umcbl')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getPosition: ', e);
expect(e).toBeNull();
}
});
it('getAccountBill()', async () => {
try {
expect(
await api.getAccountBill({
startTime: from,
endTime: to,
marginCoin,
symbol,
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {
lastEndId: null,
nextFlag: false,
preFlag: false,
result: expect.any(Array),
},
});
} catch (e) {
console.error('getAccountBill: ', e);
expect(e).toBeNull();
}
});
it('getBusinessBill()', async () => {
try {
expect(
await api.getBusinessBill({
startTime: from,
endTime: to,
productType: 'umcbl',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {
lastEndId: null,
nextFlag: false,
preFlag: false,
result: expect.any(Array),
},
});
} catch (e) {
console.error('getBusinessBill: ', e);
expect(e).toBeNull();
}
});
it('getOpenSymbolOrders()', async () => {
try {
expect(await api.getOpenSymbolOrders(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getOpenSymbolOrders: ', e);
expect(e).toBeNull();
}
});
it('getOpenOrders()', async () => {
try {
expect(await api.getOpenOrders('umcbl', marginCoin)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getOpenOrders: ', e);
expect(e).toBeNull();
}
});
it('getOrderHistory()', async () => {
try {
expect(await api.getOrderHistory(symbol, from, to, '10')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
console.error('getOrderHistory: ', e);
expect(e).toBeNull();
}
});
it('getProductTypeOrderHistory()', async () => {
try {
expect(
await api.getProductTypeOrderHistory('umcbl', from, to, '10')
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
console.error('getProductTypeOrderHistory: ', e);
expect(e).toBeNull();
}
});
it('getOrder() should throw FUTURES_ORDER_NOT_FOUND', async () => {
try {
expect(await api.getOrder(symbol, '12345')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.FUTURES_ORDER_GET_NOT_FOUND,
});
}
});
it('getOrderFills() should throw FUTURES_ORDER_NOT_FOUND', async () => {
try {
expect(await api.getOrderFills(symbol, '12345')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.FUTURES_ORDER_GET_NOT_FOUND,
});
}
});
it('getProductTypeOrderFills() ', async () => {
try {
expect(
await api.getProductTypeOrderFills('umcbl', {
startTime: from,
endTime: to,
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
console.error('getProductTypeOrderFills: ', e);
expect(e).toBeNull();
}
});
it('getPlanOrderTPSLs()', async () => {
try {
expect(await api.getPlanOrderTPSLs(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
console.error('getPlanOrderTPSLs: ', e);
expect(e).toBeNull();
}
});
it('getHistoricPlanOrdersTPSL()', async () => {
try {
expect(
await api.getHistoricPlanOrdersTPSL({
startTime: from,
endTime: to,
symbol,
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
console.error('getHistoricPlanOrdersTPSL: ', e);
expect(e).toBeNull();
}
});
it('getCopyTraderOpenOrder()', async () => {
try {
expect(
await api.getCopyTraderOpenOrder(symbol, 'umcbl', 1, 0)
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('getCopyFollowersOpenOrder()', async () => {
try {
expect(
await api.getCopyFollowersOpenOrder(symbol, 'umcbl', 1, 0)
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('getCopyTraderOrderHistory()', async () => {
try {
expect(await api.getCopyTraderOrderHistory(from, to, 1, 0)).toMatchObject(
{
...sucessEmptyResponseObject(),
data: expect.any(Object),
}
);
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('getCopyTraderProfitSummary()', async () => {
try {
expect(await api.getCopyTraderProfitSummary()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('getCopyTraderHistoricProfitSummary()', async () => {
try {
expect(await api.getCopyTraderHistoricProfitSummary()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('getCopyTraderHistoricProfitSummaryByDate()', async () => {
try {
expect(
await api.getCopyTraderHistoricProfitSummaryByDate(
marginCoin,
from,
1,
1
)
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('getCopyTraderHistoricProfitDetail()', async () => {
try {
expect(
await api.getCopyTraderHistoricProfitDetail(marginCoin, from, 1, 1)
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('getCopyTraderProfitDetails()', async () => {
try {
expect(await api.getCopyTraderProfitDetails(1, 1)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('getCopyTraderSymbols()', async () => {
try {
expect(await api.getCopyTraderSymbols()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Object),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
});

View File

@@ -0,0 +1,340 @@
import { API_ERROR_CODE, FuturesClient } from '../../src';
import { sucessEmptyResponseObject } from '../response.util';
describe('Private Futures REST API POST Endpoints', () => {
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const API_PASS = process.env.API_PASS_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
expect(API_PASS).toStrictEqual(expect.any(String));
});
const api = new FuturesClient({
apiKey: API_KEY,
apiSecret: API_SECRET,
apiPass: API_PASS,
});
const symbol = 'BTCUSDT_UMCBL';
const marginCoin = 'USDT';
const timestampOneHourAgo = new Date().getTime() - 1000 * 60 * 60;
const from = timestampOneHourAgo.toFixed(0);
const to = String(Number(from) + 1000 * 60 * 30); // 30 minutes
it('setLeverage()', async () => {
try {
expect(await api.setLeverage(symbol, marginCoin, '20')).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
console.error('setLeverage: ', e);
expect(e).toBeNull();
}
});
it('setMargin()', async () => {
try {
expect(await api.setMargin(symbol, marginCoin, '-10')).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
// expect(e).toBeNull();
expect(e.body).toMatchObject({
code: API_ERROR_CODE.PARAMETER_EXCEPTION,
});
}
});
it('setMarginMode()', async () => {
try {
expect(
await api.setMarginMode(symbol, marginCoin, 'crossed')
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
console.error('setMarginMode: ', e);
expect(e).toBeNull();
}
});
it('submitOrder()', async () => {
try {
expect(
await api.submitOrder({
marginCoin,
orderType: 'market',
symbol,
size: '1',
side: 'open_long',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.INSUFFICIENT_BALANCE,
});
}
});
it('batchSubmitOrder()', async () => {
try {
expect(
await api.batchSubmitOrder(symbol, marginCoin, [
{
orderType: 'market',
size: '1',
side: 'open_long',
},
])
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.INSUFFICIENT_BALANCE,
});
}
});
it('cancelOrder()', async () => {
try {
expect(
await api.cancelOrder(symbol, marginCoin, '1234656')
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.FUTURES_ORDER_CANCEL_NOT_FOUND,
});
}
});
it('batchCancelOrder()', async () => {
try {
expect(
await api.batchCancelOrder(symbol, marginCoin, ['1234656'])
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
console.error('batchCancelOrder: ', e);
expect(e).toBeNull();
}
});
it('cancelAllOrders()', async () => {
try {
expect(await api.cancelAllOrders('umcbl', marginCoin)).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
console.error('cancelAllOrders: ', e);
expect(e).toBeNull();
}
});
it('submitPlanOrder()', async () => {
try {
expect(
await api.submitPlanOrder({
marginCoin,
orderType: 'market',
side: 'open_long',
size: '1',
symbol,
triggerPrice: '100',
triggerType: 'market_price',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
console.error('submitPlanOrder: ', e);
expect(e).toBeNull();
}
});
it('modifyPlanOrder()', async () => {
try {
expect(
await api.modifyPlanOrder({
orderId: '123456',
marginCoin,
orderType: 'market',
symbol,
triggerPrice: '100',
triggerType: 'market_price',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.PLAN_ORDER_NOT_FOUND,
});
}
});
it('modifyPlanOrderTPSL()', async () => {
try {
expect(
await api.modifyPlanOrderTPSL({
orderId: '123456',
marginCoin,
symbol,
presetTakeProfitPrice: '100',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
// expect(e).toBeNull();
expect(e.body).toMatchObject({
code: API_ERROR_CODE.SERVICE_RETURNED_ERROR,
});
}
});
it('submitStopOrder()', async () => {
try {
expect(
await api.submitStopOrder({
marginCoin,
symbol,
planType: 'profit_plan',
triggerPrice: '100',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.FUTURES_POSITION_DIRECTION_EMPTY,
});
}
});
it('submitPositionTPSL()', async () => {
try {
expect(
await api.submitPositionTPSL({
marginCoin,
symbol,
holdSide: 'long',
planType: 'profit_plan',
triggerPrice: '50',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.FUTURES_POSITION_DIRECTION_EMPTY,
});
}
});
it('modifyStopOrder()', async () => {
try {
expect(
await api.modifyStopOrder({
marginCoin,
symbol,
orderId: '123456',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
// expect(e).toBeNull();
expect(e.body).toMatchObject({
code: API_ERROR_CODE.FUTURES_ORDER_TPSL_NOT_FOUND,
});
}
});
it('cancelPlanOrderTPSL()', async () => {
try {
expect(
await api.cancelPlanOrderTPSL({
marginCoin,
symbol,
orderId: '123456',
planType: 'profit_plan',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.FUTURES_ORDER_TPSL_NOT_FOUND,
});
}
});
it('closeCopyTraderPosition()', async () => {
try {
expect(await api.closeCopyTraderPosition(symbol, '123456')).toMatchObject(
{
...sucessEmptyResponseObject(),
data: {},
}
);
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('modifyCopyTraderTPSL()', async () => {
try {
expect(
await api.modifyCopyTraderTPSL(symbol, '123456', {
stopLossPrice: 1234,
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
it('setCopyTraderSymbols()', async () => {
try {
expect(await api.setCopyTraderSymbols(symbol, 'delete')).toMatchObject({
...sucessEmptyResponseObject(),
data: {},
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ACCOUNT_NOT_COPY_TRADER,
});
}
});
});

147
test/futures/public.test.ts Normal file
View File

@@ -0,0 +1,147 @@
import { API_ERROR_CODE, FuturesClient } from '../../src';
import {
notAuthenticatedError,
successResponseString,
sucessEmptyResponseObject,
} from '../response.util';
describe('Public Spot REST API Endpoints', () => {
const api = new FuturesClient();
const symbol = 'BTCUSDT_UMCBL';
const timestampOneHourAgo = new Date().getTime() - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0));
const to = from + 1000 * 60 * 30; // 30 minutes
// it('should throw for unauthenticated private calls', async () => {
// expect(() => api.getOpenOrders()).rejects.toMatchObject(
// notAuthenticatedError()
// );
// expect(() => api.getBalances()).rejects.toMatchObject(
// notAuthenticatedError()
// );
// });
/**
*
* Market
*
*/
it('getSymbols()', async () => {
expect(await api.getSymbols('umcbl')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
});
it('getDepth()', async () => {
expect(await api.getDepth(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
bids: expect.any(Array),
asks: expect.any(Array),
},
});
});
it('getTicker()', async () => {
expect(await api.getTicker(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
bestAsk: expect.any(String),
bestBid: expect.any(String),
},
});
});
it('getAllTickers()', async () => {
expect(await api.getAllTickers('umcbl')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
});
it('getMarketTrades()', async () => {
expect(await api.getMarketTrades(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
});
it('getCandles()', async () => {
expect(
await api.getCandles(symbol, '1min', `${from}`, `${to}`)
).toMatchObject(expect.any(Array));
});
it('getIndexPrice()', async () => {
expect(await api.getIndexPrice(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
index: expect.any(String),
symbol: expect.any(String),
timestamp: expect.any(String),
},
});
});
it('getNextFundingTime()', async () => {
expect(await api.getNextFundingTime(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
fundingTime: expect.any(String),
symbol: expect.any(String),
},
});
});
it('getHistoricFundingRate()', async () => {
expect(await api.getHistoricFundingRate(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
});
it('getCurrentFundingRate()', async () => {
expect(await api.getCurrentFundingRate(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
fundingRate: expect.any(String),
symbol: expect.any(String),
},
});
});
it('getOpenInterest()', async () => {
expect(await api.getOpenInterest(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
amount: expect.any(String),
symbol: expect.any(String),
timestamp: expect.any(String),
},
});
});
it('getMarkPrice()', async () => {
expect(await api.getMarkPrice(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
markPrice: expect.any(String),
symbol: expect.any(String),
timestamp: expect.any(String),
},
});
});
it('getLeverageMinMax()', async () => {
expect(await api.getLeverageMinMax(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
maxLeverage: expect.any(String),
minLeverage: expect.any(String),
symbol: expect.any(String),
},
});
});
});

43
test/response.util.ts Normal file
View File

@@ -0,0 +1,43 @@
import { API_ERROR_CODE } from '../src';
const SUCCESS_MSG_REGEX = /success/gim;
export function successResponseString() {
return {
data: expect.any(String),
...sucessEmptyResponseObject(),
};
}
export function sucessEmptyResponseObject() {
return {
code: API_ERROR_CODE.SUCCESS,
msg: expect.stringMatching(SUCCESS_MSG_REGEX),
};
}
export function errorResponseObject(
result: null | any = null,
ret_code: number,
ret_msg: string
) {
return {
result,
ret_code,
ret_msg,
};
}
export function errorResponseObjectV3(
result: null | any = null,
retCode: number
) {
return {
result,
retCode: retCode,
};
}
export function notAuthenticatedError() {
return new Error('Private endpoints require api and private keys set');
}

View File

@@ -0,0 +1,161 @@
import { API_ERROR_CODE, SpotClient } from '../../src';
import { sucessEmptyResponseObject } from '../response.util';
describe('Private Spot REST API GET Endpoints', () => {
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const API_PASS = process.env.API_PASS_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
expect(API_PASS).toStrictEqual(expect.any(String));
});
const api = new SpotClient({
apiKey: API_KEY,
apiSecret: API_SECRET,
apiPass: API_PASS,
});
const symbol = 'BTCUSDT_SPBL';
const coin = 'BTC';
const timestampOneHourAgo = new Date().getTime() - 1000 * 60 * 60;
const from = timestampOneHourAgo.toFixed(0);
const to = String(Number(from) + 1000 * 60 * 30); // 30 minutes
// Seems to throw a permission error, probably because withdrawal permissions aren't set on this key (requires IP whitelist)
it.skip('getDepositAddress()', async () => {
try {
expect(await api.getDepositAddress(coin)).toStrictEqual('');
} catch (e) {
console.error('exception: ', e);
expect(e).toBeNull();
}
});
it('getWithdrawals()', async () => {
try {
expect(await api.getWithdrawals(coin, from, to)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getWithdrawals: ', e);
expect(e).toBeNull();
}
});
it('getDeposits()', async () => {
try {
expect(await api.getDeposits(coin, from, to)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getDeposits: ', e);
expect(e).toBeNull();
}
});
it('getApiKeyInfo()', async () => {
// No auth error == test pass
try {
expect(await api.getApiKeyInfo()).toMatchObject({
...sucessEmptyResponseObject(),
data: {
user_id: expect.any(String),
authorities: expect.any(Array),
},
});
} catch (e) {
console.error('getApiKeyInfo: ', e);
expect(e).toBeNull();
}
});
it('getBalance()', async () => {
try {
// expect(await api.getWithdrawals(coin, from, to)).toStrictEqual('');
expect(await api.getBalance()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getBalance: ', e);
expect(e).toBeNull();
}
});
it('getTransactionHistory()', async () => {
try {
expect(await api.getTransactionHistory()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getTransactionHistory: ', e);
expect(e).toBeNull();
}
});
it('getTransferHistory()', async () => {
try {
expect(await api.getTransferHistory()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getTransferHistory: ', e);
expect(e).toBeNull();
}
});
it('getOrder()', async () => {
try {
expect(await api.getOrder(symbol, '12345')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getOrder: ', e);
expect(e).toBeNull();
}
});
it('getOpenOrders()', async () => {
try {
expect(await api.getOpenOrders()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getOpenOrders: ', e);
expect(e).toBeNull();
}
});
it('getOrderHistory()', async () => {
try {
expect(await api.getOrderHistory(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getOrderHistory: ', e);
expect(e).toBeNull();
}
});
it('getOrderFills()', async () => {
try {
expect(await api.getOrderFills(symbol, '12345')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
console.error('getOrderFills: ', e);
expect(e).toBeNull();
}
});
});

View File

@@ -0,0 +1,152 @@
import { API_ERROR_CODE, SpotClient } from '../../src';
import { sucessEmptyResponseObject } from '../response.util';
describe('Private Spot REST API POST Endpoints', () => {
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const API_PASS = process.env.API_PASS_COM;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
expect(API_PASS).toStrictEqual(expect.any(String));
});
const api = new SpotClient({
apiKey: API_KEY,
apiSecret: API_SECRET,
apiPass: API_PASS,
});
const symbol = 'BTCUSDT_SPBL';
const coin = 'USDT';
const timestampOneHourAgo = new Date().getTime() - 1000 * 60 * 60;
const from = timestampOneHourAgo.toFixed(0);
const to = String(Number(from) + 1000 * 60 * 30); // 30 minutes
it('transfer()', async () => {
try {
expect(
await api.transfer({
amount: '100',
coin,
fromType: 'spot',
toType: 'mix_usdt',
})
).toStrictEqual('');
// .toMatchObject({
// // not sure what this error means, probably no balance
// code: '42013',
// });
} catch (e) {
// console.error('transfer: ', e);
expect(e.body).toMatchObject({
// not sure what this error means, probably no balance
code: '42013',
});
}
});
it('withdraw()', async () => {
try {
expect(
await api.withdraw({
amount: '100',
coin,
chain: 'TRC20',
address: `123456`,
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.INCORRECT_PERMISSIONS,
});
}
});
it('innerWithdraw()', async () => {
try {
expect(await api.innerWithdraw(coin, '12345', '1')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.INCORRECT_PERMISSIONS,
});
}
});
it('submitOrder()', async () => {
try {
expect(
await api.submitOrder({
symbol,
side: 'buy',
orderType: 'market',
quantity: '1',
force: 'normal',
})
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.QTY_LESS_THAN_MINIMUM,
});
}
});
it('batchSubmitOrder()', async () => {
try {
expect(
await api.batchSubmitOrder(symbol, [
{
side: 'buy',
orderType: 'market',
quantity: '1',
force: 'normal',
},
])
).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.QTY_LESS_THAN_MINIMUM,
});
}
});
it('cancelOrder()', async () => {
try {
expect(await api.cancelOrder(symbol, '123456')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ORDER_NOT_FOUND,
});
}
});
it('batchCancelOrder()', async () => {
try {
expect(await api.batchCancelOrder(symbol, ['123456'])).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
} catch (e) {
expect(e.body).toMatchObject({
code: API_ERROR_CODE.ORDER_NOT_FOUND,
});
}
});
});

108
test/spot/public.test.ts Normal file
View File

@@ -0,0 +1,108 @@
import { API_ERROR_CODE, SpotClient } from '../../src';
import {
notAuthenticatedError,
successResponseString,
sucessEmptyResponseObject,
} from '../response.util';
describe('Public Spot REST API Endpoints', () => {
const api = new SpotClient();
const symbol = 'BTCUSDT_SPBL';
const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0));
// it('should throw for unauthenticated private calls', async () => {
// expect(() => api.getOpenOrders()).rejects.toMatchObject(
// notAuthenticatedError()
// );
// expect(() => api.getBalances()).rejects.toMatchObject(
// notAuthenticatedError()
// );
// });
/**
*
* Public
*
*/
it('getServerTime()', async () => {
// expect(await api.getServerTime()).toStrictEqual('');
expect(await api.getServerTime()).toMatchObject(successResponseString());
});
it('fetchServertime() returns number', async () => {
expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number));
});
it('getCoins()', async () => {
expect(await api.getCoins()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
});
it('getSymbols()', async () => {
expect(await api.getSymbols()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
});
it('getSymbol()', async () => {
expect(await api.getSymbol(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
baseCoin: expect.any(String),
},
});
});
/**
*
* Market
*
*/
it('getTicker()', async () => {
expect(await api.getTicker(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: {
askSz: expect.any(String),
baseVol: expect.any(String),
},
});
});
it('getAllTickers()', async () => {
expect(await api.getAllTickers()).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
});
it('getMarketTrades()', async () => {
expect(await api.getMarketTrades(symbol)).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
});
it('getCandles()', async () => {
expect(await api.getCandles(symbol, '1min')).toMatchObject({
...sucessEmptyResponseObject(),
data: expect.any(Array),
});
});
it('getDepth()', async () => {
expect(await api.getDepth(symbol, 'step0')).toMatchObject({
...sucessEmptyResponseObject(),
data: {
bids: expect.any(Array),
asks: expect.any(Array),
},
});
});
});

110
test/ws.private.test.ts Normal file
View File

@@ -0,0 +1,110 @@
import {
WebsocketClient,
WSClientConfigurableOptions,
WS_ERROR_ENUM,
WS_KEY_MAP,
} from '../src';
import { getSilentLogger, waitForSocketEvent } from './ws.util';
describe('Private Spot Websocket Client', () => {
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const API_PASS = process.env.API_PASS_COM;
const wsClientOptions: WSClientConfigurableOptions = {
apiKey: API_KEY,
apiSecret: API_SECRET,
apiPass: API_PASS,
};
describe('with invalid credentials', () => {
it('should reject private subscribe if keys/signature are incorrect', async () => {
const badClient = new WebsocketClient(
{
...wsClientOptions,
apiKey: 'bad',
apiSecret: 'bad',
apiPass: 'bad',
},
getSilentLogger('expect401')
);
// const wsOpenPromise = waitForSocketEvent(badClient, 'open');
const wsResponsePromise = waitForSocketEvent(badClient, 'response');
// const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
badClient.subscribeTopic('SPBL', 'account');
expect(wsResponsePromise).rejects.toMatchObject({
code: WS_ERROR_ENUM.INVALID_ACCESS_KEY,
wsKey: WS_KEY_MAP.spotv1,
event: 'error',
});
try {
await Promise.all([wsResponsePromise]);
} catch (e) {
// console.error()
}
badClient.closeAll();
});
});
describe('with valid API credentails', () => {
let wsClient: WebsocketClient;
it('should have api credentials to test with', () => {
expect(API_KEY).toStrictEqual(expect.any(String));
expect(API_SECRET).toStrictEqual(expect.any(String));
expect(API_PASS).toStrictEqual(expect.any(String));
});
beforeAll(() => {
wsClient = new WebsocketClient(
wsClientOptions,
getSilentLogger('expectSuccess')
);
wsClient.connectAll();
// logAllEvents(wsClient);
});
afterAll(() => {
wsClient.closeAll();
});
it('should successfully authenticate a private ws connection', async () => {
const wsOpenPromise = waitForSocketEvent(wsClient, 'open');
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
try {
expect(await wsOpenPromise).toMatchObject({});
} catch (e) {
expect(e).toBeFalsy();
}
try {
expect(await wsResponsePromise).toMatchObject({
code: 0,
event: 'login',
});
} catch (e) {
console.error(`Wait for "books" subscription response exception: `, e);
expect(e).toBeFalsy();
}
});
it('should subscribe to private account events and get a snapshot', async () => {
const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
const channel = 'account';
wsClient.subscribeTopic('SPBL', channel);
expect(await wsUpdatePromise).toMatchObject({
action: 'snapshot',
arg: {
channel: channel,
},
});
});
});
});

74
test/ws.public.test.ts Normal file
View File

@@ -0,0 +1,74 @@
import {
WebsocketClient,
WSClientConfigurableOptions,
WS_KEY_MAP,
} from '../src';
import { logAllEvents, getSilentLogger, waitForSocketEvent } from './ws.util';
describe('Public Spot Websocket Client', () => {
let wsClient: WebsocketClient;
const wsClientOptions: WSClientConfigurableOptions = {};
beforeAll(() => {
wsClient = new WebsocketClient(
wsClientOptions,
getSilentLogger('expectSuccess')
);
wsClient.connectAll();
logAllEvents(wsClient);
});
afterAll(() => {
wsClient.closeAll();
});
it('should open a public ws connection', async () => {
const wsOpenPromise = waitForSocketEvent(wsClient, 'open');
try {
expect(await wsOpenPromise).toMatchObject({
wsKey: expect.any(String),
});
} catch (e) {
expect(e).toBeFalsy();
}
});
it('should subscribe to public orderbook events and get a snapshot', async () => {
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
const symbol = 'BTCUSDT';
wsClient.subscribeTopic('SP', 'books', symbol);
try {
expect(await wsResponsePromise).toMatchObject({
arg: { channel: 'books', instId: symbol, instType: expect.any(String) },
event: 'subscribe',
wsKey: WS_KEY_MAP.spotv1,
});
} catch (e) {
console.error(`Wait for "books" subscription response exception: `, e);
expect(e).toBeFalsy();
}
try {
expect(await wsUpdatePromise).toMatchObject({
action: 'snapshot',
arg: { channel: 'books', instId: 'BTCUSDT', instType: 'sp' },
data: [
{
asks: expect.any(Array),
bids: expect.any(Array),
},
],
wsKey: 'spotv1',
});
} catch (e) {
console.error(`Wait for "books" event exception: `, e);
expect(e).toBeFalsy();
}
});
});

128
test/ws.util.ts Normal file
View File

@@ -0,0 +1,128 @@
import { WebsocketClient, WsClientEvent } from '../src';
export function getSilentLogger(logHint?: string) {
return {
silly: () => {},
debug: () => {},
notice: () => {},
info: () => {},
warning: () => {},
error: () => {},
};
}
export const fullLogger = {
silly: (...params) => console.log('silly', ...params),
debug: (...params) => console.log('debug', ...params),
notice: (...params) => console.log('notice', ...params),
info: (...params) => console.info('info', ...params),
warning: (...params) => console.warn('warning', ...params),
error: (...params) => console.error('error', ...params),
};
/** Resolves a promise if an event is seen before a timeout (defaults to 4.5 seconds) */
export function waitForSocketEvent(
wsClient: WebsocketClient,
event: WsClientEvent,
timeoutMs: number = 4.5 * 1000
) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
`Failed to receive "${event}" event before timeout. Check that these are correct: topic, api keys (if private), signature process (if private)`
);
}, timeoutMs);
let resolvedOnce = false;
function cleanup() {
clearTimeout(timeout);
resolvedOnce = true;
wsClient.removeListener(event, (e) => resolver(e));
wsClient.removeListener('error', (e) => rejector(e));
}
function resolver(event) {
resolve(event);
cleanup();
}
function rejector(event) {
if (!resolvedOnce) {
reject(event);
}
cleanup();
}
wsClient.on(event, (e) => resolver(e));
wsClient.on('exception', (e) => rejector(e));
// if (event !== 'close') {
// wsClient.on('close', (event) => {
// clearTimeout(timeout);
// if (!resolvedOnce) {
// reject(event);
// }
// });
// }
});
}
export function listenToSocketEvents(wsClient: WebsocketClient) {
const retVal: Record<
'update' | 'open' | 'response' | 'close' | 'error',
typeof jest.fn
> = {
open: jest.fn(),
response: jest.fn(),
update: jest.fn(),
close: jest.fn(),
error: jest.fn(),
};
wsClient.on('open', retVal.open);
wsClient.on('response', retVal.response);
wsClient.on('update', retVal.update);
wsClient.on('close', retVal.close);
wsClient.on('exception', retVal.error);
return {
...retVal,
cleanup: () => {
wsClient.removeListener('open', retVal.open);
wsClient.removeListener('response', retVal.response);
wsClient.removeListener('update', retVal.update);
wsClient.removeListener('close', retVal.close);
wsClient.removeListener('exception', retVal.error);
},
};
}
export function logAllEvents(wsClient: WebsocketClient) {
wsClient.on('update', (data) => {
// console.log('wsUpdate: ', JSON.stringify(data, null, 2));
});
wsClient.on('open', (data) => {
console.log('wsOpen: ', data.wsKey);
});
wsClient.on('response', (data) => {
console.log('wsResponse ', JSON.stringify(data, null, 2));
});
wsClient.on('reconnect', ({ wsKey }) => {
console.log('wsReconnecting ', wsKey);
});
wsClient.on('reconnected', (data) => {
console.log('wsReconnected ', data?.wsKey);
});
wsClient.on('close', (data) => {
// console.log('wsClose: ', data);
});
}
export function promiseSleep(ms: number) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}

26
tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compileOnSave": true,
"compilerOptions": {
"allowJs": true,
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"removeComments": false,
"noEmitOnError": true,
"noImplicitAny": false,
"strictNullChecks": true,
"skipLibCheck": true,
"sourceMap": true,
"esModuleInterop": true,
"lib": ["es2017","dom"],
"outDir": "lib"
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"**/node_modules/*",
"coverage",
"doc"
]
}

68
webpack/webpack.config.js Normal file
View File

@@ -0,0 +1,68 @@
const webpack = require('webpack');
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
function generateConfig(name) {
var config = {
entry: './lib/index.js',
output: {
path: path.resolve(__dirname, '../dist'),
filename: name + '.js',
sourceMapFilename: name + '.map',
library: name,
libraryTarget: 'umd'
},
devtool: "source-map",
mode: 'production',
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"],
alias: {
[path.resolve(__dirname, "../lib/util/node-support.js")]:
path.resolve(__dirname, "../lib/util/browser-support.js"),
}
},
module: {
rules: [
// All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
{ test: /\.tsx?$/, loader: "ts-loader" },
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{ test: /\.js$/, loader: "source-map-loader" },
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components|samples|lib|test|coverage)/,
use: {
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env', {
'targets': {
'node': 'current'
}
}]]
}
}
}
]
}
};
config.plugins = [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
}),
new BundleAnalyzerPlugin({
defaultSizes: 'stat',
analyzerMode: 'static',
reportFilename: '../doc/bundleReport.html',
openAnalyzer: false,
})
];
return config;
}
module.exports = generateConfig('bitgetapi');