molestiaequaerat
Version:
Common API wrapper for multiple crypto currency exchanges
552 lines (520 loc) • 19.8 kB
JavaScript
;
const moment = require("moment");
const _ = require("lodash");
const async = require("async");
const BITSTAMP = require('bitstamp');
const util = require("./util"); //custom functions
function Bitstamp (options) {
const self = this;
let bitstampPublic, bitstampPrivate;
self["options"] = options;
bitstampPublic = new BITSTAMP();
if (typeof options["key"] === "string" && typeof options["secret"] === "string" && typeof options.username === "string") {
bitstampPrivate = new BITSTAMP(options.key, options.secret, options.username);
} else {
bitstampPrivate = bitstampPublic;
}
Object.getOwnPropertyNames(Object.getPrototypeOf(bitstampPrivate)).forEach(prop => {
if (typeof bitstampPrivate[prop] === 'function' && prop !== 'constructor') {
self[prop] = bitstampPrivate[prop];
}
});
let checkErr = function (err, result) { // arguments are the result from BITSTAMP module,
// returns the error (as Error object type) or null id no error
if (err instanceof Error)
return err;
if (typeof err === "string")
return new Error(err);
if (result && typeof result.error === "string")
return new Error(result.error);
};
let user_transactions = function (options, callback) { // same function as native API wrapper, but it returns result normalized according to JSON schema
bitstampPrivate.user_transactions('', options, function (err, xTransactions) {
let transactions, amount;
transactions = {
timestamp: util.timestampNow(),
error: err && err.message || "",
data: []
};
if (err) {
return callback(err, transactions);
}
// var jf = require("jsonfile"); jf.writeFileSync(__dirname + "/bitstamp-getFee_MockApiResponse.json", xTransactions); // only used to create MockApiResponse file for the test unit
xTransactions.forEach(function (element, index, array) {
transactions.data.push(
{
tx_id: "",
datetime: "",
type: "",
symbol: "",
amount_base: '0',
amount_counter: '0',
rate: '0',
fee_base: '0',
fee_counter: '0',
order_id: "",
add_info: ""
});
let tx = transactions.data[transactions.data.length - 1];
tx.tx_id = element.id.toString();
tx.datetime = moment(element.datetime + " Z").utc().format();
let btc = parseFloat(element.btc);
let usd = parseFloat(element.usd);
let eur = parseFloat(element.eur);
switch (element.type) {
case 0:
case '0': // deposit
tx.type = "deposit";
if (btc > 0) {
tx.symbol = "XBT";
tx.amount_base = element.btc;
}
if (usd > 0) {
tx.symbol = "USD";
tx.amount_base = element.usd;
}
if (eur > 0) {
tx.symbol = "EUR";
tx.amount_base = element.eur;
}
break;
case 1:
case '1': // withdrawal
tx.type = "withdrawal";
if (btc < 0) {
tx.symbol = "XBT";
tx.amount_base = element.btc;
}
if (usd < 0) {
tx.symbol = "USD";
tx.amount_base = element.usd;
}
if (eur < 0) {
tx.symbol = "EUR";
tx.amount_base = element.eur;
}
break;
case 2:
case '2': // market trade
tx.type = btc < 0 ? "sell" : "buy";
if (usd) {
tx.symbol = "XBT_USD";
tx.amount_counter = element.usd;
tx.rate = element.btc_usd.toString();
} else {
tx.symbol = "XBT_EUR";
tx.amount_counter = element.eur;
tx.rate = element.btc_eur.toString();
}
tx.amount_base = element.btc;
tx.order_id = element.order_id ? element.order_id.toString() : "";
break;
case 14:
case '14': // deposit
tx.type = "transfer";
break;
default:
tx.type = "unknown";
}
tx.fee_counter = element.fee;
});
callback(null, transactions);
});
};
self.getRate = function (options, callback) {
self.getTicker(options, function(err, result) {
let rate = {
timestamp: util.timestampNow(),
error: "",
data: []
};
if (err) {
rate.error = err.message;
return callback(err, rate);
}
rate.timestamp = result.timestamp;
let data = {
pair: result.data[0].pair,
rate: result.data[0].last
};
rate.data.push(data);
callback(err, rate);
});
};
function formatCurrencyPair(pair) {
if (typeof pair !== "string") {
return pair;
}
let currencies = pair.split('_');
return `${currencies[0].replace(/xbt/i, 'BTC').toUpperCase()}${currencies[1].replace(/xbt/i, 'BTC').toUpperCase()}`
}
self.getTicker = function (options, callback) {
// https://www.bitstamp.net/api/ticker/
let ticker, data;
let pair = formatCurrencyPair(options.pair);
bitstampPublic.ticker(pair, function(xErr, bitstampTicker) {
let err = checkErr(xErr, bitstampTicker);
ticker = {
timestamp: util.timestampNow(),
error: err && err.message || "",
data: []
};
if (err)
return callback(err, ticker);
// let jf = require("jsonfile"); jf.writeFileSync(__dirname + "/bitstamp-getFee_MockApiResponse.json", bitstampTicker, {spaces: 2}); // only used to create MockApiResponse file for the test unit
ticker.timestamp = util.timestamp(bitstampTicker.timestamp);
data = {
pair: options.pair.toUpperCase(),
last: bitstampTicker.last,
bid: bitstampTicker.bid,
ask: bitstampTicker.ask,
volume: bitstampTicker.volume,
high: bitstampTicker.high,
low: bitstampTicker.low,
vwap: bitstampTicker.vwap
};
ticker.data.push(data);
callback(null, ticker);
})
};
self.getOrderBook = function (options, callback) {
let pair = formatCurrencyPair(options.pair);
bitstampPublic.order_book(pair, function (xErr, bitstampOrderBook) {
// https://www.bitstamp.net/api/order_book/
let err = checkErr(xErr, bitstampOrderBook);
let orderBook = {
timestamp: util.timestampNow(),
error: err && err.message || "",
data: []
};
if (err) {
return callback(err, orderBook);
}
orderBook.timestamp = util.timestamp(bitstampOrderBook.timestamp);
let data = {
pair: options.pair.toUpperCase(),
asks: [],
bids: [],
};
orderBook.data.push(data);
let order;
bitstampOrderBook.asks.forEach(function (element, index, asks) {
order = {
price: asks[index][0],
volume: asks[index][1],
};
orderBook.data[0].asks.push(order);
});
bitstampOrderBook.bids.forEach(function (element, index, bids) {
order = {
price: bids[index][0],
volume: bids[index][1],
};
orderBook.data[0].bids.push(order);
});
callback(null, orderBook);
});
};
self.getTrades = function (options, callback) {
var trades;
var err = new Error("Method not implemented");
trades = {
timestamp: util.timestampNow(),
error: err.message,
data: []
};
callback(err, trades);
};
self.getFee = function (options, callback) {
bitstampPrivate.balance(null, function (xErr, bitstampBalance) {
let err = checkErr(xErr, bitstampBalance);
let pair = formatCurrencyPair(options.pair).toLocaleLowerCase();
if (!err && pair && !bitstampBalance.hasOwnProperty(`${pair}_fee`)) {
err = new Error(`Invalid pair ${pair}`)
}
let fee = {
timestamp: util.timestampNow(),
error: err && err.message || "",
data: []
};
if (err) {
return callback(err, fee);
}
// let jf = require("jsonfile"); jf.writeFileSync(__dirname + "/bitstamp-getFee_MockApiResponse.json", bitstampBalance, {spaces: 2}); // only used to create MockApiResponse file for the test unit
if (pair) {
let bitstampFee = bitstampBalance[`${pair}_fee`];
let data = {
pair: options.pair.toUpperCase(),
maker_fee: (parseFloat(bitstampFee) / 100).toString(), // note that Bitstamp native API call returns 0.2 for 0.2%
taker_fee: (parseFloat(bitstampFee) / 100).toString(),
};
fee.data.push(data);
} else {
for (let key in bitstampBalance) {
if (bitstampBalance.hasOwnProperty(key)) {
let s = key.split('_');
if (s[1] === 'fee') {
let data = {
pair: `${s[0].substr(0, 3)}_${s[0].substr(3, 3)}`.toUpperCase(),
maker_fee: (parseFloat(bitstampBalance[key]) / 100).toString(), // note that Bitstamp native API call returns 0.2 for 0.2%
taker_fee: (parseFloat(bitstampBalance[key]) / 100).toString(),
};
fee.data.push(data);
}
}
}
}
callback(null, fee);
});
};
self.getTransactions = function (optionsP, callback) {
var limit, skip, skippedCount, beforeMoment, afterMoment, txMoment, fetchMore, manualFilter;
var transactions = {
timestamp: util.timestampNow(),
error: "",
data: []
};
let options = _.clone(optionsP);
util.addDefaults("getTransactions", options);
// store for later manipulation of result data
limit = options.limit;
skip = options.hasOwnProperty("skip") && options.skip || 0;
beforeMoment = options.hasOwnProperty("before") && moment(options.before) || null;
afterMoment = options.hasOwnProperty("after") && moment(options.after) || null;
//set xOptions to call the function
var xOptions = {
offset: skip,
limit: options.limit,
sort: options.sort
};
if (options.hasOwnProperty("before") || options.hasOwnProperty("after") || options.hasOwnProperty("type")) {
// if we will aplly manual filter, don't skip anything and fetch maximum
xOptions.limit = self.properties.dictionary.getTransactions.limit.maximum;
xOptions.offset = 0;
manualFilter = true;
}
async.doWhilst(
function (cb) {
user_transactions(xOptions, function (err, result) {
if (err)
return cb(err);
fetchMore = !(xOptions.limit < self.properties.dictionary.getTransactions.limit.maximum); // if we fetched less than maximum we don't need to fetchMore for sure
fetchMore = fetchMore && !(result.data.length < xOptions.limit); // if fetched below the xOptions.limit, we can't fetch more (we fetched all)
// drop records before, after and skip
var drop, p = 0; // points to the current record to check;
while (p < result.data.length) {
drop = false;
txMoment = moment(result.data[p].datetime);
drop = beforeMoment && txMoment.isAfter(beforeMoment);
if (!drop && options.hasOwnProperty("type")) {
drop = (options.type === "movements") && (["buy", "sell"].indexOf(result.data[p].type) > -1);
drop = drop || (options.type === "trades") && (["deposit", "withdrawal"].indexOf(result.data[p].type) > -1);
}
if (!drop && afterMoment && txMoment.isBefore(afterMoment)) {
fetchMore = false;
drop = true;
}
if (!drop && options.hasOwnProperty("symbol")) {
let currency, found;
currency = options.symbol.substr(0,3);
found = result.data[p].symbol.indexOf(currency) > -1;
found = found || result.data[p].symbol.indexOf(currency.replace(/xbt/i, "BTC")) > -1;
currency = options.symbol.substr(3,3);
found = found || currency !== "" && (result.data[p].symbol.indexOf(currency) > -1);
found = found || currency !== "" && (result.data[p].symbol.indexOf(currency.replace(/xbt/i, "BTC")) > -1);
drop = !found;
}
if (drop)
result.data.splice(p, 1); // drop the record
else
p++; // or move pointer to next one
}
util.extendTransactions(transactions, result);
fetchMore = fetchMore && transactions.data.length < limit + skip;
cb(null);
});
},
function () {
if (fetchMore)
xOptions.offset += xOptions.limit;
return fetchMore;
},
function (err) {
if (err) {
transactions.error = err.message;
return callback(err, transactions);
}
if (manualFilter && skip)
transactions.data.splice(0, skip);
transactions.data.splice(limit, transactions.data.length - limit);
callback(null, transactions);
});
};
self.getBalance = function (options, callback) {
bitstampPrivate.balance(null, function (err, bitstampBalance) {
let balance = {
timestamp: util.timestampNow(),
error: err && err.message || "",
data: []
};
if (err) {
return callback(err, balance);
}
// var jf = require("jsonfile"); jf.writeFileSync(__dirname + "/bitstamp-getFee_MockApiResponse.json", bitstampBalance); // only used to create MockApiResponse file for the test unit
let data = {
total: [],
available: []
};
let amount = bitstampBalance.btc_balance;
if (amount)
data.total.push({currency: "XBT", amount: amount});
amount = bitstampBalance.usd_balance;
if (bitstampBalance.usd_balance)
data.total.push({currency: "USD", amount: amount});
amount = bitstampBalance.btc_available;
if (amount)
data.available.push({currency: "XBT", amount: amount});
amount = bitstampBalance.usd_available;
if (amount)
data.available.push({currency: "USD", amount: amount});
balance.data.push(data);
callback(null, balance);
});
};
self.getMarginPositions = function (options, callback) {
let err = new Error("Method not supported");
let result = {
timestamp: util.timestampNow(),
error: err && err.message || "",
data: []
};
if (err)
return callback(err, result);
return callback(null, result);
};
self.getOpenOrders = function (options, callback) {
var openOrders;
var err = new Error("Method not implemented");
openOrders = {
timestamp: util.timestampNow(),
error: err.message,
data: []
};
callback(err, openOrders);
};
self.postSellOrder = function (options, callback) {
var orderResult;
var err = new Error("Method not implemented");
orderResult = {
timestamp: util.timestampNow(),
error: err.message,
data: []
};
callback(err, orderResult);
};
self.postBuyOrder = function (options, callback) {
var orderResult;
var err = new Error("Method not implemented");
orderResult = {
timestamp: util.timestampNow(),
error: err.message,
data: []
};
callback(err, orderResult);
};
self.cancelOrder = function (options, callback) {
var orderResult;
var err = new Error("Method not implemented");
orderResult = {
timestamp: util.timestampNow(),
error: err.message,
data: []
};
callback(err, orderResult);
};
}
Bitstamp.prototype.properties = {
name: "Bitstamp", // Proper name of the exchange/provider
slug: "bitstamp", // slug name of the exchange. Needs to be the same as the .js filename
methods: {
implemented: ["getRate", "getTicker", "getOrderBook", "getFee", "getTransactions", "getBalance"],
notSupported: ["getLendBook", "getActiveOffers", "postOffer", "cancelOffer", "getMarginPositions"]
},
instruments: [ // all allowed currency/asset combinatinos (pairs) that form a market
{
pair: "XBT_USD"
}
],
dictionary: {
"withdrawal_requests": {
type: {
"0": "SEPA",
"1": "Bitcoin",
"2": "WIRE transfer"
},
status: {
"0": "open",
"1": "in process",
"2": "finished",
"3": "canceled",
"4": "failed"
}
},
"sell": {
"type": {
"0": "buy",
"1": "sell"
}
},
"buy": {
"type" :{
"0": "buy",
"1": "sell"
}
},
"open_orders": {
"type" :{
"0": "buy",
"1": "sell"
}
},
"user_transactions": {
"offset" : {
"default": 0
},
"limit" : {
"default": 100,
"maximum": 1000
},
"sort" : {
"desc": "desc",
"asc": "asc",
"default": "desc"
},
"type" :{
"0": "deposit",
"1": "withdrawal",
"2": "trade"
}
},
"getTransactions": {
"limit" : {
"maximum": 1000
}
}
},
publicAPI: {
supported: true, // is public API (not requireing user authentication) supported by this exchange?
requires: [] // required parameters
},
privateAPI: {
supported: true, // is public API (requireing user authentication) supported by this exchange?
requires: ["key", "secret", "username"]
},
marketOrder: false, // does it support market orders?
infinityOrder: false, // does it supports infinity orders?
// (which means that it will accept orders bigger then the current balance and order at the full balance instead)
monitorError: "", //if not able to monitor this exchange, please set it to an URL explaining the problem
tradeError: "" //if not able to trade at this exchange, please set it to an URL explaining the problem
};
module.exports = Bitstamp;