From 243f485770690dd73c81f2fff33acced219434df Mon Sep 17 00:00:00 2001 From: Nick Freeman Date: Sat, 29 Jul 2017 21:51:02 -0400 Subject: [PATCH 1/9] Flatten callback API from `(err, response, data)` to `(err, data)` - Better standardize callback format to accept two parameters, `error` and `data`. (BREAKING CHANGE) - Switch from `request` to `superagent` library - Add client-side handling of throttling requests within rate limits (via [`superagent-throttle`](https://github.com/leviwheatcroft/superagent-throttle) plugin) - Refactor tests to use new two-parameter API and to use mocha's promise API where possible (instead of manually using (`done`) method) - Bump version in `package.json` to reflect breaking API change --- README.md | 52 ++++--- lib/clients/authenticated.js | 153 ++++++++++----------- lib/clients/public.js | 260 +++++++++++++++++------------------ lib/orderbook_sync.js | 9 +- lib/request_signer.js | 34 +++-- package.json | 7 +- tests/authenticated.spec.js | 241 +++++++++++++++----------------- tests/orderbook_sync.spec.js | 24 +++- tests/public_client.spec.js | 28 ++-- 9 files changed, 408 insertions(+), 400 deletions(-) diff --git a/README.md b/README.md index 15763743..d4c6ebe3 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ npm install coinbase/gdax-node ``` You can learn about the API responses of each endpoint [by reading our -documentation](https://docs.gdax.com/#market-data). +documentation](https://docs.gdax.com). ## Quick Start @@ -61,6 +61,24 @@ publicClient }); ``` +Promises will be resolved with an object `data` whose structure varies depending +on the GDAX endpoint called (see the [GDAX API docs](https://docs.gdax.com)). + +Upon encountering an error, the Promise will be rejected with an `error` object. +If the error is related to the network or a response recieved from the GDAX API, +`error` may contain additional properties as described in `superagent`'s [error +handling docs](https://visionmedia.github.io/superagent/#error-handling). + +```js +publicClient + .getProducts() // assume this produces some error + .then(data => { /* ... */ }) + .catch(error => { + error typeof Error; // true + typeof error.message; // string + }); +``` + The promise API can be used as expected in `async` functions in ES2017+ environments: @@ -80,17 +98,14 @@ async function yourFunction() { ### Using Callbacks -Your callback should accept two arguments: +Your callback should accept two arguments, `error` and `data`, as described +above. In callbacks, -- `error`: contains an error message (`string`), or `null` if no was error - encountered -- `response`: a generic HTTP response abstraction created by the [`request` - library](https://github.com/request/request) -- `data`: contains data returned by the GDAX API, or `undefined` if an error was - encountered +- `error` will be `null` if no error is encountered +- `data` will be `undefined` if an error is encountered ```js -publicClient.getProducts((error, response, data) => { +publicClient.getProducts((error, data) => { if (error) { // handle the error } else { @@ -104,7 +119,7 @@ prevent potential `UnhandledPromiseRejectionWarning`s, which will cause future versions of Node to terminate. ```js -const myCallback = (err, response, data) => { /* ... */ }; +const myCallback = (err, data) => { /* ... */ }; const result = publicClient.getProducts(myCallback); @@ -118,7 +133,8 @@ Some methods accept optional parameters, e.g. ```js publicClient .getProductOrderBook({ level: 3 }) - .then(book => { /* ... */ }); + .then(book => { /* ... */ }) + .catch(error => { /* ... */ }; ``` To use optional parameters with callbacks, supply the options as the first @@ -126,17 +142,17 @@ parameter(s) and the callback as the last parameter: ```js publicClient - .getProductOrderBook({ level: 3 }, (error, response, book) => { /* ... */ }); + .getProductOrderBook({ level: 3 }, (error, book) => { /* ... */ }); ``` ### The Public API Client ```js -const publicClient = new Gdax.PublicClient(productID, endpoint); +const publicClient = new Gdax.PublicClient(productID, apiURI); ``` -- `productID` *optional* - defaults to 'BTC-USD' if not specified. -- `endpoint` *optional* - defaults to 'https://api.gdax.com' if not specified. +- `productID` *optional* - defaults to `'BTC-USD'` +- `apiURI` *optional* - defaults to `'https://api.gdax.com'` #### Public API Methods @@ -231,9 +247,9 @@ const authedClient = new Gdax.AuthenticatedClient(key, b64secret, passphrase, ap Like `PublicClient`, all API methods can be used with either callbacks or will return promises. -`AuthenticatedClient` inherits all of the API methods from -`PublicClient`, so if you're hitting both public and private API endpoints you -only need to create a single client. +`AuthenticatedClient` inherits all of the API methods from `PublicClient`, so if +you're hitting both public and private API endpoints you only need to create a +single authenticated client. #### Private API Methods diff --git a/lib/clients/authenticated.js b/lib/clients/authenticated.js index 3896c13e..bc963fa5 100644 --- a/lib/clients/authenticated.js +++ b/lib/clients/authenticated.js @@ -1,44 +1,43 @@ const { signRequest } = require('../../lib/request_signer'); - const PublicClient = require('./public.js'); class AuthenticatedClient extends PublicClient { - constructor(key, b64secret, passphrase, apiURI) { - super('', apiURI); - this.key = key; - this.b64secret = b64secret; - this.passphrase = passphrase; - } - - request(method, uriParts, opts = {}, callback) { - if (!callback && typeof opts === 'function') { - callback = opts; - opts = {}; + constructor( + key, + b64secret, + passphrase, + productID, + apiURI, + { rateLimit = 5 } = {} + ) { + if (!key || !b64secret || !passphrase) { + throw new Error( + 'AuthenticatedClient constructor requires arguments `key`, `b64secret`, and `passphrase`.' + ); } - this.addHeaders( - opts, - this._getSignature( - method.toUpperCase(), - this.makeRelativeURI(uriParts), - opts - ) - ); + super(productID, apiURI, { rateLimit }); + this._key = key; + this._b64secret = b64secret; + this._passphrase = passphrase; + } - return super.request(method, uriParts, opts, callback); + _request({ method, uri, queries = {}, headers = {} }) { + const [arg] = arguments; + arg.queries = queries; + arg.headers = headers; + method = method.toUpperCase(); + Object.assign(headers, this._getSignatureHeaders(method, uri, queries)); + return super._request(arg); } - _getSignature(method, relativeURI, opts) { + _getSignatureHeaders(method, relativeURI, queries, body) { const auth = { - key: this.key, - secret: this.b64secret, - passphrase: this.passphrase, + key: this._key, + secret: this._b64secret, + passphrase: this._passphrase, }; - const sig = signRequest(auth, method, relativeURI, opts); - - if (opts.body) { - opts.body = JSON.stringify(opts.body); - } + const sig = signRequest(auth, method, relativeURI, queries, body); return { 'CB-ACCESS-KEY': sig.key, 'CB-ACCESS-SIGN': sig.signature, @@ -48,11 +47,11 @@ class AuthenticatedClient extends PublicClient { } getAccounts(callback) { - return this.get(['accounts'], callback); + return this._get({ uri: `/accounts`, callback }); } getAccount(accountID, callback) { - return this.get(['accounts', accountID], callback); + return this._get({ uri: `/accounts/${accountID}`, callback }); } getAccountHistory(accountID, args = {}, callback) { @@ -61,7 +60,11 @@ class AuthenticatedClient extends PublicClient { args = {}; } - return this.get(['accounts', accountID, 'ledger'], { qs: args }, callback); + return this._get({ + uri: `/accounts/${accountID}/ledger`, + queries: args, + callback, + }); } getAccountHolds(accountID, args = {}, callback) { @@ -70,7 +73,11 @@ class AuthenticatedClient extends PublicClient { args = {}; } - return this.get(['accounts', accountID, 'holds'], { qs: args }, callback); + return this._get({ + uri: `/accounts/${accountID}/holds`, + queries: args, + callback, + }); } _placeOrder(params, callback) { @@ -83,11 +90,7 @@ class AuthenticatedClient extends PublicClient { this._requireParams(params, requiredParams); - if (!needsSize && !params.size && !params.funds) { - throw new Error('`opts` must include either `size` or `funds`'); - } - - return this.post(['orders'], { body: params }, callback); + return this._post({ uri: '/orders', body: params, callback }); } buy(params, callback) { @@ -101,7 +104,7 @@ class AuthenticatedClient extends PublicClient { } getTrailingVolume(callback) { - return this.get(['users', 'self', 'trailing-volume'], {}, callback); + return this._get({ uri: `/users/self/trailing-volume`, callback }); } cancelOrder(orderID, callback) { @@ -113,11 +116,11 @@ class AuthenticatedClient extends PublicClient { return Promise.reject(err); } - return this.delete(['orders', orderID], callback); + return this._delete({ uri: `/orders/${orderID}`, callback }); } cancelOrders(callback) { - return this.delete(['orders'], callback); + return this._delete({ uri: `/orders`, callback }); } // temp over ride public call to get Product Orderbook @@ -127,7 +130,11 @@ class AuthenticatedClient extends PublicClient { args = {}; } - return this.get(['products', productId, 'book'], { qs: args }, callback); + return this._get({ + uri: `/products/${productId}/book`, + queries: args, + callback, + }); } cancelAllOrders(args = {}, callback) { @@ -135,34 +142,23 @@ class AuthenticatedClient extends PublicClient { callback = args; args = {}; } - - const opts = { qs: args }; const totalDeletedOrders = []; - const p = function deleteNext() { - return new Promise((resolve, reject) => { - this.delete(['orders'], opts, (err, response, data) => { - if (err || data === null) { - reject(err || 'Could not delete all orders'); - } else { - resolve([response, data]); - } - }); + const deleteNext = () => { + return this._delete({ + uri: `/orders`, + queries: args, }) - .then(values => { - let [response, data] = values; + .then(data => { totalDeletedOrders.push(...data); if (data.length) { - return deleteNext.call(this); + return deleteNext(); } else { - return response; - } - }) - .then(response => { - if (callback) { - callback(undefined, response, totalDeletedOrders); + if (callback) { + callback(undefined, totalDeletedOrders); + } + return totalDeletedOrders; } - return totalDeletedOrders; }) .catch(err => { if (callback) { @@ -170,14 +166,9 @@ class AuthenticatedClient extends PublicClient { } throw err; }); - }.call(this); + }; - if (callback) { - p.catch(() => {}); - return undefined; - } else { - return p; - } + return deleteNext(); } getOrders(args = {}, callback) { @@ -186,7 +177,7 @@ class AuthenticatedClient extends PublicClient { args = {}; } - return this.get(['orders'], { qs: args }, callback); + return this._get({ uri: `/orders`, queries: args, callback }); } getOrder(orderID, callback) { @@ -198,7 +189,7 @@ class AuthenticatedClient extends PublicClient { return Promise.reject(err); } - return this.get(['orders', orderID], callback); + return this._get({ uri: `/orders/${orderID}`, callback }); } getFills(args = {}, callback) { @@ -207,16 +198,16 @@ class AuthenticatedClient extends PublicClient { args = {}; } - return this.get(['fills'], { qs: args }, callback); + return this._get({ uri: `/fills`, queries: args, callback }); } getFundings(callback) { - return this.get(['funding'], callback); + return this._get({ uri: `/funding`, callback }); } repay(params, callback) { this._requireParams(params, ['amount', 'currency']); - return this.post(['funding/repay'], { body: params }, callback); + return this._post({ uri: `/funding/repay`, body: params, callback }); } marginTransfer(params, callback) { @@ -226,12 +217,16 @@ class AuthenticatedClient extends PublicClient { 'currency', 'amount', ]); - return this.post(['profiles/margin-transfer'], { body: params }, callback); + return this._post({ + uri: `/profiles/margin-transfer`, + body: params, + callback, + }); } closePosition(params, callback) { this._requireParams(params, ['repay_only']); - return this.post(['position/close'], { body: params }, callback); + return this._post({ uri: `/position/close`, body: params, callback }); } deposit(params, callback) { @@ -246,7 +241,7 @@ class AuthenticatedClient extends PublicClient { _transferFunds(params, callback) { this._requireParams(params, ['type', 'amount', 'coinbase_account_id']); - return this.post(['transfers'], { body: params }, callback); + return this._post({ uri: `/transfers`, body: params, callback }); } _requireParams(params, required) { diff --git a/lib/clients/public.js b/lib/clients/public.js index 2688cda1..0386a60a 100644 --- a/lib/clients/public.js +++ b/lib/clients/public.js @@ -1,94 +1,102 @@ -const request = require('request'); const { Readable } = require('stream'); +const superagent = require('superagent'); +const agentUse = require('superagent-use'); +const agentPrefix = require('superagent-prefix'); +const Throttle = require('superagent-throttle'); class PublicClient { - constructor(productID = 'BTC-USD', apiURI = 'https://api.gdax.com') { - this.productID = productID; - this.apiURI = apiURI; - this.API_LIMIT = 100; + constructor( + productID = 'BTC-USD', + apiURI = 'https://api.gdax.com', + { rateLimit = 3 } = {} + ) { + this._productID = productID; + this._API_LIMIT = 100; + + this._agent = agentUse(superagent).use(agentPrefix(apiURI)).use( + new Throttle({ + rate: rateLimit, + ratePer: 1000, + }).plugin() + ); } - get(...args) { - return this.request('get', ...args); + _get(args) { + return this._request(Object.assign({ method: 'get' }, args)); } - put(...args) { - return this.request('put', ...args); + _put(args) { + return this._request(Object.assign({ method: 'put' }, args)); } - post(...args) { - return this.request('post', ...args); + _post(args) { + return this._request(Object.assign({ method: 'post' }, args)); } - delete(...args) { - return this.request('delete', ...args); + _delete(args) { + return this._request(Object.assign({ method: 'delete' }, args)); } - addHeaders(obj, additional) { - obj.headers = obj.headers || {}; - return Object.assign( - obj.headers, - { - 'User-Agent': 'gdax-node-client', - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - additional - ); - } + _requestParamsToObj(...args) { + if (args.length === 1 && typeof args[0] === 'object') { + return args[0]; + } - makeRelativeURI(parts) { - return '/' + parts.join('/'); + let obj = {}; + ['uri', 'queries', 'headers', 'body', 'callback'].forEach((param, i) => { + obj[param] = args[i]; + }); + return obj; } - makeAbsoluteURI(relativeURI) { - return this.apiURI + relativeURI; - } + _request({ method, uri, queries = {}, headers = {}, body = {}, callback }) { + method = method.toLowerCase(); - makeRequestCallback(callback, resolve, reject) { - return (err, response, data) => { - try { - data = JSON.parse(data); - } catch (e) { - data = null; - } - if (typeof callback === 'function') { - callback(err, response, data); - return; - } - if (err) { - reject(err); - } - if (data === null) { - reject(new Error('Response could not be parsed as JSON')); - } else { - resolve(data); - } - }; - } + const request = this._agent[method](uri); + + request.set('User-Agent', 'gdax-node-client').query(queries).accept('json'); - request(method, uriParts, opts = {}, callback) { - if (!callback && typeof opts === 'function') { - callback = opts; - opts = {}; + for (let h in headers) { + request.set(h, headers[h]); } - Object.assign(opts, { - method: method.toUpperCase(), - uri: this.makeAbsoluteURI(this.makeRelativeURI(uriParts)), - }); - this.addHeaders(opts); - const p = new Promise((resolve, reject) => { - request(opts, this.makeRequestCallback(callback, resolve, reject)); - }); + if (method === 'put' || method === 'post') { + request.type('json').send(body); + } + + let responsePromise = request + .then(response => { + if (callback) { + callback(null, response.body); + } else { + return response.body; + } + }) + .catch(error => { + // If GDAX returns an error message, override and make it the primary + // message of the error object returned to the user. + // GDAX API returns json body { "message": "" } + if ( + typeof error.response === 'object' && + typeof error.response.body === 'object' && + error.response.body.message + ) { + error.message = error.response.body.message; + } + + if (callback) { + callback(error); + } else { + throw error; + } + }); if (callback) { - p.catch(() => {}); return undefined; } else { - return p; + return responsePromise; } } getProducts(callback) { - return this.get(['products'], callback); + return this._get({ uri: '/products', callback }); } getProductOrderBook(args = {}, callback) { @@ -97,15 +105,15 @@ class PublicClient { args = {}; } - return this.get( - ['products', this.productID, 'book'], - { qs: args }, - callback - ); + return this._get({ + uri: `/products/${this._productID}/book`, + queries: args, + callback, + }); } getProductTicker(callback) { - return this.get(['products', this.productID, 'ticker'], callback); + return this._get({ uri: `/products/${this._productID}/ticker`, callback }); } getProductTrades(args = {}, callback) { @@ -113,87 +121,67 @@ class PublicClient { callback = args; args = {}; } - return this.get( - ['products', this.productID, 'trades'], - { qs: args }, - callback - ); + return this._get({ + uri: `/products/${this._productID}/trades`, + queries: args, + callback, + }); } getProductTradeStream(tradesFrom, tradesTo) { - let shouldStop = null; + let stopFn = () => false; if (typeof tradesTo === 'function') { - shouldStop = tradesTo; - tradesTo = null; + stopFn = tradesTo; + tradesTo = Infinity; } - const rs = new Readable({ objectMode: true }); + const stream = new Readable({ objectMode: true }); let started = false; - rs._read = () => { + stream._read = () => { if (!started) { started = true; - fetchTrades.call(this, rs, tradesFrom, tradesTo, shouldStop, 0); + fetchTrades.call(this, tradesFrom, tradesTo); } }; - return rs; + return stream; - function fetchTrades(stream, tradesFrom, tradesTo, shouldStop) { - let after = tradesFrom + this.API_LIMIT + 1; - let loop = true; + function fetchTrades(tradesFrom, tradesTo) { + let after = tradesFrom + this._API_LIMIT + 1; - if (tradesTo && tradesTo <= after) { + if (tradesTo < after) { after = tradesTo; - loop = false; } - let opts = { before: tradesFrom, after: after, limit: this.API_LIMIT }; - - this.getProductTrades(opts, (err, resp, data) => { - if (err) { - stream.emit('error', err); - return; - } + let opts = { before: tradesFrom, after: after, limit: this._API_LIMIT }; - if (resp.statusCode === 429) { - // rate-limited, try again - setTimeout(() => { - fetchTrades.call(this, stream, tradesFrom, tradesTo, shouldStop); - }, 900); - return; - } + this.getProductTrades(opts) + .then(data => { + let trade; - if (resp.statusCode !== 200) { - stream.emit( - 'error', - new Error('Encountered status code ' + resp.statusCode) - ); - } + while (data.length) { + trade = data.pop(); + trade.trade_id = parseInt(trade.trade_id); - for (var i = data.length - 1; i >= 0; i--) { - if (shouldStop && shouldStop(data[i])) { - stream.push(null); - return; - } + if (stopFn(trade)) { + stream.push(null); + return; + } - stream.push(data[i]); - } + if (trade.trade_id >= tradesTo - 1) { + stream.push(trade); + stream.push(null); + return; + } - if (!loop) { - stream.push(null); - return; - } + stream.push(trade); + } - fetchTrades.call( - this, - stream, - tradesFrom + this.API_LIMIT, - tradesTo, - shouldStop - ); - }); + fetchTrades.call(this, trade.trade_id, tradesTo); + }) + .catch(err => stream.emit('error', err)); } } @@ -202,23 +190,23 @@ class PublicClient { callback = args; args = {}; } - return this.get( - ['products', this.productID, 'candles'], - { qs: args }, - callback - ); + return this._get({ + uri: `/products/${this._productID}/candles`, + queries: args, + callback, + }); } getProduct24HrStats(callback) { - return this.get(['products', this.productID, 'stats'], callback); + return this._get({ uri: `/products/${this._productID}/stats`, callback }); } getCurrencies(callback) { - return this.get(['currencies'], callback); + return this._get({ uri: '/currencies', callback }); } getTime(callback) { - return this.get(['time'], callback); + return this._get({ uri: '/time', callback }); } } diff --git a/lib/orderbook_sync.js b/lib/orderbook_sync.js index 5a2747ab..a86018a7 100644 --- a/lib/orderbook_sync.js +++ b/lib/orderbook_sync.js @@ -17,6 +17,7 @@ class OrderbookSync extends WebsocketClient { this._queues = {}; // [] this._sequences = {}; // -1 this._public_clients = {}; + this._apiURI = apiURI; this.books = {}; this.productIDs.forEach(productID => { @@ -37,9 +38,9 @@ class OrderbookSync extends WebsocketClient { this._queues[product_id].push(data); if (this._sequences[product_id] === -2) { - // Start first resync - this._sequences[product_id] = -1; - this.loadOrderbook(product_id); + // Start first resync + this._sequences[product_id] = -1; + this.loadOrderbook(product_id); } } else { this.processMessage(data); @@ -63,7 +64,7 @@ class OrderbookSync extends WebsocketClient { if (!this._public_clients[productID]) { this._public_clients[productID] = new PublicClient( productID, - this.apiURI + this._apiURI ); } this._public_clients[productID] diff --git a/lib/request_signer.js b/lib/request_signer.js index b4f4275f..53976595 100644 --- a/lib/request_signer.js +++ b/lib/request_signer.js @@ -3,26 +3,32 @@ const crypto = require('crypto'); const querystring = require('querystring'); /** Signs request messages for authenticated requests to GDAX - * @param auth {object} hash containing key, secret and passphrase - * @param method {string} The REST method to use - * @param path {string} The request path, e.g. /products/BTC-USD/ticker - * @param options {object} An optional object containing one of - * @param options.body {object} A hash of body properties - * @param options.qs {object} A hash of query string parameters + * @param {object} auth - hash containing key, secret and passphrase + * @param {string} method - The REST method to use + * @param {string} path - The request path, e.g. /products/BTC-USD/ticker + * @param {object} queries - An optional object containing one of + * @param {object} body - An object used as JSON payload for a POST or PUT request * @returns {{key: string, signature: *, timestamp: number, passphrase: string}} */ -module.exports.signRequest = (auth, method, path, options = {}) => { - const timestamp = Date.now() / 1000; - let body = ''; - if (options.body) { - body = JSON.stringify(options.body); - } else if (options.qs && Object.keys(options.qs).length !== 0) { - body = '?' + querystring.stringify(options.qs); +module.exports.signRequest = (auth, method, path, queries, body) => { + let queriesString = ''; + let bodyString = ''; + + if (queries && typeof queries === 'object') { + queriesString = '?' + querystring.stringify(queries); + } + if (body && typeof body === 'object') { + bodyString = JSON.stringify(body); } - const what = timestamp + method.toUpperCase() + path + body; + + const timestamp = Date.now() / 1000; + + const what = + timestamp + method.toUpperCase() + path + queriesString + bodyString; const key = Buffer(auth.secret, 'base64'); const hmac = crypto.createHmac('sha256', key); const signature = hmac.update(what).digest('base64'); + return { key: auth.key, signature: signature, diff --git a/package.json b/package.json index 03f845e8..bf4a7ce5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gdax", - "version": "0.4.3", + "version": "0.5.0", "author": "Coinbase", "bugs": "https://github.com/coinbase/gdax-node/issues", "contributors": [ @@ -23,7 +23,10 @@ "dependencies": { "bintrees": "1.0.1", "num": "0.3.0", - "request": "2.81.0", + "superagent": "3.5.2", + "superagent-prefix": "0.0.2", + "superagent-throttle": "0.2.5", + "superagent-use": "0.1.0", "ws": "3.0.0" }, "description": "Client for the GDAX API", diff --git a/tests/authenticated.spec.js b/tests/authenticated.spec.js index 2f29bbf0..ab048d7d 100644 --- a/tests/authenticated.spec.js +++ b/tests/authenticated.spec.js @@ -4,12 +4,21 @@ const nock = require('nock'); const Gdax = require('../index.js'); const key = 'key'; -const secret = 'secret'; +const b64secret = 'secret'; const passphrase = 'passphrase'; const EXCHANGE_API_URL = 'https://api.gdax.com'; -const authClient = new Gdax.AuthenticatedClient(key, secret, passphrase); +const authClient = new Gdax.AuthenticatedClient( + key, + b64secret, + passphrase, + undefined, + undefined, + { + rateLimit: Infinity, + } +); suite('AuthenticatedClient', () => { afterEach(() => nock.cleanAll()); @@ -22,7 +31,7 @@ suite('AuthenticatedClient', () => { uri: 'https://api.gdax.com/orders', }; - const sig = authClient._getSignature(method, relativeURI, opts); + const sig = authClient._getSignatureHeaders(method, relativeURI, opts); assert.equal(sig['CB-ACCESS-KEY'], key); assert.equal(sig['CB-ACCESS-PASSPHRASE'], passphrase); @@ -31,7 +40,7 @@ suite('AuthenticatedClient', () => { assert(sig['CB-ACCESS-SIGN']); }); - test('.getAccount()', done => { + test('.getAccount()', () => { const expectedResponse = { id: 'a1b2c3d4', balance: '1.100', @@ -46,7 +55,7 @@ suite('AuthenticatedClient', () => { .reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => - authClient.getAccount('test-id', (err, resp, data) => { + authClient.getAccount('test-id', (err, data) => { if (err) { reject(err); } @@ -59,12 +68,10 @@ suite('AuthenticatedClient', () => { .getAccount('test-id') .then(data => assert.deepEqual(data, expectedResponse)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.getAccounts()', done => { + test('.getAccounts()', () => { const expectedResponse = [ { id: 'a1b2c3d4', @@ -81,7 +88,7 @@ suite('AuthenticatedClient', () => { .reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => - authClient.getAccounts((err, resp, data) => { + authClient.getAccounts((err, data) => { if (err) { reject(err); } @@ -94,12 +101,10 @@ suite('AuthenticatedClient', () => { .getAccounts() .then(data => assert.deepEqual(data, expectedResponse)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.getAccountHistory()', done => { + test('.getAccountHistory()', () => { const expectedResponse = [ { id: '100', @@ -121,7 +126,7 @@ suite('AuthenticatedClient', () => { .reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => - authClient.getAccountHistory('test-id', (err, resp, data) => { + authClient.getAccountHistory('test-id', (err, data) => { if (err) { reject(err); } @@ -134,12 +139,10 @@ suite('AuthenticatedClient', () => { .getAccountHistory('test-id') .then(data => assert.deepEqual(data, expectedResponse)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.getAccountHolds()', done => { + test('.getAccountHolds()', () => { const expectedResponse = [ { id: '82dcd140-c3c7-4507-8de4-2c529cd1a28f', @@ -158,7 +161,7 @@ suite('AuthenticatedClient', () => { .reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { - authClient.getAccountHolds('test-id', (err, resp, data) => { + authClient.getAccountHolds('test-id', (err, data) => { if (err) { reject(err); } @@ -171,12 +174,10 @@ suite('AuthenticatedClient', () => { .getAccountHolds('test-id') .then(data => assert.deepEqual(data, expectedResponse)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.buy()', done => { + test('.buy()', () => { const order = { size: '10', product_id: 'BTC-USD', @@ -196,7 +197,7 @@ suite('AuthenticatedClient', () => { .reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { - authClient.buy(order, (err, resp, data) => { + authClient.buy(order, (err, data) => { if (err) { reject(err); } @@ -209,13 +210,11 @@ suite('AuthenticatedClient', () => { .buy(order) .then(data => assert.deepEqual(data, expectedResponse)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.buy() market order', done => { - var order = { + test('.buy() market order', () => { + const order = { funds: '20.00', product_id: 'BTC-USD', type: 'market', @@ -224,24 +223,33 @@ suite('AuthenticatedClient', () => { const expectedOrder = order; expectedOrder.side = 'buy'; - var expectedResponse = { + const expectedResponse = { id: '0428b97b-bec1-429e-a94c-59992926778d', }; nock(EXCHANGE_API_URL) .post('/orders', expectedOrder) + .times(2) .reply(200, expectedResponse); - authClient.buy(order, (err, resp, data) => { - assert.ifError(err); - assert.deepEqual(data, expectedResponse); - - nock.cleanAll(); - done(); + const cbTest = new Promise((resolve, reject) => { + authClient.buy(order, (err, data) => { + if (err) { + reject(err); + } + assert.deepEqual(data, expectedResponse); + resolve(); + }); }); + + const promiseTest = cbTest + .then(() => authClient.buy(order)) + .then(data => assert.deepEqual(data, expectedResponse)); + + return Promise.all([cbTest, promiseTest]); }); - test('.sell()', done => { + test('.sell()', () => { const order = { size: '10', product_id: 'BTC-USD', @@ -260,8 +268,8 @@ suite('AuthenticatedClient', () => { .times(2) .reply(200, expectedResponse); - let cbtest = new Promise((resolve, reject) => { - authClient.sell(order, (err, resp, data) => { + const cbtest = new Promise((resolve, reject) => { + authClient.sell(order, (err, data) => { if (err) { reject(err); } @@ -270,16 +278,14 @@ suite('AuthenticatedClient', () => { }); }); - let promisetest = authClient - .sell(order) + const promisetest = cbtest + .then(() => authClient.sell(order)) .then(data => assert.deepEqual(data, expectedResponse)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.getProductOrderBook()', done => { + test('.getProductOrderBook()', () => { nock(EXCHANGE_API_URL) .get('/products/BTC-USD/book?level=3') .times(2) @@ -289,26 +295,20 @@ suite('AuthenticatedClient', () => { }); let cbtest = new Promise((resolve, reject) => { - authClient.getProductOrderBook( - { level: 3 }, - 'BTC-USD', - (err, resp, data) => { - if (err) { - reject(err); - } - assert(data); - resolve(); + authClient.getProductOrderBook({ level: 3 }, 'BTC-USD', (err, data) => { + if (err) { + reject(err); } - ); + assert(data); + resolve(); + }); }); let promisetest = authClient .getProductOrderBook({ level: 3 }, 'BTC-USD') .then(data => assert(data)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); suite('.cancelAllOrders()', () => { @@ -326,9 +326,10 @@ suite('AuthenticatedClient', () => { 'deleted-id-7', 'deleted-id-8', ]; - const totalExpectedDeleted = cancelledOrdersOne.concat( - cancelledOrdersTwo - ); + const totalExpectedDeleted = [ + ...cancelledOrdersOne, + ...cancelledOrdersTwo, + ]; const nockSetup = () => { // first list of Id's that just got cancelled @@ -347,7 +348,7 @@ suite('AuthenticatedClient', () => { nockSetup(); let cbtest = new Promise((resolve, reject) => { - const p = authClient.cancelAllOrders((err, resp, data) => { + authClient.cancelAllOrders((err, data) => { if (err) { reject(err); } else { @@ -355,10 +356,6 @@ suite('AuthenticatedClient', () => { } resolve(); }); - - if (p !== undefined) { - reject(); - } }); return cbtest @@ -370,33 +367,25 @@ suite('AuthenticatedClient', () => { }); test('handles errors', () => { - nock(EXCHANGE_API_URL).delete('/orders').times(2).reply(404, null); - - let cbTest = new Promise((resolve, reject) => { - authClient.cancelAllOrders(err => { - if (err) { - resolve(); - } else { - reject(); - } - }); - }); + nock(EXCHANGE_API_URL).delete('/orders').reply(404, null); - return cbTest - .then(() => { - return authClient.cancelAllOrders(); + return authClient + .cancelAllOrders((err, data) => { + assert(err); + assert.ifError(data); }) - .then(() => assert.fail('should have thrown an error')) + .then(() => assert.fail(`Didn't throw error`)) .catch(err => assert(err)); }); }); suite('.cancelOrder()', () => { - test('requires orderID', done => { + test('requires orderID', () => { let cbtest = new Promise(resolve => { authClient - .cancelOrder(err => { + .cancelOrder((err, data) => { assert(err); + assert.ifError(data); resolve(); }) .catch(() => {}); @@ -407,12 +396,10 @@ suite('AuthenticatedClient', () => { .catch(err => !assert(err) && true) .then(val => assert.strictEqual(val, true)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('cancels order', done => { + test('cancels order', () => { const expectedResponse = [ { id: 'd50ec984-77a8-460a-b958-66f114b0de9b', @@ -434,7 +421,7 @@ suite('AuthenticatedClient', () => { .reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { - authClient.getOrders((err, resp, data) => { + authClient.getOrders((err, data) => { if (err) { reject(err); } @@ -447,13 +434,11 @@ suite('AuthenticatedClient', () => { .getOrders() .then(data => assert.deepEqual(data, expectedResponse)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); }); - test('.getFills()', done => { + test('.getFills()', () => { const expectedResponse = [ { trade_id: 74, @@ -472,7 +457,7 @@ suite('AuthenticatedClient', () => { nock(EXCHANGE_API_URL).get('/fills').times(2).reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { - authClient.getFills((err, response, data) => { + authClient.getFills((err, data) => { if (err) { reject(err); } @@ -485,12 +470,10 @@ suite('AuthenticatedClient', () => { .getFills() .then(data => assert.deepEqual(data, expectedResponse)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.getFundings()', done => { + test('.getFundings()', () => { const expectedResponse = [ { id: '280c0a56-f2fa-4d3b-a199-92df76fff5cd', @@ -510,7 +493,7 @@ suite('AuthenticatedClient', () => { .reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { - authClient.getFundings((err, resp, data) => { + authClient.getFundings((err, data) => { if (err) { reject(err); } @@ -521,37 +504,36 @@ suite('AuthenticatedClient', () => { let promisetest = authClient.getFundings(); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.repay()', done => { + test('.repay()', () => { const params = { amount: 10000, currency: 'USD', }; - nock(EXCHANGE_API_URL).post('/funding/repay', params).reply(200, {}); - nock(EXCHANGE_API_URL).post('/funding/repay', params).reply(200, {}); + nock(EXCHANGE_API_URL) + .post('/funding/repay', params) + .times(2) + .reply(200, {}); // TODO mock with actual data let cbtest = new Promise((resolve, reject) => { - authClient.repay(params, err => { + authClient.repay(params, (err, data) => { if (err) { reject(err); } + assert.deepEqual(data, {}); resolve(); }); }); let promisetest = authClient.repay(params); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.marginTransfer()', done => { + test('.marginTransfer()', () => { const params = { margin_profile_id: '45fa9e3b-00ba-4631-b907-8a98cbdf21be', type: 'deposit', @@ -580,7 +562,7 @@ suite('AuthenticatedClient', () => { .reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { - authClient.marginTransfer(params, (err, resp, data) => { + authClient.marginTransfer(params, (err, data) => { if (err) { reject(err); } @@ -591,12 +573,10 @@ suite('AuthenticatedClient', () => { let promisetest = authClient.marginTransfer(params); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.closePosition()', done => { + test('.closePosition()', () => { const params = { repay_only: false, }; @@ -604,25 +584,24 @@ suite('AuthenticatedClient', () => { nock(EXCHANGE_API_URL) .post('/position/close', params) .times(2) - .reply(200, {}); + .reply(200, {}); // TODO mock with real data let cbtest = new Promise((resolve, reject) => { - authClient.closePosition(params, err => { + authClient.closePosition(params, (err, data) => { if (err) { reject(err); } + assert.deepEqual(data, {}); resolve(); }); }); let promisetest = authClient.closePosition(params); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.deposit()', done => { + test('.deposit()', () => { const transfer = { amount: 10480, coinbase_account_id: 'test-id', @@ -634,25 +613,24 @@ suite('AuthenticatedClient', () => { nock(EXCHANGE_API_URL) .post('/transfers', expectedTransfer) .times(2) - .reply(200, {}); + .reply(200, {}); // TODO mock with real data; let cbtest = new Promise((resolve, reject) => { - authClient.deposit(transfer, err => { + authClient.deposit(transfer, (err, data) => { if (err) { reject(err); } + assert.deepEqual(data, {}); resolve(); }); }); let promisetest = authClient.deposit(transfer); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail); + return Promise.all([cbtest, promisetest]); }); - test('.withdraw()', done => { + test('.withdraw()', () => { const transfer = { amount: 10480, coinbase_account_id: 'test-id', @@ -664,21 +642,20 @@ suite('AuthenticatedClient', () => { nock(EXCHANGE_API_URL) .post('/transfers', expectedTransfer) .times(2) - .reply(200, {}); + .reply(200, {}); // TODO mock with real data let cbtest = new Promise((resolve, reject) => { - authClient.withdraw(transfer, err => { + authClient.withdraw(transfer, (err, data) => { if (err) { reject(err); } + assert.deepEqual(data, {}); resolve(); }); }); let promisetest = authClient.withdraw(transfer); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.ifError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); }); diff --git a/tests/orderbook_sync.spec.js b/tests/orderbook_sync.spec.js index c1867287..620f5f7b 100644 --- a/tests/orderbook_sync.spec.js +++ b/tests/orderbook_sync.spec.js @@ -8,6 +8,10 @@ let port = 56632; const EXCHANGE_API_URL = 'https://api.gdax.com'; +const key = 'key'; +const b64secret = 'secret'; +const passphrase = 'passphrase'; + suite('OrderbookSync', () => { test('emits a message event', done => { nock(EXCHANGE_API_URL) @@ -49,7 +53,14 @@ suite('OrderbookSync', () => { }); const server = testserver(++port, () => { - const authClient = new Gdax.AuthenticatedClient('key', 'secret', 'pass'); + const authClient = new Gdax.AuthenticatedClient( + key, + b64secret, + passphrase, + { + rateLimit: Infinity, + } + ); const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', EXCHANGE_API_URL, @@ -103,7 +114,16 @@ suite('OrderbookSync', () => { .replyWithError('whoops'); const server = testserver(++port, () => { - const authClient = new Gdax.AuthenticatedClient('key', 'secret', 'pass'); + const authClient = new Gdax.AuthenticatedClient( + key, + b64secret, + passphrase, + undefined, + undefined, + { + rateLimit: Infinity, + } + ); const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', EXCHANGE_API_URL, diff --git a/tests/public_client.spec.js b/tests/public_client.spec.js index 9e302d47..1af83c9d 100644 --- a/tests/public_client.spec.js +++ b/tests/public_client.spec.js @@ -2,14 +2,16 @@ const assert = require('assert'); const nock = require('nock'); const Gdax = require('../index.js'); -const publicClient = new Gdax.PublicClient(); +const publicClient = new Gdax.PublicClient(undefined, undefined, { + rateLimit: Infinity, +}); const EXCHANGE_API_URL = 'https://api.gdax.com'; suite('PublicClient', () => { afterEach(() => nock.cleanAll()); - test('.getProductTrades()', done => { + test('.getProductTrades()', () => { const expectedResponse = [ { time: '2014-11-07T22:19:28.578544Z', @@ -33,25 +35,27 @@ suite('PublicClient', () => { .reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { - publicClient.getProductTrades((err, resp, data) => { + const p = publicClient.getProductTrades((err, data) => { if (err) { reject(err); } assert.deepEqual(data, expectedResponse); resolve(); }); + + if (typeof p !== 'undefined') { + reject('Should not return Promise when callback provided.'); + } }); let promisetest = publicClient .getProductTrades() .then(data => assert.deepEqual(data, expectedResponse)); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.isError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); - test('.getProductTicker() should return values', done => { + test('.getProductTicker() should return values', () => { nock(EXCHANGE_API_URL).get('/products/BTC-USD/ticker').times(2).reply(200, { trade_id: 'test-id', price: '9.00', @@ -59,7 +63,7 @@ suite('PublicClient', () => { }); let cbtest = new Promise((resolve, reject) => { - publicClient.getProductTicker((err, resp, data) => { + publicClient.getProductTicker((err, data) => { if (err) { reject(err); } @@ -78,9 +82,7 @@ suite('PublicClient', () => { assert.equal(data.size, '5'); }); - Promise.all([cbtest, promisetest]) - .then(() => done()) - .catch(err => assert.isError(err) || assert.fail()); + return Promise.all([cbtest, promisetest]); }); suite('.getProductTradeStream()', () => { @@ -124,8 +126,8 @@ suite('PublicClient', () => { from, trade => Date.parse(trade.time) >= 1463068800000 ) - .on('data', data => { - current = data.trade_id; + .on('data', trade => { + current = trade.trade_id; assert.equal(typeof current, 'number'); assert.equal( current, From 0e3ef779d63d008b124da748b7c1724650fe2467 Mon Sep 17 00:00:00 2001 From: Nick Freeman Date: Wed, 2 Aug 2017 17:30:51 -0500 Subject: [PATCH 2/9] Factor out common library constants --- lib/clients/authenticated.js | 6 ++-- lib/clients/public.js | 6 ++-- lib/clients/websocket.js | 6 ++-- lib/constants.js | 7 +++++ lib/orderbook_sync.js | 13 ++++++--- tests/authenticated.spec.js | 55 +++++++++++++----------------------- tests/orderbook_sync.spec.js | 36 +++++++++++------------ tests/public_client.spec.js | 6 ++-- 8 files changed, 69 insertions(+), 66 deletions(-) create mode 100644 lib/constants.js diff --git a/lib/clients/authenticated.js b/lib/clients/authenticated.js index bc963fa5..e351843f 100644 --- a/lib/clients/authenticated.js +++ b/lib/clients/authenticated.js @@ -1,5 +1,7 @@ const { signRequest } = require('../../lib/request_signer'); -const PublicClient = require('./public.js'); +const PublicClient = require('./public'); + +const { GDAX_AUTHED_RATE_LIMIT } = require('../constants'); class AuthenticatedClient extends PublicClient { constructor( @@ -8,7 +10,7 @@ class AuthenticatedClient extends PublicClient { passphrase, productID, apiURI, - { rateLimit = 5 } = {} + { rateLimit = GDAX_AUTHED_RATE_LIMIT } = {} ) { if (!key || !b64secret || !passphrase) { throw new Error( diff --git a/lib/clients/public.js b/lib/clients/public.js index 0386a60a..18c77645 100644 --- a/lib/clients/public.js +++ b/lib/clients/public.js @@ -4,11 +4,13 @@ const agentUse = require('superagent-use'); const agentPrefix = require('superagent-prefix'); const Throttle = require('superagent-throttle'); +const { GDAX_API_URI, GDAX_PUBLIC_RATE_LIMIT } = require('../constants'); + class PublicClient { constructor( productID = 'BTC-USD', - apiURI = 'https://api.gdax.com', - { rateLimit = 3 } = {} + apiURI = GDAX_API_URI, + { rateLimit = GDAX_PUBLIC_RATE_LIMIT } = {} ) { this._productID = productID; this._API_LIMIT = 100; diff --git a/lib/clients/websocket.js b/lib/clients/websocket.js index cfb09dd2..caae4167 100644 --- a/lib/clients/websocket.js +++ b/lib/clients/websocket.js @@ -1,8 +1,10 @@ const { EventEmitter } = require('events'); const Websocket = require('ws'); -const Utils = require('../utilities.js'); +const Utils = require('../utilities'); const { signRequest } = require('../../lib/request_signer'); +const { GDAX_WEBSOCKET_URI } = require('../constants'); + /** * Create a new connection to a websocket feed * @param {String[]} [productIDs] - The GDAX products to listen to. Default: ['BTC-USD'] @@ -10,7 +12,7 @@ const { signRequest } = require('../../lib/request_signer'); * @param {Object} [auth] - An optional object containing your API key details (key, secret & passphrase) */ class WebsocketClient extends EventEmitter { - constructor(productIDs, websocketURI = 'wss://ws-feed.gdax.com', auth) { + constructor(productIDs, websocketURI = GDAX_WEBSOCKET_URI, auth) { super(); this.productIDs = Utils.determineProductIDs(productIDs); this.websocketURI = websocketURI; diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 00000000..a573147b --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,7 @@ +module.exports = { + DEFAULT_BOOK_LEVEL: 3, + GDAX_API_URI: 'https://api.gdax.com', + GDAX_AUTHED_RATE_LIMIT: 5, + GDAX_PUBLIC_RATE_LIMIT: 3, + GDAX_WEBSOCKET_URI: 'wss://ws-feed.gdax.com', +}; diff --git a/lib/orderbook_sync.js b/lib/orderbook_sync.js index a86018a7..7879c9a4 100644 --- a/lib/orderbook_sync.js +++ b/lib/orderbook_sync.js @@ -2,12 +2,18 @@ const WebsocketClient = require('./clients/websocket.js'); const PublicClient = require('./clients/public.js'); const Orderbook = require('./orderbook.js'); +const { + DEFAULT_BOOK_LEVEL, + GDAX_API_URI, + GDAX_WEBSOCKET_URI, +} = require('./constants'); + // Orderbook syncing class OrderbookSync extends WebsocketClient { constructor( productIDs, - apiURI = 'https://api.gdax.com', - websocketURI = 'wss://ws-feed.gdax.com', + apiURI = GDAX_API_URI, + websocketURI = GDAX_WEBSOCKET_URI, authenticatedClient ) { super(productIDs, websocketURI); @@ -52,8 +58,7 @@ class OrderbookSync extends WebsocketClient { return; } - const bookLevel = 3; - const args = { level: bookLevel }; + const args = { level: DEFAULT_BOOK_LEVEL }; if (this.authenticatedClient) { this.authenticatedClient diff --git a/tests/authenticated.spec.js b/tests/authenticated.spec.js index ab048d7d..fe61bb6a 100644 --- a/tests/authenticated.spec.js +++ b/tests/authenticated.spec.js @@ -7,7 +7,7 @@ const key = 'key'; const b64secret = 'secret'; const passphrase = 'passphrase'; -const EXCHANGE_API_URL = 'https://api.gdax.com'; +const { DEFAULT_BOOK_LEVEL, GDAX_API_URI } = require('../lib/constants'); const authClient = new Gdax.AuthenticatedClient( key, @@ -49,7 +49,7 @@ suite('AuthenticatedClient', () => { currency: 'USD', }; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .get('/accounts/test-id') .times(2) .reply(200, expectedResponse); @@ -82,10 +82,7 @@ suite('AuthenticatedClient', () => { }, ]; - nock(EXCHANGE_API_URL) - .get('/accounts') - .times(2) - .reply(200, expectedResponse); + nock(GDAX_API_URI).get('/accounts').times(2).reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => authClient.getAccounts((err, data) => { @@ -120,7 +117,7 @@ suite('AuthenticatedClient', () => { }, ]; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .get('/accounts/test-id/ledger') .times(2) .reply(200, expectedResponse); @@ -155,7 +152,7 @@ suite('AuthenticatedClient', () => { }, ]; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .get('/accounts/test-id/holds') .times(2) .reply(200, expectedResponse); @@ -191,7 +188,7 @@ suite('AuthenticatedClient', () => { id: '0428b97b-bec1-429e-a94c-59992926778d', }; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .post('/orders', expectedOrder) .times(2) .reply(200, expectedResponse); @@ -227,7 +224,7 @@ suite('AuthenticatedClient', () => { id: '0428b97b-bec1-429e-a94c-59992926778d', }; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .post('/orders', expectedOrder) .times(2) .reply(200, expectedResponse); @@ -263,7 +260,7 @@ suite('AuthenticatedClient', () => { id: '0428b97b-bec1-429e-a94c-59992926778d', }; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .post('/orders', expectedOrder) .times(2) .reply(200, expectedResponse); @@ -286,8 +283,8 @@ suite('AuthenticatedClient', () => { }); test('.getProductOrderBook()', () => { - nock(EXCHANGE_API_URL) - .get('/products/BTC-USD/book?level=3') + nock(GDAX_API_URI) + .get(`/products/BTC-USD/book?level=${DEFAULT_BOOK_LEVEL}`) .times(2) .reply(200, { asks: [], @@ -333,7 +330,7 @@ suite('AuthenticatedClient', () => { const nockSetup = () => { // first list of Id's that just got cancelled - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .delete('/orders') .reply(200, cancelledOrdersOne) // second list of Id's that just got cancelled @@ -367,7 +364,7 @@ suite('AuthenticatedClient', () => { }); test('handles errors', () => { - nock(EXCHANGE_API_URL).delete('/orders').reply(404, null); + nock(GDAX_API_URI).delete('/orders').reply(404, null); return authClient .cancelAllOrders((err, data) => { @@ -415,10 +412,7 @@ suite('AuthenticatedClient', () => { }, ]; - nock(EXCHANGE_API_URL) - .get('/orders') - .times(2) - .reply(200, expectedResponse); + nock(GDAX_API_URI).get('/orders').times(2).reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { authClient.getOrders((err, data) => { @@ -454,7 +448,7 @@ suite('AuthenticatedClient', () => { }, ]; - nock(EXCHANGE_API_URL).get('/fills').times(2).reply(200, expectedResponse); + nock(GDAX_API_URI).get('/fills').times(2).reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { authClient.getFills((err, data) => { @@ -487,10 +481,7 @@ suite('AuthenticatedClient', () => { }, ]; - nock(EXCHANGE_API_URL) - .get('/funding') - .times(2) - .reply(200, expectedResponse); + nock(GDAX_API_URI).get('/funding').times(2).reply(200, expectedResponse); let cbtest = new Promise((resolve, reject) => { authClient.getFundings((err, data) => { @@ -513,10 +504,7 @@ suite('AuthenticatedClient', () => { currency: 'USD', }; - nock(EXCHANGE_API_URL) - .post('/funding/repay', params) - .times(2) - .reply(200, {}); // TODO mock with actual data + nock(GDAX_API_URI).post('/funding/repay', params).times(2).reply(200, {}); // TODO mock with actual data let cbtest = new Promise((resolve, reject) => { authClient.repay(params, (err, data) => { @@ -556,7 +544,7 @@ suite('AuthenticatedClient', () => { nonce: 25, }; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .post('/profiles/margin-transfer', params) .times(2) .reply(200, expectedResponse); @@ -581,10 +569,7 @@ suite('AuthenticatedClient', () => { repay_only: false, }; - nock(EXCHANGE_API_URL) - .post('/position/close', params) - .times(2) - .reply(200, {}); // TODO mock with real data + nock(GDAX_API_URI).post('/position/close', params).times(2).reply(200, {}); // TODO mock with real data let cbtest = new Promise((resolve, reject) => { authClient.closePosition(params, (err, data) => { @@ -610,7 +595,7 @@ suite('AuthenticatedClient', () => { const expectedTransfer = transfer; expectedTransfer.type = 'deposit'; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .post('/transfers', expectedTransfer) .times(2) .reply(200, {}); // TODO mock with real data; @@ -639,7 +624,7 @@ suite('AuthenticatedClient', () => { const expectedTransfer = transfer; expectedTransfer.type = 'withdraw'; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .post('/transfers', expectedTransfer) .times(2) .reply(200, {}); // TODO mock with real data diff --git a/tests/orderbook_sync.spec.js b/tests/orderbook_sync.spec.js index 620f5f7b..4f19dcf9 100644 --- a/tests/orderbook_sync.spec.js +++ b/tests/orderbook_sync.spec.js @@ -6,7 +6,7 @@ const Gdax = require('../index.js'); const testserver = require('./lib/ws_testserver'); let port = 56632; -const EXCHANGE_API_URL = 'https://api.gdax.com'; +const { DEFAULT_BOOK_LEVEL, GDAX_API_URI } = require('../lib/constants'); const key = 'key'; const b64secret = 'secret'; @@ -14,8 +14,8 @@ const passphrase = 'passphrase'; suite('OrderbookSync', () => { test('emits a message event', done => { - nock(EXCHANGE_API_URL) - .get('/products/BTC-USD/book?level=3') + nock(GDAX_API_URI) + .get(`/products/BTC-USD/book?level=${DEFAULT_BOOK_LEVEL}`) .times(2) .reply(200, { asks: [], @@ -25,7 +25,7 @@ suite('OrderbookSync', () => { const server = testserver(++port, () => { const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', - EXCHANGE_API_URL, + GDAX_API_URI, 'ws://localhost:' + port ); orderbookSync.on('message', data => { @@ -44,8 +44,8 @@ suite('OrderbookSync', () => { }); test('emits a message event (with AuthenticatedClient)', done => { - nock(EXCHANGE_API_URL) - .get('/products/BTC-USD/book?level=3') + nock(GDAX_API_URI) + .get(`/products/BTC-USD/book?level=${DEFAULT_BOOK_LEVEL}`) .times(2) .reply(200, { asks: [], @@ -63,7 +63,7 @@ suite('OrderbookSync', () => { ); const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', - EXCHANGE_API_URL, + GDAX_API_URI, 'ws://localhost:' + port, authClient ); @@ -83,14 +83,14 @@ suite('OrderbookSync', () => { }); test('emits an error event on error', done => { - nock(EXCHANGE_API_URL) - .get('/products/BTC-USD/book?level=3') + nock(GDAX_API_URI) + .get(`/products/BTC-USD/book?level=${DEFAULT_BOOK_LEVEL}`) .replyWithError('whoops'); const server = testserver(++port, () => { const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', - EXCHANGE_API_URL, + GDAX_API_URI, 'ws://localhost:' + port ); @@ -109,8 +109,8 @@ suite('OrderbookSync', () => { }); test('emits an error event on error (with AuthenticatedClient)', done => { - nock(EXCHANGE_API_URL) - .get('/products/BTC-USD/book?level=3') + nock(GDAX_API_URI) + .get(`/products/BTC-USD/book?level=${DEFAULT_BOOK_LEVEL}`) .replyWithError('whoops'); const server = testserver(++port, () => { @@ -126,7 +126,7 @@ suite('OrderbookSync', () => { ); const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', - EXCHANGE_API_URL, + GDAX_API_URI, 'ws://localhost:' + port, authClient ); @@ -146,16 +146,16 @@ suite('OrderbookSync', () => { }); test('builds specified books', done => { - nock(EXCHANGE_API_URL) - .get('/products/BTC-USD/book?level=3') + nock(GDAX_API_URI) + .get(`/products/BTC-USD/book?level=${DEFAULT_BOOK_LEVEL}`) .times(2) .reply(200, { asks: [], bids: [], }); - nock(EXCHANGE_API_URL) - .get('/products/ETH-USD/book?level=3') + nock(GDAX_API_URI) + .get(`/products/ETH-USD/book?level=${DEFAULT_BOOK_LEVEL}`) .times(2) .reply(200, { asks: [], @@ -165,7 +165,7 @@ suite('OrderbookSync', () => { const server = testserver(++port, () => { const orderbookSync = new Gdax.OrderbookSync( ['BTC-USD', 'ETH-USD'], - EXCHANGE_API_URL, + GDAX_API_URI, 'ws://localhost:' + port ); const btc_usd_state = orderbookSync.books['BTC-USD'].state(); diff --git a/tests/public_client.spec.js b/tests/public_client.spec.js index 1af83c9d..5bb6a7f3 100644 --- a/tests/public_client.spec.js +++ b/tests/public_client.spec.js @@ -6,7 +6,7 @@ const publicClient = new Gdax.PublicClient(undefined, undefined, { rateLimit: Infinity, }); -const EXCHANGE_API_URL = 'https://api.gdax.com'; +const { GDAX_API_URI } = require('../lib/constants'); suite('PublicClient', () => { afterEach(() => nock.cleanAll()); @@ -29,7 +29,7 @@ suite('PublicClient', () => { }, ]; - nock(EXCHANGE_API_URL) + nock(GDAX_API_URI) .get('/products/BTC-USD/trades') .times(2) .reply(200, expectedResponse); @@ -56,7 +56,7 @@ suite('PublicClient', () => { }); test('.getProductTicker() should return values', () => { - nock(EXCHANGE_API_URL).get('/products/BTC-USD/ticker').times(2).reply(200, { + nock(GDAX_API_URI).get('/products/BTC-USD/ticker').times(2).reply(200, { trade_id: 'test-id', price: '9.00', size: '5', From eaefb34fde5dbfdabfccb262df15231e892c6285 Mon Sep 17 00:00:00 2001 From: Nick Freeman Date: Wed, 2 Aug 2017 17:33:24 -0500 Subject: [PATCH 3/9] Rename `args` to `opts` throughout codebase to be more idiomatic with how these "options" are discussed. - Reduce ambiguity with function arguments; options are given as properties on an object which *is an argument itself* - Intended to help users looking at the source for the first time; will also benefit with future auto-generated docs, etc. --- lib/clients/authenticated.js | 60 ++++++++++++++++++------------------ lib/clients/public.js | 58 ++++++++++++++-------------------- lib/orderbook_sync.js | 8 ++--- 3 files changed, 57 insertions(+), 69 deletions(-) diff --git a/lib/clients/authenticated.js b/lib/clients/authenticated.js index e351843f..22bf0634 100644 --- a/lib/clients/authenticated.js +++ b/lib/clients/authenticated.js @@ -56,28 +56,28 @@ class AuthenticatedClient extends PublicClient { return this._get({ uri: `/accounts/${accountID}`, callback }); } - getAccountHistory(accountID, args = {}, callback) { - if (!callback && typeof args === 'function') { - callback = args; - args = {}; + getAccountHistory(accountID, opts = {}, callback) { + if (!callback && typeof opts === 'function') { + callback = opts; + opts = {}; } return this._get({ uri: `/accounts/${accountID}/ledger`, - queries: args, + queries: opts, callback, }); } - getAccountHolds(accountID, args = {}, callback) { - if (!callback && typeof args === 'function') { - callback = args; - args = {}; + getAccountHolds(accountID, opts = {}, callback) { + if (!callback && typeof opts === 'function') { + callback = opts; + opts = {}; } return this._get({ uri: `/accounts/${accountID}/holds`, - queries: args, + queries: opts, callback, }); } @@ -126,30 +126,30 @@ class AuthenticatedClient extends PublicClient { } // temp over ride public call to get Product Orderbook - getProductOrderBook(args = {}, productId, callback) { - if (!callback && typeof args === 'function') { - callback = args; - args = {}; + getProductOrderBook(opts = {}, productId, callback) { + if (!callback && typeof opts === 'function') { + callback = opts; + opts = {}; } return this._get({ uri: `/products/${productId}/book`, - queries: args, + queries: opts, callback, }); } - cancelAllOrders(args = {}, callback) { - if (!callback && typeof args === 'function') { - callback = args; - args = {}; + cancelAllOrders(opts = {}, callback) { + if (!callback && typeof opts === 'function') { + callback = opts; + opts = {}; } const totalDeletedOrders = []; const deleteNext = () => { return this._delete({ uri: `/orders`, - queries: args, + queries: opts, }) .then(data => { totalDeletedOrders.push(...data); @@ -173,13 +173,13 @@ class AuthenticatedClient extends PublicClient { return deleteNext(); } - getOrders(args = {}, callback) { - if (!callback && typeof args === 'function') { - callback = args; - args = {}; + getOrders(opts = {}, callback) { + if (!callback && typeof opts === 'function') { + callback = opts; + opts = {}; } - return this._get({ uri: `/orders`, queries: args, callback }); + return this._get({ uri: `/orders`, queries: opts, callback }); } getOrder(orderID, callback) { @@ -194,13 +194,13 @@ class AuthenticatedClient extends PublicClient { return this._get({ uri: `/orders/${orderID}`, callback }); } - getFills(args = {}, callback) { - if (!callback && typeof args === 'function') { - callback = args; - args = {}; + getFills(opts = {}, callback) { + if (!callback && typeof opts === 'function') { + callback = opts; + opts = {}; } - return this._get({ uri: `/fills`, queries: args, callback }); + return this._get({ uri: `/fills`, queries: opts, callback }); } getFundings(callback) { diff --git a/lib/clients/public.js b/lib/clients/public.js index 18c77645..8be6b974 100644 --- a/lib/clients/public.js +++ b/lib/clients/public.js @@ -23,29 +23,17 @@ class PublicClient { ); } - _get(args) { - return this._request(Object.assign({ method: 'get' }, args)); + _get(opts) { + return this._request(Object.assign({ method: 'get' }, opts)); } - _put(args) { - return this._request(Object.assign({ method: 'put' }, args)); + _put(opts) { + return this._request(Object.assign({ method: 'put' }, opts)); } - _post(args) { - return this._request(Object.assign({ method: 'post' }, args)); + _post(opts) { + return this._request(Object.assign({ method: 'post' }, opts)); } - _delete(args) { - return this._request(Object.assign({ method: 'delete' }, args)); - } - - _requestParamsToObj(...args) { - if (args.length === 1 && typeof args[0] === 'object') { - return args[0]; - } - - let obj = {}; - ['uri', 'queries', 'headers', 'body', 'callback'].forEach((param, i) => { - obj[param] = args[i]; - }); - return obj; + _delete(opts) { + return this._request(Object.assign({ method: 'delete' }, opts)); } _request({ method, uri, queries = {}, headers = {}, body = {}, callback }) { @@ -101,15 +89,15 @@ class PublicClient { return this._get({ uri: '/products', callback }); } - getProductOrderBook(args = {}, callback) { - if (!callback && typeof args === 'function') { - callback = args; - args = {}; + getProductOrderBook(opts = {}, callback) { + if (!callback && typeof opts === 'function') { + callback = opts; + opts = {}; } return this._get({ uri: `/products/${this._productID}/book`, - queries: args, + queries: opts, callback, }); } @@ -118,14 +106,14 @@ class PublicClient { return this._get({ uri: `/products/${this._productID}/ticker`, callback }); } - getProductTrades(args = {}, callback) { - if (!callback && typeof args === 'function') { - callback = args; - args = {}; + getProductTrades(opts = {}, callback) { + if (!callback && typeof opts === 'function') { + callback = opts; + opts = {}; } return this._get({ uri: `/products/${this._productID}/trades`, - queries: args, + queries: opts, callback, }); } @@ -187,14 +175,14 @@ class PublicClient { } } - getProductHistoricRates(args = {}, callback) { - if (!callback && typeof args === 'function') { - callback = args; - args = {}; + getProductHistoricRates(opts = {}, callback) { + if (!callback && typeof opts === 'function') { + callback = opts; + opts = {}; } return this._get({ uri: `/products/${this._productID}/candles`, - queries: args, + queries: opts, callback, }); } diff --git a/lib/orderbook_sync.js b/lib/orderbook_sync.js index 7879c9a4..8571931c 100644 --- a/lib/orderbook_sync.js +++ b/lib/orderbook_sync.js @@ -58,11 +58,11 @@ class OrderbookSync extends WebsocketClient { return; } - const args = { level: DEFAULT_BOOK_LEVEL }; + const opts = { level: DEFAULT_BOOK_LEVEL }; if (this.authenticatedClient) { this.authenticatedClient - .getProductOrderBook(args, productID) + .getProductOrderBook(opts, productID) .then(onData.bind(this)) .catch(onError.bind(this)); } else { @@ -73,7 +73,7 @@ class OrderbookSync extends WebsocketClient { ); } this._public_clients[productID] - .getProductOrderBook(args) + .getProductOrderBook(opts) .then(onData.bind(this)) .catch(onError.bind(this)); } @@ -82,7 +82,7 @@ class OrderbookSync extends WebsocketClient { this.books[productID].state(data); this._sequences[productID] = data.sequence; - this._queues[productID].forEach(this.processMessage.bind(this)); + this._queues[productID].forEach(this.processMessage, this); this._queues[productID] = []; } From e6baf36dac9f4129d4d93ae8bf79c07a1d4419ce Mon Sep 17 00:00:00 2001 From: Nick Freeman Date: Wed, 2 Aug 2017 02:57:21 -0400 Subject: [PATCH 4/9] Move `_requireParams()` helper function from authed to public client --- lib/clients/authenticated.js | 9 --------- lib/clients/public.js | 9 +++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/clients/authenticated.js b/lib/clients/authenticated.js index 22bf0634..a3574c8e 100644 --- a/lib/clients/authenticated.js +++ b/lib/clients/authenticated.js @@ -245,15 +245,6 @@ class AuthenticatedClient extends PublicClient { this._requireParams(params, ['type', 'amount', 'coinbase_account_id']); return this._post({ uri: `/transfers`, body: params, callback }); } - - _requireParams(params, required) { - for (let param of required) { - if (params[param] === undefined) { - throw new Error('`opts` must include param `' + param + '`'); - } - } - return true; - } } module.exports = exports = AuthenticatedClient; diff --git a/lib/clients/public.js b/lib/clients/public.js index 8be6b974..c33d01e9 100644 --- a/lib/clients/public.js +++ b/lib/clients/public.js @@ -85,6 +85,15 @@ class PublicClient { } } + _requireParams(params, required) { + for (let param of required) { + if (params[param] === undefined) { + throw new Error('parameter `' + param + '` is required'); + } + } + return true; + } + getProducts(callback) { return this._get({ uri: '/products', callback }); } From 12f45b56e8d1e78004fd4fc552b6844fef3fe0bf Mon Sep 17 00:00:00 2001 From: Nick Freeman Date: Wed, 2 Aug 2017 03:52:13 -0400 Subject: [PATCH 5/9] Add static method `PublicClient.getProductOrderBook()` Implementation also lays foundation for more static methods --- lib/clients/public.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/clients/public.js b/lib/clients/public.js index c33d01e9..e5ff6c92 100644 --- a/lib/clients/public.js +++ b/lib/clients/public.js @@ -6,6 +6,13 @@ const Throttle = require('superagent-throttle'); const { GDAX_API_URI, GDAX_PUBLIC_RATE_LIMIT } = require('../constants'); +const staticAgent = agentUse(superagent).use(agentPrefix(GDAX_API_URI)).use( + new Throttle({ + rate: GDAX_PUBLIC_RATE_LIMIT, + ratePer: 1000, + }).plugin() +); + class PublicClient { constructor( productID = 'BTC-USD', @@ -23,6 +30,10 @@ class PublicClient { ); } + static _get(opts) { + return PublicClient._request(Object.assign({ method: 'get' }, opts)); + } + _get(opts) { return this._request(Object.assign({ method: 'get' }, opts)); } @@ -36,6 +47,10 @@ class PublicClient { return this._request(Object.assign({ method: 'delete' }, opts)); } + static _request(arg) { + return PublicClient.prototype._request.call({ _agent: staticAgent }, arg); + } + _request({ method, uri, queries = {}, headers = {}, body = {}, callback }) { method = method.toLowerCase(); @@ -85,6 +100,10 @@ class PublicClient { } } + static _requireParams() { + return PublicClient.prototype._requireParams(...arguments); + } + _requireParams(params, required) { for (let param of required) { if (params[param] === undefined) { @@ -98,14 +117,29 @@ class PublicClient { return this._get({ uri: '/products', callback }); } + static getProductOrderBook(opts = {}, callback) { + PublicClient._requireParams(opts, ['productID']); + let { productID } = opts; + delete opts.productID; + + return PublicClient._get({ + uri: `/products/${productID}/book`, + queries: opts, + callback, + }); + } + getProductOrderBook(opts = {}, callback) { if (!callback && typeof opts === 'function') { callback = opts; opts = {}; } + const productID = opts.productID || this._productID; + delete opts.productID; + return this._get({ - uri: `/products/${this._productID}/book`, + uri: `/products/${productID}/book`, queries: opts, callback, }); From 6bff3c27ad142e913806cc9f3bcd444cc5a7fab9 Mon Sep 17 00:00:00 2001 From: Nick Freeman Date: Wed, 2 Aug 2017 17:43:32 -0500 Subject: [PATCH 6/9] `PublicClient`'s `getProductOrderBook` methods should fall back to library's `DEFAULT_BOOK_LEVEL` --- lib/clients/public.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/clients/public.js b/lib/clients/public.js index e5ff6c92..a47839c9 100644 --- a/lib/clients/public.js +++ b/lib/clients/public.js @@ -4,7 +4,11 @@ const agentUse = require('superagent-use'); const agentPrefix = require('superagent-prefix'); const Throttle = require('superagent-throttle'); -const { GDAX_API_URI, GDAX_PUBLIC_RATE_LIMIT } = require('../constants'); +const { + DEFAULT_BOOK_LEVEL, + GDAX_API_URI, + GDAX_PUBLIC_RATE_LIMIT, +} = require('../constants'); const staticAgent = agentUse(superagent).use(agentPrefix(GDAX_API_URI)).use( new Throttle({ @@ -122,6 +126,8 @@ class PublicClient { let { productID } = opts; delete opts.productID; + opts.level = opts.level || DEFAULT_BOOK_LEVEL; + return PublicClient._get({ uri: `/products/${productID}/book`, queries: opts, @@ -138,6 +144,8 @@ class PublicClient { const productID = opts.productID || this._productID; delete opts.productID; + opts.level = opts.level || DEFAULT_BOOK_LEVEL; + return this._get({ uri: `/products/${productID}/book`, queries: opts, From 311d5b528f4b13c9de3780f6655443bf07c8af56 Mon Sep 17 00:00:00 2001 From: Nick Freeman Date: Wed, 2 Aug 2017 03:01:42 -0400 Subject: [PATCH 7/9] Remove overriden `getProductOrderBook()` in `AuthenticatedClient` See #46 --- lib/clients/authenticated.js | 14 --------- lib/orderbook_sync.js | 56 +++++++++++------------------------- tests/authenticated.spec.js | 4 +-- 3 files changed, 19 insertions(+), 55 deletions(-) diff --git a/lib/clients/authenticated.js b/lib/clients/authenticated.js index a3574c8e..0c417ae0 100644 --- a/lib/clients/authenticated.js +++ b/lib/clients/authenticated.js @@ -125,20 +125,6 @@ class AuthenticatedClient extends PublicClient { return this._delete({ uri: `/orders`, callback }); } - // temp over ride public call to get Product Orderbook - getProductOrderBook(opts = {}, productId, callback) { - if (!callback && typeof opts === 'function') { - callback = opts; - opts = {}; - } - - return this._get({ - uri: `/products/${productId}/book`, - queries: opts, - callback, - }); - } - cancelAllOrders(opts = {}, callback) { if (!callback && typeof opts === 'function') { callback = opts; diff --git a/lib/orderbook_sync.js b/lib/orderbook_sync.js index 8571931c..9c09a20e 100644 --- a/lib/orderbook_sync.js +++ b/lib/orderbook_sync.js @@ -1,12 +1,8 @@ const WebsocketClient = require('./clients/websocket.js'); -const PublicClient = require('./clients/public.js'); +const PublicClient = require('./clients/public'); const Orderbook = require('./orderbook.js'); -const { - DEFAULT_BOOK_LEVEL, - GDAX_API_URI, - GDAX_WEBSOCKET_URI, -} = require('./constants'); +const { GDAX_API_URI, GDAX_WEBSOCKET_URI } = require('./constants'); // Orderbook syncing class OrderbookSync extends WebsocketClient { @@ -14,16 +10,15 @@ class OrderbookSync extends WebsocketClient { productIDs, apiURI = GDAX_API_URI, websocketURI = GDAX_WEBSOCKET_URI, - authenticatedClient + client = PublicClient ) { super(productIDs, websocketURI); - this.apiURI = apiURI; - this.authenticatedClient = authenticatedClient; - this._queues = {}; // [] - this._sequences = {}; // -1 - this._public_clients = {}; this._apiURI = apiURI; + this._client = client; + + this._queues = {}; + this._sequences = {}; this.books = {}; this.productIDs.forEach(productID => { @@ -53,46 +48,29 @@ class OrderbookSync extends WebsocketClient { } } - loadOrderbook(productID) { + loadOrderbook(productID, opts = {}) { if (!this.books[productID]) { return; } - const opts = { level: DEFAULT_BOOK_LEVEL }; - - if (this.authenticatedClient) { - this.authenticatedClient - .getProductOrderBook(opts, productID) - .then(onData.bind(this)) - .catch(onError.bind(this)); - } else { - if (!this._public_clients[productID]) { - this._public_clients[productID] = new PublicClient( - productID, - this._apiURI - ); - } - this._public_clients[productID] - .getProductOrderBook(opts) - .then(onData.bind(this)) - .catch(onError.bind(this)); - } + opts.productID = productID; - function onData(data) { + const onData = data => { this.books[productID].state(data); - this._sequences[productID] = data.sequence; - this._queues[productID].forEach(this.processMessage, this); + this._queues[productID].forEach(message => this._processMessage(message)); this._queues[productID] = []; - } + }; - function onError(err) { + const onError = err => { err = err ? (err.message ? err.message : err) : ''; this.emit('error', new Error('Failed to load orderbook: ' + err)); - } + }; + + this._client.getProductOrderBook(opts).then(onData).catch(onError); } - processMessage(data) { + _processMessage(data) { const { product_id } = data; if (this._sequences[product_id] === -1) { diff --git a/tests/authenticated.spec.js b/tests/authenticated.spec.js index fe61bb6a..52e09366 100644 --- a/tests/authenticated.spec.js +++ b/tests/authenticated.spec.js @@ -292,7 +292,7 @@ suite('AuthenticatedClient', () => { }); let cbtest = new Promise((resolve, reject) => { - authClient.getProductOrderBook({ level: 3 }, 'BTC-USD', (err, data) => { + authClient.getProductOrderBook({ level: 3 }, (err, data) => { if (err) { reject(err); } @@ -302,7 +302,7 @@ suite('AuthenticatedClient', () => { }); let promisetest = authClient - .getProductOrderBook({ level: 3 }, 'BTC-USD') + .getProductOrderBook({ level: 3 }) .then(data => assert(data)); return Promise.all([cbtest, promisetest]); From eda6aea868498a21e46ede3d14eb5f1e9d7ae27b Mon Sep 17 00:00:00 2001 From: Nick Freeman Date: Wed, 2 Aug 2017 18:07:35 -0500 Subject: [PATCH 8/9] Remove redundant parameter `apiURI` from `OrderbookSync` constructor If a different gdax api URI is desired, the user should provide an instance of a client that has its `apiURI` set BREAKING CHANGE --- lib/orderbook_sync.js | 4 +--- tests/orderbook_sync.spec.js | 5 ----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/orderbook_sync.js b/lib/orderbook_sync.js index 9c09a20e..a0feadf8 100644 --- a/lib/orderbook_sync.js +++ b/lib/orderbook_sync.js @@ -2,19 +2,17 @@ const WebsocketClient = require('./clients/websocket.js'); const PublicClient = require('./clients/public'); const Orderbook = require('./orderbook.js'); -const { GDAX_API_URI, GDAX_WEBSOCKET_URI } = require('./constants'); +const { GDAX_WEBSOCKET_URI } = require('./constants'); // Orderbook syncing class OrderbookSync extends WebsocketClient { constructor( productIDs, - apiURI = GDAX_API_URI, websocketURI = GDAX_WEBSOCKET_URI, client = PublicClient ) { super(productIDs, websocketURI); - this._apiURI = apiURI; this._client = client; this._queues = {}; diff --git a/tests/orderbook_sync.spec.js b/tests/orderbook_sync.spec.js index 4f19dcf9..f52b264b 100644 --- a/tests/orderbook_sync.spec.js +++ b/tests/orderbook_sync.spec.js @@ -25,7 +25,6 @@ suite('OrderbookSync', () => { const server = testserver(++port, () => { const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', - GDAX_API_URI, 'ws://localhost:' + port ); orderbookSync.on('message', data => { @@ -63,7 +62,6 @@ suite('OrderbookSync', () => { ); const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', - GDAX_API_URI, 'ws://localhost:' + port, authClient ); @@ -90,7 +88,6 @@ suite('OrderbookSync', () => { const server = testserver(++port, () => { const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', - GDAX_API_URI, 'ws://localhost:' + port ); @@ -126,7 +123,6 @@ suite('OrderbookSync', () => { ); const orderbookSync = new Gdax.OrderbookSync( 'BTC-USD', - GDAX_API_URI, 'ws://localhost:' + port, authClient ); @@ -165,7 +161,6 @@ suite('OrderbookSync', () => { const server = testserver(++port, () => { const orderbookSync = new Gdax.OrderbookSync( ['BTC-USD', 'ETH-USD'], - GDAX_API_URI, 'ws://localhost:' + port ); const btc_usd_state = orderbookSync.books['BTC-USD'].state(); From 64c81996842d8d4cc90266f33b6f73c999e3f893 Mon Sep 17 00:00:00 2001 From: Nick Freeman Date: Fri, 4 Aug 2017 06:13:55 -0500 Subject: [PATCH 9/9] Add ability to specify falsy values to disable `rateLimit` and bypass adding throttling plugin at all, e.g. `rateLimit: false`. This is potentially more intuitive than setting `rateLimit: Infinity` (which still works though). --- lib/clients/public.js | 16 +++++++++------- tests/authenticated.spec.js | 2 +- tests/orderbook_sync.spec.js | 4 ++-- tests/public_client.spec.js | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/clients/public.js b/lib/clients/public.js index a47839c9..37853b4c 100644 --- a/lib/clients/public.js +++ b/lib/clients/public.js @@ -25,13 +25,15 @@ class PublicClient { ) { this._productID = productID; this._API_LIMIT = 100; - - this._agent = agentUse(superagent).use(agentPrefix(apiURI)).use( - new Throttle({ - rate: rateLimit, - ratePer: 1000, - }).plugin() - ); + this._agent = agentUse(superagent).use(agentPrefix(apiURI)); + if (rateLimit) { + this._agent.use( + new Throttle({ + rate: rateLimit, + ratePer: 1000, + }).plugin() + ); + } } static _get(opts) { diff --git a/tests/authenticated.spec.js b/tests/authenticated.spec.js index 52e09366..3ff3eebf 100644 --- a/tests/authenticated.spec.js +++ b/tests/authenticated.spec.js @@ -16,7 +16,7 @@ const authClient = new Gdax.AuthenticatedClient( undefined, undefined, { - rateLimit: Infinity, + rateLimit: false, } ); diff --git a/tests/orderbook_sync.spec.js b/tests/orderbook_sync.spec.js index f52b264b..ab378a8b 100644 --- a/tests/orderbook_sync.spec.js +++ b/tests/orderbook_sync.spec.js @@ -57,7 +57,7 @@ suite('OrderbookSync', () => { b64secret, passphrase, { - rateLimit: Infinity, + rateLimit: false, } ); const orderbookSync = new Gdax.OrderbookSync( @@ -118,7 +118,7 @@ suite('OrderbookSync', () => { undefined, undefined, { - rateLimit: Infinity, + rateLimit: false, } ); const orderbookSync = new Gdax.OrderbookSync( diff --git a/tests/public_client.spec.js b/tests/public_client.spec.js index 5bb6a7f3..d22ea5ca 100644 --- a/tests/public_client.spec.js +++ b/tests/public_client.spec.js @@ -3,7 +3,7 @@ const nock = require('nock'); const Gdax = require('../index.js'); const publicClient = new Gdax.PublicClient(undefined, undefined, { - rateLimit: Infinity, + rateLimit: false, }); const { GDAX_API_URI } = require('../lib/constants');