From f17411a3654b86626e411bd68392003fd01e2ffd Mon Sep 17 00:00:00 2001 From: "Buddy Lindsey, Jr" Date: Thu, 31 May 2018 13:03:24 -0500 Subject: [PATCH 1/7] Add coinbase exchange with minimal feature set as a start --- ccxt.js | 1 + js/coinbase.js | 206 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 js/coinbase.js diff --git a/ccxt.js b/ccxt.js index 6f723e9eeeab..a56f168a446f 100644 --- a/ccxt.js +++ b/ccxt.js @@ -84,6 +84,7 @@ const exchanges = { 'chbtc': require ('./js/chbtc.js'), 'chilebit': require ('./js/chilebit.js'), 'cobinhood': require ('./js/cobinhood.js'), + 'coinbase': require ('./js/coinbase.js'), 'coincheck': require ('./js/coincheck.js'), 'coinegg': require ('./js/coinegg.js'), 'coinex': require ('./js/coinex.js'), diff --git a/js/coinbase.js b/js/coinbase.js new file mode 100644 index 000000000000..4788a64785f2 --- /dev/null +++ b/js/coinbase.js @@ -0,0 +1,206 @@ +'use strict'; + +// ---------------------------------------------------------------------------- + +const Exchange = require ('./base/Exchange'); +const { ExchangeError, AuthenticationError } = require ('./base/errors'); + +// ---------------------------------------------------------------------------- + +module.exports = class coinbase extends Exchange { + describe () { + return this.deepExtend (super.describe (), { + 'id': 'coinbase', + 'name': 'coinbase', + 'countries': 'US', + 'rateLimit': 1000, + 'userAgent': this.userAgents['chrome'], + 'has': { + 'CORS': true, + 'cancelOrder': false, + 'createDepositAddress': false, + 'createOrder': false, + 'deposit': false, + 'fetchBalance': true, + 'fetchClosedOrders': false, + 'fetchCurrencies': false, + 'fetchDepositAddress': false, + 'fetchMarkets': false, + 'fetchMyTrades': false, + 'fetchOHLCV': false, + 'fetchOpenOrders': false, + 'fetchOrder': false, + 'fetchOrderBook': false, + 'fetchOrders': false, + 'fetchTicker': false, + 'fetchTickers': false, + 'fetchBidsAsks': false, + 'fetchTrades': false, + 'withdraw': false, + 'fetchTransactions': false, + 'fetchDeposits': false, + 'fetchWithdrawals': false, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766527-b1be41c6-5edb-11e7-95f6-5b496c469e2c.jpg', + 'api': 'https://api.coinbase.com/v2', + 'www': 'https://www.coinbase.com', + 'doc': 'https://developers.coinbase.com/api/v2', + 'fees': [ + 'https://support.coinbase.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + ], + }, + 'requiredCredentials': { + 'apiKey': true, + 'secret': true, + }, + 'version': 'v2', + 'api': { + 'public': { + 'get': [ + 'currencies', + 'time', + 'exchange-rates', + 'users/{userId}', + 'prices/{symbol}/buy', + 'prices/{symbol}/sell', + 'prices/{symbol}/spot', + ], + }, + 'private': { + 'get': [ + 'accounts', + 'accounts/{accountId}', + 'accounts/{accountId}/addresses', + 'accounts/{accountId}/addresses/{address_id}', + 'accounts/{accountId}/addresses/{address_id}/transactions', + 'accounts/{accountId}/transactions', + 'accounts/{accountId}/transactions/{transaction_id}', + 'accounts/{accountId}/buys', + 'accounts/{accountId}/buys/{buyId}', + 'accounts/{accountId}/sells', + 'accounts/{accountId}/sells/{sellId}', + 'accounts/{accountId}/deposits', + 'accounts/{accountId}/deposits/{depositId}', + 'accounts/{accountId}/withdrawals', + 'accounts/{accountId}/withdrawals/{withdrawalId}', + 'payment-methods', + 'payment-methods/{methodId}', + 'user', + 'user/auth', + ], + 'post': [ + 'accounts', + 'accounts/{accountId}/primary', + 'accounts/{accountId}/addresses', + 'accounts/{accountId}/transactions', + 'accounts/{accountId}/transactions/{transactionId}/complete', + 'accounts/{accountId}/transactions/{transactionId}/resend', + 'accounts/{accountId}/buys', + 'accounts/{accountId}/buys/{buyId}/commit', + 'accounts/{accountId}/sells', + 'accounts/{accountId}/sells/{sellId}/commit', + 'accounts/{accountId}/deposists', + 'accounts/{accountId}/deposists/{depositId}/commit', + 'accounts/{accountId}/withdrawals', + 'accounts/{accountId}/withdrawals/{withdrawalId}/commit', + ], + 'put': [ + 'accounts/{accountId}', + 'user', + ], + 'delete': [ + 'accounts/{id}', + 'accounts/{accountId}/transactions/{transactionId}', + ], + }, + }, + 'markets': { + 'BTC/USD': { 'id': 'btc-usd', 'symbol': 'BTC/USD', 'base': 'BTC', 'quote': 'USD' }, + 'LTC/USD': { 'id': 'ltc-usd', 'symbol': 'LTC/USD', 'base': 'LTC', 'quote': 'USD' }, + 'ETH/USD': { 'id': 'eth-usd', 'symbol': 'ETH/USD', 'base': 'ETH', 'quote': 'USD' }, + 'BCH/USD': { 'id': 'bch-usd', 'symbol': 'BCH/USD', 'base': 'BCH', 'quote': 'USD' }, + }, + }); + } + + async fetchTime () { + let response = await this.publicGetTime (); + return this.parse8601 (response['data']['iso']); + } + + async fetchBalance (params = {}) { + let balances = await this.privateGetAccounts (); + let result = { 'info': balances }; + for (let b = 0; b < balances.data.length; b++) { + let balance = balances.data[b]; + let currency = balance['balance']['currency']; + let account = { + 'free': this.safeFloat (balance['balance'], 'amount'), + 'used': null, + 'total': this.safeFloat (balance['balance'], 'amount'), + }; + result[currency] = account; + } + return this.parseBalance (result); + } + + sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) { + let request = '/' + this.implodeParams (path, params); + let query = this.omit (params, this.extractParams (path)); + if (method === 'GET') { + if (Object.keys (query).length) + request += '?' + this.urlencode (query); + } + let url = this.urls['api'] + request; + if (api === 'private') { + this.checkRequiredCredentials (); + let nonce = this.nonce ().toString (); + let payload = ''; + if (method !== 'GET') { + if (Object.keys (query).length) { + body = this.json (query); + payload = body; + } + } + let what = nonce + method + '/v2' + request + payload; + let signature = this.hmac (this.encode (what), this.secret); + headers = { + 'CB-ACCESS-KEY': this.apiKey, + 'CB-ACCESS-SIGN': this.decode (signature), + 'CB-ACCESS-TIMESTAMP': nonce, + 'CB-VERSION': '2018-05-30', + 'Content-Type': 'application/json', + }; + } + if (!headers) { + headers = { + 'CB-VERSION': '2018-05-30', + }; + } + return { 'url': url, 'method': method, 'body': body, 'headers': headers }; + } + + handleErrors (code, reason, url, method, headers, body) { + if ((code === 400) || (code === 404) || (code === 401)) { + if (body[0] === '{') { + let response = JSON.parse (body); + let message = response['errors'][0]['message']; + let error = this.id + ' ' + message; + if (message === 'invalid api key') { + throw new AuthenticationError (error); + } + throw new ExchangeError (this.id + ' ' + message); + } + throw new ExchangeError (this.id + ' ' + body); + } + } + + async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) { + let response = await this.fetch2 (path, api, method, params, headers, body); + if ('message' in response) { + throw new ExchangeError (this.id + ' ' + this.json (response)); + } + return response; + } +}; From 093c562352bcd5b31f5e0d1d2b7eea3d822b4ff0 Mon Sep 17 00:00:00 2001 From: Igor Kroitor Date: Fri, 1 Jun 2018 00:12:09 +0300 Subject: [PATCH 2/7] ccxt.js reverted back --- ccxt.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ccxt.js b/ccxt.js index a56f168a446f..6f723e9eeeab 100644 --- a/ccxt.js +++ b/ccxt.js @@ -84,7 +84,6 @@ const exchanges = { 'chbtc': require ('./js/chbtc.js'), 'chilebit': require ('./js/chilebit.js'), 'cobinhood': require ('./js/cobinhood.js'), - 'coinbase': require ('./js/coinbase.js'), 'coincheck': require ('./js/coincheck.js'), 'coinegg': require ('./js/coinegg.js'), 'coinex': require ('./js/coinex.js'), From 42b6d96b164dcb77e3a0e4d9ff8bb6b58316c2c2 Mon Sep 17 00:00:00 2001 From: Igor Kroitor Date: Fri, 1 Jun 2018 00:13:02 +0300 Subject: [PATCH 3/7] transpilation fix --- js/coinbase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/coinbase.js b/js/coinbase.js index 4788a64785f2..aac8c25e0a6c 100644 --- a/js/coinbase.js +++ b/js/coinbase.js @@ -137,7 +137,7 @@ module.exports = class coinbase extends Exchange { let currency = balance['balance']['currency']; let account = { 'free': this.safeFloat (balance['balance'], 'amount'), - 'used': null, + 'used': undefined, 'total': this.safeFloat (balance['balance'], 'amount'), }; result[currency] = account; From 8359c21b321e052c6eb258cd770b907139c206f6 Mon Sep 17 00:00:00 2001 From: Igor Kroitor Date: Fri, 1 Jun 2018 00:36:21 +0300 Subject: [PATCH 4/7] added unified exception codes for handleErrors --- js/coinbase.js | 82 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/js/coinbase.js b/js/coinbase.js index aac8c25e0a6c..0cad86f1fdcc 100644 --- a/js/coinbase.js +++ b/js/coinbase.js @@ -3,7 +3,7 @@ // ---------------------------------------------------------------------------- const Exchange = require ('./base/Exchange'); -const { ExchangeError, AuthenticationError } = require ('./base/errors'); +const { ExchangeError, AuthenticationError, DDoSProtection } = require ('./base/errors'); // ---------------------------------------------------------------------------- @@ -115,6 +115,25 @@ module.exports = class coinbase extends Exchange { ], }, }, + 'exceptions': { + 'two_factor_required': AuthenticationError, // 402 When sending money over 2fa limit + 'param_required': ExchangeError, // 400 Missing parameter + 'validation_error': ExchangeError, // 400 Unable to validate POST/PUT + 'invalid_request': ExchangeError, // 400 Invalid request + 'personal_details_required': AuthenticationError, // 400 User’s personal detail required to complete this request + 'identity_verification_required': AuthenticationError, // 400 Identity verification is required to complete this request + 'jumio_verification_required': AuthenticationError, // 400 Document verification is required to complete this request + 'jumio_face_match_verification_required': AuthenticationError, // 400 Document verification including face match is required to complete this request + 'unverified_email': AuthenticationError, // 400 User has not verified their email + 'authentication_error': AuthenticationError, // 401 Invalid auth (generic) + 'invalid_token': AuthenticationError, // 401 Invalid Oauth token + 'revoked_token': AuthenticationError, // 401 Revoked Oauth token + 'expired_token': AuthenticationError, // 401 Expired Oauth token + 'invalid_scope': AuthenticationError, // 403 User hasn’t authenticated necessary scope + 'not_found': ExchangeError, // 404 Resource not found + 'rate_limit_exceeded': DDoSProtection, // 429 Rate limit exceeded + 'internal_server_error': ExchangeError, // 500 Internal server error + }, 'markets': { 'BTC/USD': { 'id': 'btc-usd', 'symbol': 'BTC/USD', 'base': 'BTC', 'quote': 'USD' }, 'LTC/USD': { 'id': 'ltc-usd', 'symbol': 'LTC/USD', 'base': 'LTC', 'quote': 'USD' }, @@ -182,25 +201,52 @@ module.exports = class coinbase extends Exchange { } handleErrors (code, reason, url, method, headers, body) { - if ((code === 400) || (code === 404) || (code === 401)) { - if (body[0] === '{') { - let response = JSON.parse (body); - let message = response['errors'][0]['message']; - let error = this.id + ' ' + message; - if (message === 'invalid api key') { - throw new AuthenticationError (error); + if (typeof body !== 'string') + return; // fallback to default error handler + if (body.length < 2) + return; // fallback to default error handler + if ((body[0] === '{') || (body[0] === '[')) { + let response = JSON.parse (body); + let feedback = id + ' ' + body; + // + // {"error": "invalid_request", "error_description": "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."} + // + // or + // + // { + // "errors": [ + // { + // "id": "not_found", + // "message": "Not found" + // } + // ] + // } + // + let exceptions = this.exceptions; + let errorCode = this.safeString (response, 'error'); + if (typeof errorCode !== 'undefined') { + if (errorCode in exceptions) { + throw new exceptions[errorCode] (feedback); + } else { + throw new ExchangeError (feedback); + } + } + let errors = this.safeValue (response, 'errors'); + if (typeof errors !== 'undefined') { + if (Array.isArray (errors)) { + let numErrors = errors.length; + if (numErrors > 0) { + errorCode = this.safeString (errors[0], 'id'); + if (typeof errorCode !== 'undefined') { + if (errorCode in exceptions) { + throw new exceptions[errorCode] (feedback); + } else { + throw new ExchangeError (feedback); + } + } + } } - throw new ExchangeError (this.id + ' ' + message); } - throw new ExchangeError (this.id + ' ' + body); - } - } - - async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) { - let response = await this.fetch2 (path, api, method, params, headers, body); - if ('message' in response) { - throw new ExchangeError (this.id + ' ' + this.json (response)); } - return response; } }; From 95718ef34b5cc3e04bac8a299b0ef87087be366a Mon Sep 17 00:00:00 2001 From: Igor Kroitor Date: Fri, 1 Jun 2018 00:41:00 +0300 Subject: [PATCH 5/7] added v2 version constant --- js/coinbase.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/coinbase.js b/js/coinbase.js index 0cad86f1fdcc..46704eba1d23 100644 --- a/js/coinbase.js +++ b/js/coinbase.js @@ -14,6 +14,7 @@ module.exports = class coinbase extends Exchange { 'name': 'coinbase', 'countries': 'US', 'rateLimit': 1000, + 'version': 'v2', 'userAgent': this.userAgents['chrome'], 'has': { 'CORS': true, @@ -182,7 +183,7 @@ module.exports = class coinbase extends Exchange { payload = body; } } - let what = nonce + method + '/v2' + request + payload; + let what = nonce + method + '/' + this.version + request + payload; let signature = this.hmac (this.encode (what), this.secret); headers = { 'CB-ACCESS-KEY': this.apiKey, From 530c8fdedb20cf5e77d7506d21f0465d58ebf0fa Mon Sep 17 00:00:00 2001 From: Igor Kroitor Date: Fri, 1 Jun 2018 00:50:13 +0300 Subject: [PATCH 6/7] minor transpiler issues --- js/coinbase.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/js/coinbase.js b/js/coinbase.js index 46704eba1d23..b3add2f1822c 100644 --- a/js/coinbase.js +++ b/js/coinbase.js @@ -55,7 +55,6 @@ module.exports = class coinbase extends Exchange { 'apiKey': true, 'secret': true, }, - 'version': 'v2', 'api': { 'public': { 'get': [ @@ -119,7 +118,7 @@ module.exports = class coinbase extends Exchange { 'exceptions': { 'two_factor_required': AuthenticationError, // 402 When sending money over 2fa limit 'param_required': ExchangeError, // 400 Missing parameter - 'validation_error': ExchangeError, // 400 Unable to validate POST/PUT + 'validation_error': ExchangeError, // 400 Unable to validate POST/PUT 'invalid_request': ExchangeError, // 400 Invalid request 'personal_details_required': AuthenticationError, // 400 User’s personal detail required to complete this request 'identity_verification_required': AuthenticationError, // 400 Identity verification is required to complete this request @@ -208,7 +207,7 @@ module.exports = class coinbase extends Exchange { return; // fallback to default error handler if ((body[0] === '{') || (body[0] === '[')) { let response = JSON.parse (body); - let feedback = id + ' ' + body; + let feedback = this.id + ' ' + body; // // {"error": "invalid_request", "error_description": "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."} // From e361cda59a82acfdf4223223c69951952b0248c7 Mon Sep 17 00:00:00 2001 From: Igor Kroitor Date: Fri, 1 Jun 2018 00:54:43 +0300 Subject: [PATCH 7/7] tab cleanup --- js/coinbase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/coinbase.js b/js/coinbase.js index b3add2f1822c..67baed2b4092 100644 --- a/js/coinbase.js +++ b/js/coinbase.js @@ -124,7 +124,7 @@ module.exports = class coinbase extends Exchange { 'identity_verification_required': AuthenticationError, // 400 Identity verification is required to complete this request 'jumio_verification_required': AuthenticationError, // 400 Document verification is required to complete this request 'jumio_face_match_verification_required': AuthenticationError, // 400 Document verification including face match is required to complete this request - 'unverified_email': AuthenticationError, // 400 User has not verified their email + 'unverified_email': AuthenticationError, // 400 User has not verified their email 'authentication_error': AuthenticationError, // 401 Invalid auth (generic) 'invalid_token': AuthenticationError, // 401 Invalid Oauth token 'revoked_token': AuthenticationError, // 401 Revoked Oauth token