Skip to content

Commit

Permalink
Merge pull request #3 from tiagosiebler/ftxus
Browse files Browse the repository at this point in the history
Add support for ftx.us
  • Loading branch information
tiagosiebler authored Apr 16, 2021
2 parents dff7c8b + a7e5f36 commit e5b4954
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 28 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,

Expand Down Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions examples/ftxus.ts
Original file line number Diff line number Diff line change
@@ -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();
})();
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
42 changes: 39 additions & 3 deletions src/util/requestUtils.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,14 +18,17 @@ 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.
parse_exceptions?: boolean;

// Subaccount nickname URI-encoded
subAccountName?: string;

// Default: 'ftxcom'. Choose between ftxcom or ftxus.
domain?: FtxDomain;
}

export interface WSClientConfigurableOptions {
Expand All @@ -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<any>;
Expand Down Expand Up @@ -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;
Expand Down
43 changes: 38 additions & 5 deletions src/util/requestWrapper.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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') {
Expand Down
26 changes: 9 additions & 17 deletions src/websocket-client.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit e5b4954

Please sign in to comment.