ccxt-mxc
Version:
A JavaScript / Python / PHP cryptocurrency trading library with support for 130+ exchanges
792 lines (764 loc) • 30.9 kB
JavaScript
'use strict';
// ----------------------------------------------------------------------------
const Exchange = require ('./base/Exchange');
const { InvalidNonce, AuthenticationError, AccountSuspended, InsufficientFunds, ExchangeError, ArgumentsRequired, NotSupported } = require ('./base/errors');
// ----------------------------------------------------------------------------
module.exports = class stronghold extends Exchange {
describe () {
return this.deepExtend (super.describe (), {
'id': 'stronghold',
'name': 'Stronghold',
'country': [ 'US' ],
'rateLimit': 1000,
'version': 'v1',
'comment': 'This comment is optional',
'urls': {
'logo': 'https://user-images.githubusercontent.com/1294454/52160042-98c1f300-26be-11e9-90dd-da8473944c83.jpg',
'api': {
'public': 'https://api.stronghold.co',
'private': 'https://api.stronghold.co',
},
'www': 'https://stronghold.co',
'doc': [
'https://docs.stronghold.co',
],
},
'requiredCredentials': {
'apiKey': true,
'secret': true,
'password': true,
},
'has': {
'fetchMarkets': true,
'fetchCurrencies': true,
'fetchOrderBook': true,
'fetchOpenOrders': true,
'fetchTime': true,
'fetchTrades': true,
'fetchMyTrades': true,
'fetchDepositAddress': false,
'createDepositAddress': true,
'withdraw': true,
'fetchTicker': false,
'fetchTickers': false,
'fetchAccounts': true,
'fetchTransactions': true,
},
'api': {
'public': {
'get': [
'utilities/time',
'utilities/uuid',
'venues',
'venues/{venueId}/assets',
'venues/{venueId}/markets',
'venues/{venueId}/markets/{marketId}/orderbook',
'venues/{venueId}/markets/{marketId}/trades',
],
'post': [
'venues/{venueId}/assets',
'iam/credentials',
'identities',
],
'patch': [
'identities',
],
'put': [
'iam/credentials/{credentialId}',
],
'delete': [
'iam/credentials/{credentialId}',
],
},
'private': {
'get': [
'venues',
'venues/{venueId}/accounts',
'venues/{venueId}/accounts/{accountId}',
'venues/{venueId}/accounts/{accountId}/payments/{paymentId}',
'venues/{venueId}/accounts/{accountId}/orders',
'venues/{venueId}/accounts/{accountId}/trades',
'venues/{venueId}/accounts/{accountId}/transactions',
],
'post': [
'venues/{venueId}/accounts',
'venues/{venueId}/accounts/{accountId}/orders',
'venues/{venueId}/accounts/{accountId}/deposit',
'venues/{venueId}/accounts/{accountId}/withdrawal',
'venues/{venueId}/accounts/{accountId}/payments',
'venues/{venueId}/accounts/{accountId}/payments/{paymentId}/stop',
'venues/{venueId}/custody/accounts/{accountId}/operations/{operationId}/signatures',
'venues/{venueId}/anchor/withdrawal',
'venues/{venueId}/testing/friendbot',
],
'delete': [
'venues/{venueId}/accounts/{accountId}/orders/{orderId}',
],
},
},
'options': {
'accountId': undefined,
'venueId': 'trade-public',
'venues': {
'trade': 'trade-public',
'sandbox': 'sandbox-public',
},
'paymentMethods': {
'ETH': 'ethereum',
'BTC': 'bitcoin',
'XLM': 'stellar',
'XRP': 'ripple',
'LTC': 'litecoin',
'SHX': 'stellar',
},
},
'exceptions': {
'CREDENTIAL_MISSING': AuthenticationError,
'CREDENTIAL_INVALID': AuthenticationError,
'CREDENTIAL_REVOKED': AccountSuspended,
'CREDENTIAL_NO_IDENTITY': AuthenticationError,
'PASSPHRASE_INVALID': AuthenticationError,
'SIGNATURE_INVALID': AuthenticationError,
'TIME_INVALID': InvalidNonce,
'BYPASS_INVALID': AuthenticationError,
'INSUFFICIENT_FUNDS': InsufficientFunds,
},
});
}
async getActiveAccount () {
if (this.options['accountId'] !== undefined) {
return this.options['accountId'];
}
await this.loadAccounts ();
const numAccounts = this.accounts.length;
if (numAccounts > 0) {
return this.accounts[0]['id'];
}
throw new ExchangeError (this.id + ' requires an accountId.');
}
async fetchAccounts (params = {}) {
const request = {
'venueId': this.options['venueId'],
};
const response = await this.privateGetVenuesVenueIdAccounts (this.extend (request, params));
//
// [ { id: '34080200-b25a-483d-a734-255d30ba324d',
// venueSpecificId: '' } ... ]
//
return response['result'];
}
async fetchTime (params = {}) {
const response = await this.publicGetUtilitiesTime (params);
//
// {
// "requestId": "6de8f506-ad9d-4d0d-94f3-ec4d55dfcdb9",
// "timestamp": 1536436649207281,
// "success": true,
// "statusCode": 200,
// "result": {
// "timestamp": "2018-09-08T19:57:29.207282Z"
// }
// }
//
return this.parse8601 (this.safeString (response['result'], 'timestamp'));
}
async fetchMarkets (params = {}) {
const request = {
'venueId': this.options['venueId'],
};
const response = await this.publicGetVenuesVenueIdMarkets (this.extend (request, params));
const data = response['result'];
//
// [
// {
// id: 'SHXUSD',
// baseAssetId: 'SHX/stronghold.co',
// counterAssetId: 'USD/stronghold.co',
// minimumOrderSize: '1.0000000',
// minimumOrderIncrement: '1.0000000',
// minimumPriceIncrement: '0.00010000',
// displayDecimalsPrice: 4,
// displayDecimalsAmount: 0
// },
// ...
// ]
//
const result = {};
for (let i = 0; i < data.length; i++) {
const entry = data[i];
const marketId = entry['id'];
const baseId = this.safeString (entry, 'baseAssetId');
const quoteId = this.safeString (entry, 'counterAssetId');
const baseAssetId = baseId.split ('/')[0];
const quoteAssetId = quoteId.split ('/')[0];
const base = this.safeCurrencyCode (baseAssetId);
const quote = this.safeCurrencyCode (quoteAssetId);
const symbol = base + '/' + quote;
const limits = {
'amount': {
'min': this.safeFloat (entry, 'minimumOrderSize'),
'max': undefined,
},
};
const precision = {
'price': this.safeInteger (entry, 'displayDecimalsPrice'),
'amount': this.safeInteger (entry, 'displayDecimalsAmount'),
};
result[symbol] = {
'symbol': symbol,
'id': marketId,
'base': base,
'quote': quote,
'baseId': baseId,
'quoteId': quoteId,
'precision': precision,
'info': entry,
'limits': limits,
'active': undefined,
};
}
return result;
}
async fetchCurrencies (params = {}) {
const request = {
'venueId': this.options['venueId'],
};
const response = await this.publicGetVenuesVenueIdAssets (this.extend (request, params));
//
// [
// {
// id: 'XLM/native',
// alias: '',
// code: 'XLM',
// name: '',
// displayDecimalsFull: 7,
// displayDecimalsSignificant: 2,
// },
// ...
// ]
//
const data = response['result'];
const result = {};
const limits = {
'amount': {
'min': undefined,
'max': undefined,
},
'price': {
'min': undefined,
'max': undefined,
},
'cost': {
'min': undefined,
'max': undefined,
},
'withdraw': {
'min': undefined,
'max': undefined,
},
};
for (let i = 0; i < data.length; i++) {
const entry = data[i];
const assetId = this.safeString (entry, 'id');
const currencyId = this.safeString (entry, 'code');
const code = this.safeCurrencyCode (currencyId);
const precision = this.safeInteger (entry, 'displayDecimalsFull');
result[code] = {
'code': code,
'id': assetId,
'precision': precision,
'info': entry,
'active': undefined,
'name': undefined,
'limits': limits,
'fee': undefined,
};
}
return result;
}
async fetchOrderBook (symbol, limit = undefined, params = {}) {
await this.loadMarkets ();
const marketId = this.marketId (symbol);
const request = {
'marketId': marketId,
'venueId': this.options['venueId'],
};
const response = await this.publicGetVenuesVenueIdMarketsMarketIdOrderbook (this.extend (request, params));
//
// {
// marketId: 'ETHBTC',
// bids: [
// [ '0.031500', '7.385000' ],
// ...,
// ],
// asks: [
// [ '0.031500', '7.385000' ],
// ...,
// ],
// }
//
const data = response['result'];
const timestamp = this.parse8601 (this.safeString (response, 'timestamp'));
return this.parseOrderBook (data, timestamp);
}
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
await this.loadMarkets ();
const market = this.market (symbol);
const request = {
'marketId': market['id'],
'venueId': this.options['venueId'],
};
const response = await this.publicGetVenuesVenueIdMarketsMarketIdTrades (this.extend (request, params));
//
// {
// "requestId": "4d343700-b53f-4975-afcc-732ae9d3c828",
// "timestamp": "2018-11-08T19:22:11.399543Z",
// "success": true,
// "statusCode": 200,
// "result": {
// "marketId": "",
// "trades": [
// [ "0.9", "3.10", "sell", "2018-11-08T19:22:11.399547Z" ],
// ...
// ],
// }
// }
//
return this.parseTrades (response['result']['trades'], market, since, limit);
}
parseTrade (trade, market = undefined) {
//
// fetchTrades (public)
//
// [ '0.03177000', '0.0643501', 'sell', '2019-01-27T23:02:04Z' ]
//
// fetchMyTrades (private)
//
// {
// id: '9cdb109c-d035-47e2-81f8-a0c802c9c5f9',
// orderId: 'a38d8bcb-9ff5-4c52-81a0-a40196a66462',
// marketId: 'XLMUSD',
// side: 'sell',
// size: '1.0000000',
// price: '0.10440600',
// settled: true,
// maker: false,
// executedAt: '2019-02-01T18:44:21Z'
// }
//
let id = undefined;
let takerOrMaker = undefined;
let price = undefined;
let amount = undefined;
let cost = undefined;
let side = undefined;
let timestamp = undefined;
let orderId = undefined;
if (Array.isArray (trade)) {
price = parseFloat (trade[0]);
amount = parseFloat (trade[1]);
side = trade[2];
timestamp = this.parse8601 (trade[3]);
} else {
id = this.safeString (trade, 'id');
price = this.safeFloat (trade, 'price');
amount = this.safeFloat (trade, 'size');
side = this.safeString (trade, 'side');
timestamp = this.parse8601 (this.safeString (trade, 'executedAt'));
orderId = this.safeString (trade, 'orderId');
const marketId = this.safeString (trade, 'marketId');
market = this.safeValue (this.markets_by_id, marketId);
const isMaker = this.safeValue (trade, 'maker');
takerOrMaker = isMaker ? 'maker' : 'taker';
}
if (amount !== undefined && price !== undefined) {
cost = amount * price;
}
let symbol = undefined;
if (market !== undefined) {
symbol = market['symbol'];
}
return {
'id': id,
'info': trade,
'timestamp': timestamp,
'datetime': this.iso8601 (timestamp),
'symbol': symbol,
'type': undefined,
'order': orderId,
'side': side,
'price': price,
'amount': amount,
'cost': cost,
'takerOrMaker': takerOrMaker,
'fee': {
'cost': undefined,
'currency': undefined,
'rate': undefined,
},
};
}
async fetchTransactions (code = undefined, since = undefined, limit = undefined, params = {}) {
await this.loadMarkets ();
const request = this.extend ({
'venueId': this.options['venueId'],
'accountId': await this.getActiveAccount (),
}, params);
if (!request['accountId']) {
throw new ArgumentsRequired (this.id + " fetchTransactions requires either the 'accountId' extra parameter or exchange.options['accountId'] = 'YOUR_ACCOUNT_ID'.");
}
const response = await this.privateGetVenuesVenueIdAccountsAccountIdTransactions (request);
let currency = undefined;
if (code !== undefined) {
currency = this.currency (code);
}
return this.parseTransactions (response['result'], currency, since, limit);
}
parseTransactionStatus (status) {
const statuses = {
'queued': 'pending',
'settling': 'pending',
};
return this.safeString (statuses, status, status);
}
parseTransaction (transaction, currency = undefined) {
// {
// "id": "6408e003-0f14-4457-9340-ba608992ad5c",
// "status": "queued",
// "direction": "outgoing",
// "amount": "98.95000000",
// "assetId": "XLM/native",
// "sourceAccount": {
// "id": "774fa8ef-600b-4636-b9ed-cd6d23421915",
// "venueSpecificId": "GC5FIBIQZTQRMJE34GYF5EKH77GEQ3OHFX3NIP5OKDIZFA6VERLZSHY6"
// },
// "destinationAccount": {
// "id": "f72b9fb5-9607-4dd3-b31f-6ded21337056",
// "venueSpecificId": "GAOWV6CYBE7DEWSWPODXLMI5YB75VXXZJX5OYVQ2YLZH2TVA3TMMSNYW"
// }
// }
const id = this.safeString (transaction, 'id');
const assetId = this.safeString (transaction, 'assetId');
let code = undefined;
if (assetId !== undefined) {
const currencyId = assetId.split ('/')[0];
code = this.safeCurrencyCode (currencyId);
} else {
if (currency !== undefined) {
code = currency['code'];
}
}
const amount = this.safeFloat (transaction, 'amount');
const status = this.parseTransactionStatus (this.safeString (transaction, 'status'));
const feeCost = this.safeFloat (transaction, 'feeAmount');
let feeRate = undefined;
if (feeCost !== undefined) {
feeRate = feeCost / amount;
}
const direction = this.safeString (transaction, 'direction');
const datetime = this.safeString (transaction, 'requestedAt');
const timestamp = this.parse8601 (datetime);
const updated = this.parse8601 (this.safeString (transaction, 'updatedAt'));
const type = (direction === 'outgoing' || direction === 'withdrawal') ? 'withdrawal' : 'deposit';
const fee = {
'cost': feeCost,
'rate': feeRate,
};
return {
'id': id,
'info': transaction,
'currency': code,
'amount': amount,
'status': status,
'fee': fee,
'tag': undefined,
'type': type,
'updated': updated,
'address': undefined,
'txid': undefined,
'timestamp': timestamp,
'datetime': datetime,
};
}
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
await this.loadMarkets ();
const market = this.market (symbol);
const request = this.extend ({
'venueId': this.options['venueId'],
'accountId': await this.getActiveAccount (),
'marketID': market['id'],
'type': type,
'side': side,
'size': this.amountToPrecision (symbol, amount),
'price': this.priceToPrecision (symbol, price),
}, params);
if (!request['accountId']) {
throw new ArgumentsRequired (this.id + " createOrder requires either the 'accountId' extra parameter or exchange.options['accountId'] = 'YOUR_ACCOUNT_ID'.");
}
const response = await this.privatePostVenuesVenueIdAccountsAccountIdOrders (request);
return this.parseOrder (response, market);
}
async cancelOrder (id, symbol = undefined, params = {}) {
const request = this.extend ({
'venueId': this.options['venueId'],
'accountId': await this.getActiveAccount (),
'orderId': id,
}, params);
if (!request['accountId']) {
throw new ArgumentsRequired (this.id + " cancelOrder requires either the 'accountId' extra parameter or exchange.options['accountId'] = 'YOUR_ACCOUNT_ID'.");
}
const response = await this.privateDeleteVenuesVenueIdAccountsAccountIdOrdersOrderId (request);
return this.parseOrder (response);
}
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
await this.loadMarkets ();
let market = undefined;
if (symbol !== undefined) {
market = this.market (symbol);
}
const request = this.extend ({
'venueId': this.options['venueId'],
'accountId': await this.getActiveAccount (),
}, params);
if (!request['accountId']) {
throw new ArgumentsRequired (this.id + " cancelOrder requires either the 'accountId' extra parameter or exchange.options['accountId'] = 'YOUR_ACCOUNT_ID'.");
}
const response = await this.privateGetVenuesVenueIdAccountsAccountIdOrders (request);
return this.parseOrders (response['result'], market, since, limit);
}
parseOrder (order, market = undefined) {
// { id: '178596',
// marketId: 'XLMUSD',
// side: 'buy',
// size: '1.0000000',
// sizeFilled: '0',
// price: '0.10000000',
// placedAt: '2019-02-01T19:47:52Z' }
const marketId = this.safeString (order, 'marketId');
if (marketId !== undefined) {
market = this.safeValue (this.marketsById, marketId);
}
let symbol = undefined;
if (market !== undefined) {
symbol = market['symbol'];
}
const id = this.safeString (order, 'id');
const datetime = this.safeString (order, 'placedAt');
const amount = this.safeFloat (order, 'size');
const price = this.safeFloat (order, 'price');
const filled = this.safeFloat (order, 'sizeFilled');
let cost = undefined;
let remaining = undefined;
if (amount !== undefined) {
if (filled !== undefined) {
remaining = amount - filled;
}
if (price !== undefined) {
cost = amount * price;
}
}
return {
'id': id,
'clientOrderId': undefined,
'info': order,
'symbol': symbol,
'datetime': datetime,
'timestamp': this.parse8601 (datetime),
'side': this.safeString (order, 'side'),
'amount': amount,
'filled': filled,
'remaining': remaining,
'price': price,
'cost': cost,
'trades': [],
'lastTradeTimestamp': undefined,
'status': undefined,
'type': undefined,
'average': undefined,
'fee': undefined,
};
}
nonce () {
return this.seconds ();
}
setSandboxMode (enabled) {
if (enabled) {
this.options['venueId'] = this.options['venues']['sandbox'];
} else {
this.options['venueId'] = this.options['venues']['trade'];
}
}
async fetchBalance (params = {}) {
const request = this.extend ({
'venueId': this.options['venueId'],
'accountId': await this.getActiveAccount (),
}, params);
if (!('accountId' in request)) {
throw new ArgumentsRequired (this.id + " fetchBalance requires either the 'accountId' extra parameter or exchange.options['accountId'] = 'YOUR_ACCOUNT_ID'.");
}
const response = await this.privateGetVenuesVenueIdAccountsAccountId (request);
const balances = this.safeValue (response['result'], 'balances');
const result = { 'info': response };
for (let i = 0; i < balances.length; i++) {
const balance = balances[i];
const assetId = this.safeString (balance, 'assetId');
if (assetId !== undefined) {
const currencyId = assetId.split ('/')[0];
const code = this.safeCurrencyCode (currencyId);
const account = {};
account['total'] = this.safeFloat (balance, 'amount');
account['free'] = this.safeFloat (balance, 'availableForTrade');
result[code] = account;
}
}
return this.parseBalance (result);
}
async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
await this.loadMarkets ();
const request = this.extend ({
'venueId': this.options['venueId'],
'accountId': await this.getActiveAccount (),
}, params);
if (!request['accountId']) {
throw new ArgumentsRequired (this.id + " fetchMyTrades requires either the 'accountId' extra parameter or exchange.options['accountId'] = 'YOUR_ACCOUNT_ID'.");
}
const response = await this.privateGetVenuesVenueIdAccountsAccountIdTrades (request);
let market = undefined;
if (symbol !== undefined) {
market = this.market (symbol);
}
return this.parseTrades (response['result'], market, since, limit);
}
async createDepositAddress (code, params = {}) {
await this.loadMarkets ();
const paymentMethod = this.safeString (this.options['paymentMethods'], code);
if (paymentMethod === undefined) {
throw new NotSupported (this.id + ' createDepositAddress requires code to be BTC, ETH, or XLM');
}
const request = this.extend ({
'venueId': this.options['venueId'],
'accountId': await this.getActiveAccount (),
'assetId': this.currencyId (code),
'paymentMethod': paymentMethod,
}, params);
if (!request['accountId']) {
throw new ArgumentsRequired (this.id + " createDepositAddress requires either the 'accountId' extra parameter or exchange.options['accountId'] = 'YOUR_ACCOUNT_ID'.");
}
const response = await this.privatePostVenuesVenueIdAccountsAccountIdDeposit (request);
//
// {
// assetId: 'BTC/stronghold.co',
// paymentMethod: 'bitcoin',
// paymentMethodInstructions: {
// deposit_address: 'mzMT9Cfw8JXVWK7rMonrpGfY9tt57ytHt4',
// reference: 'sometimes-exists',
// },
// direction: 'deposit',
// }
//
const data = response['result']['paymentMethodInstructions'];
const address = data['deposit_address'];
const tag = this.safeString (data, 'reference');
return {
'currency': code,
'address': this.checkAddress (address),
'tag': tag,
'info': response,
};
}
async withdraw (code, amount, address, tag = undefined, params = {}) {
await this.loadMarkets ();
const paymentMethod = this.safeString (this.options['paymentMethods'], code);
if (paymentMethod === undefined) {
throw new NotSupported (this.id + ' withdraw requires code to be BTC, ETH, or XLM');
}
const request = this.extend ({
'venueId': this.options['venueId'],
'accountId': await this.getActiveAccount (),
'assetId': this.currencyId (code),
'amount': amount,
'paymentMethod': paymentMethod,
'paymentMethodDetails': {
'withdrawal_address': address,
},
}, params);
if (tag !== undefined) {
request['paymentMethodDetails']['reference'] = tag;
}
if (!request['accountId']) {
throw new ArgumentsRequired (this.id + " withdraw requires either the 'accountId' extra parameter or exchange.options['accountId'] = 'YOUR_ACCOUNT_ID'.");
}
const response = await this.privatePostVenuesVenueIdAccountsAccountIdWithdrawal (request);
//
// {
// "id": "5be48892-1b6e-4431-a3cf-34b38811e82c",
// "assetId": "BTC/stronghold.co",
// "amount": "10",
// "feeAmount": "0.01",
// "paymentMethod": "bitcoin",
// "paymentMethodDetails": {
// "withdrawal_address": "1vHysJeXYV6nqhroBaGi52QWFarbJ1dmQ"
// },
// "direction": "withdrawal",
// "status": "pending"
// }
//
const data = response['result'];
return {
'id': this.safeString (data, 'id'),
'info': response,
};
}
handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
if (!response) {
return; // fallback to base error handler by default
}
//
// {
// requestId: '3e7d17ab-b316-4721-b5aa-f7e6497eeab9',
// timestamp: '2019-01-31T21:59:06.696855Z',
// success: true,
// statusCode: 200,
// result: []
// }
//
const errorCode = this.safeString (response, 'errorCode');
if (errorCode in this.exceptions) {
const Exception = this.exceptions[errorCode];
throw new Exception (this.id + ' ' + body);
}
const success = this.safeValue (response, 'success');
if (!success) {
throw new ExchangeError (this.id + ' ' + body);
}
}
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
const request = '/' + this.version + '/' + this.implodeParams (path, params);
const query = this.omit (params, this.extractParams (path));
let url = this.urls['api'][api] + request;
if (Object.keys (query).length) {
if (method === 'GET') {
url += '?' + this.urlencode (query);
} else {
body = this.json (query);
}
}
if (api === 'private') {
this.checkRequiredCredentials ();
const timestamp = this.nonce ().toString ();
let payload = timestamp + method + request;
if (body !== undefined) {
payload += body;
}
const secret = this.base64ToBinary (this.secret);
headers = {
'SH-CRED-ID': this.apiKey,
'SH-CRED-SIG': this.hmac (this.encode (payload), secret, 'sha256', 'base64'),
'SH-CRED-TIME': timestamp,
'SH-CRED-PASS': this.password,
'Content-Type': 'application/json',
};
}
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
}
};