diff --git a/README.md b/README.md index 77e5631..baab650 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ const restClientOptions = { strict_param_validation?: boolean; // Optionally override API protocol + domain - // e.g 'https://api.bytick.com' + // e.g 'https://ftx.us/api' baseUrl?: string; // Default: true. whether to try and post-process request exceptions. @@ -71,7 +71,7 @@ const restClientOptions = { const API_KEY = 'xxx'; const PRIVATE_KEY = 'yyy'; -const client = new InverseClient( +const client = new RestClient( API_KEY, PRIVATE_KEY, @@ -237,6 +237,9 @@ The bundle can be found in `dist/`. Altough usage should be largely consistent, --- +## FTX.US +This client also supports the US FTX exchange. Simply set the "domain" to "ftxus" in both the RestClient and the WebsocketClient. See [examples/ftxus.ts](./examples/ftxus.ts) for a demonstration. + ## Contributions & Thanks ### Donations #### tiagosiebler diff --git a/examples/ftxus.ts b/examples/ftxus.ts new file mode 100644 index 0000000..6fc0440 --- /dev/null +++ b/examples/ftxus.ts @@ -0,0 +1,78 @@ +import { RestClient } from "../src/rest-client"; +import { RestClientOptions, WSClientConfigurableOptions } from "../src/util/requestUtils"; +import { WebsocketClient } from "../src/websocket-client"; + +/* + + FTX.us uses a different API domain for both REST and Websockets. Headers are also slightly different. + + Set the domain in all client options to 'ftxus' to connect to ftx.us. Examples for REST and Websockets are below. + + Note: some API calls may be unavailable due to limitations in the FTX.us APIs. + +*/ + +(async () => { + // Optional, but required for private endpoints + const key = 'apiKeyHere'; + const secret = 'apiSecretHere'; + + const restClientOptions: RestClientOptions = { + domain: 'ftxus' + }; + + const wsClientOptions: WSClientConfigurableOptions = { + key: 'apikeyhere', + secret: 'apisecrethere', + // subAccountName: 'sub1', + domain: 'ftxus', + restOptions: restClientOptions + }; + + // Prepare websocket connection + const ws = new WebsocketClient(wsClientOptions); + ws.on('response', msg => console.log('response: ', msg)); + ws.on('update', msg => console.log('update: ', msg)); + ws.on('error', msg => console.log('err: ', msg)); + + // Prepare rest client and trigger API calls as desired + const client = new RestClient(key, secret, restClientOptions); + + // Try some public API calls + try { + console.log('getBalances: ', await client.getBalances()); + console.log('getSubaccountBalances: ', await client.getSubaccountBalances('sub1')); + console.log('getMarkets: ', await client.getMarket('ABNB-0326')); + } catch (e) { + console.error('public get method failed: ', e); + } + + // Try some authenticated API calls + const market = 'BTC/USD'; + try { + console.log('buysome: ', await client.placeOrder({ + market, + side: 'buy', + type: 'market', + size: 0.001, + price: null + })); + } catch (e) { + console.error('buy failed: ', e); + } + + try { + console.log('sellsome: ', await client.placeOrder({ + market, + side: 'sell', + type: 'market', + size: 0.001, + price: null + })); + } catch (e) { + console.error('sell failed: ', e); + } + + // Nothing left - close the process + process.exit(); +})(); \ No newline at end of file diff --git a/package.json b/package.json index 1a6511f..0fe3aab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ftx-api", - "version": "1.0.0", + "version": "1.0.2", "description": "Node.js connector for FTX's REST APIs and WebSockets", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index ae67891..a1d0248 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -1,5 +1,9 @@ +import { AxiosRequestConfig } from 'axios'; import { createHmac } from 'crypto'; + +export type FtxDomain = 'ftxcom' | 'ftxus'; + export interface RestClientOptions { // override the max size of the request window (in ms) recv_window?: number; @@ -14,7 +18,7 @@ export interface RestClientOptions { strict_param_validation?: boolean; // Optionally override API protocol + domain - // e.g 'https://api.bytick.com' + // e.g 'https://ftx.us/api' baseUrl?: string; // Default: true. whether to try and post-process request exceptions. @@ -22,6 +26,9 @@ export interface RestClientOptions { // Subaccount nickname URI-encoded subAccountName?: string; + + // Default: 'ftxcom'. Choose between ftxcom or ftxus. + domain?: FtxDomain; } export interface WSClientConfigurableOptions { @@ -35,10 +42,21 @@ export interface WSClientConfigurableOptions { pongTimeout?: number; pingInterval?: number; reconnectTimeout?: number; - restOptions?: any; - requestOptions?: any; + restOptions?: RestClientOptions; + requestOptions?: AxiosRequestConfig; + // Optionally override websocket API protocol + domain + // E.g: 'wss://ftx.com/ws/' wsUrl?: string; + + // Default: 'ftxcom'. Choose between ftxcom or ftxus. + domain?: FtxDomain; +}; + +export interface WebsocketClientOptions extends WSClientConfigurableOptions { + pongTimeout: number; + pingInterval: number; + reconnectTimeout: number; }; export type GenericAPIResponse = Promise; @@ -68,14 +86,32 @@ export function serializeParams(params: object = {}, strict_validation = false): .join('&'); }; +export type apiNetwork = 'ftxcom' | 'ftxus'; + export function getRestBaseUrl(restClientOptions: RestClientOptions) { if (restClientOptions.baseUrl) { return restClientOptions.baseUrl; } + if (restClientOptions.domain === 'ftxus') { + return 'https://ftx.us/api'; + } + return 'https://ftx.com/api'; }; +export function getWsUrl(options: WebsocketClientOptions): string { + if (options.wsUrl) { + return options.wsUrl; + } + + if (options.domain === 'ftxus') { + return 'wss://ftx.us/ws/'; + } + + return 'wss://ftx.com/ws/'; +}; + export function isPublicEndpoint (endpoint: string): boolean { if (endpoint.startsWith('https')) { return true; diff --git a/src/util/requestWrapper.ts b/src/util/requestWrapper.ts index 9ffa919..96e57ec 100644 --- a/src/util/requestWrapper.ts +++ b/src/util/requestWrapper.ts @@ -1,6 +1,39 @@ import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios'; -import { signMessage, serializeParams, RestClientOptions, GenericAPIResponse, isPublicEndpoint } from './requestUtils'; +import { signMessage, serializeParams, RestClientOptions, GenericAPIResponse, FtxDomain } from './requestUtils'; + +type ApiHeaders = 'key' | 'ts' | 'sign' | 'subaccount'; + +const getHeader = (headerId: ApiHeaders, domain: FtxDomain = 'ftxcom'): string => { + if (domain === 'ftxcom') { + switch (headerId) { + case 'key': + return 'FTX-KEY'; + case 'ts': + return 'FTX-TS'; + case 'sign': + return 'FTX-SIGN'; + case 'subaccount': + return 'FTX-SUBACCOUNT'; + } + } + + if (domain === 'ftxus') { + switch (headerId) { + case 'key': + return 'FTXUS-KEY'; + case 'ts': + return 'FTXUS-TS'; + case 'sign': + return 'FTXUS-SIGN'; + case 'subaccount': + return 'FTXUS-SUBACCOUNT'; + } + } + + console.warn('No matching header name: ', { headerId, domain }); + return 'null'; +} export default class RequestUtil { private timeOffset: number | null; @@ -38,12 +71,12 @@ export default class RequestUtil { ...requestOptions, // FTX requirements headers: { - 'FTX-KEY': key, + [getHeader('key', options.domain)]: key, }, }; if (typeof this.options.subAccountName === 'string') { - this.globalRequestOptions.headers['FTX-SUBACCOUNT'] = this.options.subAccountName; + this.globalRequestOptions.headers[getHeader('subaccount', options.domain)] = this.options.subAccountName; } this.baseUrl = baseUrl; @@ -92,8 +125,8 @@ export default class RequestUtil { } const { timestamp, sign } = this.getRequestSignature(method, endpoint, this.secret, params); - options.headers['FTX-TS'] = String(timestamp); - options.headers['FTX-SIGN'] = sign; + options.headers[getHeader('ts', this.options.domain)] = String(timestamp); + options.headers[getHeader('sign', this.options.domain)] = sign; } if (method === 'GET') { diff --git a/src/websocket-client.ts b/src/websocket-client.ts index e0ab8b2..d3db8b7 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'events'; import { RestClient } from './rest-client'; import { DefaultLogger } from './logger'; -import { signMessage, serializeParams, signWsAuthenticate, WSClientConfigurableOptions } from './util/requestUtils'; +import { signMessage, serializeParams, signWsAuthenticate, WSClientConfigurableOptions, getWsUrl, WebsocketClientOptions } from './util/requestUtils'; import WebSocket from 'isomorphic-ws'; import WsStore from './util/WsStore'; @@ -23,14 +23,7 @@ export enum WsConnectionState { READY_STATE_RECONNECTING }; -export interface WebsocketClientOptions extends WSClientConfigurableOptions { - pongTimeout: number; - pingInterval: number; - reconnectTimeout: number; -}; - export const wsKeyGeneral = 'ftx'; -export const wsBaseUrl = 'wss://ftx.com/ws/'; export declare interface WebsocketClient { on(event: 'open' | 'reconnected', listener: ({ wsKey: string, event: any }) => void): this; @@ -63,6 +56,13 @@ export class WebsocketClient extends EventEmitter { ...options }; + if (options.domain != this.options.restOptions?.domain) { + this.options.restOptions = { + ...this.options.restOptions, + domain: options.domain + }; + } + this.restClient = new RestClient(undefined, undefined, this.options.restOptions, this.options.requestOptions); } @@ -149,7 +149,7 @@ export class WebsocketClient extends EventEmitter { this.setWsState(wsKey, READY_STATE_CONNECTING); } - const url = this.getWsUrl(wsKey); + const url = getWsUrl(this.options); const ws = this.connectToWsUrl(url, wsKey); return this.wsStore.setWs(wsKey, ws); @@ -390,14 +390,6 @@ export class WebsocketClient extends EventEmitter { this.wsStore.setConnectionState(wsKey, state); } - private getWsUrl(wsKey?: string): string { - if (this.options.wsUrl) { - return this.options.wsUrl; - } - - return wsBaseUrl; - } - private getWsKeyForTopic(topic: any) { return wsKeyGeneral; }