crypto-nodes
Version:
969 lines (642 loc) • 21.8 kB
JavaScript
const Gdax = require('gdax');
var request = require('request');
const https = require('https');
var uuid = require('uuid');
var tls = require('tls');
var fix = require('fixjs');
var crypto = require('crypto');
var exchange = 'gdax';
var per_second = 0;
var connected = false;
var no_data = 0;
var max_no_data = 5;
// wrapper for GDAX FIX
var gdax_fix = {
uuid_map: {},
init: function (node, config) {
this.node = node;
this.config = config;
this.connect();
},
connect: function() {
var that = this;
if(!this.config.server_fix || !this.config.server_fix_port) {
that.node.status({ fill: 'red', shape:'ring', text: "Missing FIX Config" });
return;
}
if(!this.config.api_key || !this.config.api_secret || !this.config.api_pass) {
that.node.status({ fill: 'red', shape:'ring', text: "Missing FIX Auth" });
return;
}
this.stream = tls.connect({ host: 'fix-public.sandbox.gdax.com', port: '4198' }, function() {
that.node.status({ fill: 'green', shape:'ring', text: "FIX Connected" });
});
this.client = fix.createClient(this.stream);
this.session = this.client.session(this.config.api_key, 'Coinbase');
// Stream Events
// this.stream.on('data', function(data) { console.log("FIX DATA :: " + data.toString()); });
this.stream.on('error', function(err) { console.log("FIX ERR :: " + err); });
this.stream.on('close', function () { console.log("FIX DISCONNECT"); });
this.stream.on('end', function() { console.log('FIX STREAM ENDED'); });
// Session Events
this.session.on('send', function(msg) { /*console.log('sending message: %s', msg);*/ });
this.session.on('error', function(err) { console.error(err.stack); });
this.session.on('logout', function() { console.log('FIX SESSION :: Logged out'); });
this.session.on('end', function() { console.log('FIX SESSION :: Ended'); /*this.stream.end();*/ });
this.session.on('logon', function() {
that.node.status({ fill: 'green', shape:'ring', text: "FIX Authenticated" });
});
// this.session.on('Reject', function(msg, next) { console.log('reject: %s', msg); next(); });
// this.session.on('OrderCancelReject', function(msg, next) { console.log('order cancel reject: %s', msg); next(); });
// The rest of these are handlers for various FIX keywords
this.session.on('ExecutionReport', function(msg, next) {
that.node.status({ fill: 'green', shape:'ring', text: "FIX Order Report" });
// console.log('FIX ORDER DATA :: ' + msg.toString());
var fnk_map = {
'0': 'Added',
'1': 'Filled',
'3': 'Done',
'4': 'Canceled',
'7': 'Stopped',
'8': 'Rejected',
'D': 'Changed',
'I': 'Info'
};
// console.log(msg.ExecType);
switch(msg.ExecType) {
case '0':
// NEW
if(that.uuid_map[msg.ClOrdID]) {
// Create mapping between REMOTE ORDER ID and LOCAL ORDER ID
that.uuid_map[msg.OrderID] = that.uuid_map[msg.ClOrdID];
var order_log_obj = {
op: 'order_log',
order_id: that.uuid_map[msg.OrderID],
status: 'ADDED',
msg: 'Order Added',
data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() }
}
that.node.send({ payload: order_log_obj });
}
break;
case '1':
// FILL
var order_log_obj = {
op: 'order_log',
order_id: that.uuid_map[msg.OrderID],
status: 'FILLED',
filled_amount: parseFloat(msg.LastShares),
msg: 'Order Filled',
data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() }
}
that.node.send({ payload: order_log_obj });
break;
case '3':
// DONE
var order_log_obj = {
op: 'order_log',
order_id: that.uuid_map[msg.OrderID],
status: 'DONE',
msg: 'Order Done',
data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() }
}
that.node.send({ payload: order_log_obj });
break;
case '4':
// CANCELED
var order_log_obj = {
op: 'order_log',
order_id: that.uuid_map[msg.OrderID],
status: 'CANCEL',
msg: 'Order Canceled',
data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() }
}
that.node.send({ payload: order_log_obj });
break;
case '7':
// STOPPED
var order_log_obj = {
op: 'order_log',
order_id: that.uuid_map[msg.OrderID],
status: 'CANCEL',
msg: 'Order Stopped',
data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() }
}
that.node.send({ payload: order_log_obj });
break;
case '8':
// REJECTED
if(that.uuid_map[msg.ClOrdID]) {
// Create mapping between REMOTE ORDER ID and LOCAL ORDER ID
that.uuid_map[msg.OrderID] = that.uuid_map[msg.ClOrdID];
}
var order_log_obj = {
op: 'order_log',
order_id: that.uuid_map[msg.OrderID],
status: 'ERROR',
msg: (msg.OrdRejReason && msg.OrdRejReason == 3 ? 'FUNDS': 'OTHER'),
data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() }
}
that.node.send({ payload: order_log_obj });
break;
case 'D':
// CHANGED
break;
case 'I':
// STATUS
var order_log_obj = {
op: 'order_log',
order_id: that.uuid_map[msg.OrderID],
status: 'INFO',
msg: 'Order Info',
data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() }
}
that.node.send({ payload: order_log_obj });
break;
}
//if(fnk_map[msg.ExecType]) {
if(that.uuid_map[msg.OrderID]) {
var text = 'Order ' + fnk_map[msg.ExecType] + '! Local ID ' + that.uuid_map[msg.OrderID] || ' NOT FOUND';
console.log(text);
that.node.status({ fill: 'green', shape:'ring', text: text });
}
//}
next();
});
// DO LOGIN ::
var logon = new fix.Msgs.Logon();
logon.SendingTime = new Date();
logon.HeartBtInt = 30;
logon.EncryptMethod = 0;
logon.set(554, this.config.api_pass); // FIX 4.4 Password tag
var presign = [
logon.SendingTime,
logon.MsgType,
this.session.outgoing_seq_num,
this.session.sender_comp_id,
this.session.target_comp_id,
this.config.api_pass
];
var what = presign.join('\x01');
logon.RawData = this.sign(what, this.config.api_secret);
// console.log(logon);
this.session.send(logon, true);
},
status: function () {
var orders = new fix.Msgs.OrderStatusRequest();
orders.OrderID = '*';
this.session.send(orders);
},
sign: function(what, secret) {
var key = Buffer(secret, 'base64');
var hmac = crypto.createHmac('sha256', key);
//console.log("presign: " + what);
var signature = hmac.update(what).digest('base64');
return signature;
},
sendOrder(symbol, price, amount, is_buy, id) {
var order = new fix.Msgs.NewOrderSingle();
var order_uuid = uuid();
// Map our numeric ID's to UUID to send to order
this.uuid_map[order_uuid] = id;
// Populate order object
order.Symbol = symbol;
order.ClOrdID = order_uuid;
order.Side = is_buy ? 1 : 2;
order.HandlInst = 1;
order.OrdType = 2; // 2=Limit
order.OrderQty = amount;
order.Price = price;
order.TimeInForce = '3'; // 1=GTC 3=IOC
order.TransactTime = new Date();
order.set(7928, 'D'); // STP
this.session.send(order);
},
cancelOrder: function(ClOrdID, OrderID, symbol) {
var cancel = new fix.Msgs.OrderCancelRequest();
cancel.Symbol = symbol;
cancel.OrigClOrdID = ClOrdID;
cancel.ClOrdID = 123456;
cancel.OrderID = OrderID;
this.session.send(cancel);
}
}
var gdax_obj = {
init_ws: function(node, symbols) {
if(this.gdax_ws && this.gdax_ws.socket &&
(this.gdax_ws.socket.readyState == 1 ||
this.gdax_ws.socket.readyState == 2)) {
console.log('Already have socket..');
return;
}
var that = this;
//console.log(this.gdax_ws_addr);
//console.log(this.gdax_ws_auth);
this.gdax_ws = new Gdax.WebsocketClient(symbols, this.gdax_ws_addr, this.gdax_ws_auth, this.gdax_ws_opts);
this.gdax_ws.on('open', (data) => {
console.log('GDAX :: CONNECTED');
// connected = true;
});
this.gdax_ws.on('message', (data) => {
if(!data.product_id) {
//console.log(data);
if(data.type == 'subscriptions') {
// console.log(data.channels[0].product_ids);
}
return;
}
switch(data.type) {
case 'user':
console.log('USER DATA::');
console.log(data);
break;
case 'heartbeat':
//console.log('HB');
break;
case 'snapshot':
// console.log('SNAPSHOT!!');
case 'l2update':
//console.log('.');
var sym_from = data.product_id.substr(0, data.product_id.indexOf("-"));
var sym_to = data.product_id.substr(data.product_id.indexOf("-") + 1, data.product_id.length - data.product_id.indexOf("-") + 1);
var payload = {
exchange: 'gdax',
fees: this.fees,
sym_from: sym_from,
sym_to: sym_to,
data: data
};
node.send({ payload: payload });
per_second++;
break;
default:
console.log('GDAX ::');
console.log(data);
break;
}
});
this.gdax_ws.on('error', err => {
if(err.message.indexOf('Failed to initialize level 2 channel for ') > -1) {
//console.log(err);
function resub(sym) {
setTimeout(function () {
//console.log('GDAX RESUB :: ' + sym);
that.gdax_ws.subscribe({ product_ids: [ sym ], channels: ['level2'] });
}, 5000);
}
var sym = err.message.substr(41, 7);
resub(sym);
} else {
console.log('GDAX ::');
console.log(err);
}
});
this.gdax_ws.on('close', () => {
console.log('GDAX :: WebSocket connection disconnected');
setTimeout(function () {
// that.gdax_ws.disconnect();
setTimeout(function () {
that.init_ws(node, symbols);
}, 1);
}, 5000);
});
setTimeout(function () {
console.log('RESET ::');
that.gdax_ws.disconnect();
setTimeout(function () {
that.init_ws(node, symbols);
}, 1);
}, 60000);
},
exchange_symbols: [],
configured_symbols: [],
enabled_symbols: [],
get_enabled_symbols: function() {
var out = [];
for(x in this.exchange_symbols) {
var pass = false;
var exc_tmp = this.exchange_symbols[x].split('-');
for(y in this.configured_symbols) {
var cfg_tmp = this.configured_symbols[y].split('/');
// If its matching or inverted variant matches its enabled symbol
if((exc_tmp[0] == cfg_tmp[0] && exc_tmp[1] == cfg_tmp[1]) ||
(exc_tmp[0] == cfg_tmp[1] && exc_tmp[1] == cfg_tmp[0])
) {
pass = true;
}
}
if(pass) {
out.push(this.exchange_symbols[x]);
}
}
return out;
},
msg: function(node, sym, ob) {
var sym_from = sym.substr(0,3);
var sym_to = sym.substr(3);
per_second++;
node.send({ payload: { 'exchange' : 'gdax', fees: this.fees, sym_from: sym_from, sym_to: sym_to, 'data' : ob } });
},
init: function(config, node, symbols) {
console.log(config);
var that = this;
try { this.fees = JSON.parse(config.fees); } catch (e) {}
this.configured_symbols = symbols;
this.gdax_ws_opts = { "channels": ['level2', 'user'] };
if(config.api_key && config.api_secret && config.api_pass) {
this.gdax_ws_auth = { key: config.api_key, secret: config.api_secret, passphrase: config.api_pass };
// Client
this.authedClient = new Gdax.AuthenticatedClient(
config.api_key,
config.api_secret,
config.api_pass,
config.server_api
);
} else {
this.authedClient = new Gdax.PublicClient(config.server_api);
this.gdax_ws_auth = null;
}
this.gdax_rq_opts = { url: config.server_api + '/products', headers: { 'User-Agent': '*' }};
this.gdax_ws_addr = config.server_ws;
if(this.gdax_ws && this.gdax_ws.socket) {
this.gdax_ws.disconnect();
}
if (!this.interval) {
this.interval = setInterval(function() {
var color = per_second > 0 ? 'green' : 'red';
node.status({
fill: color,
shape: 'ring',
text: per_second + ' requests per second'
});
per_second = 0;
}, 1000);
}
this.get_exchange_symbols(function (exchange_symbols) {
that.exchange_symbols = exchange_symbols;
var enabled_symbols = that.get_enabled_symbols();
//console.log('ENABLED::');
//console.log(enabled_symbols);
//console.log(that.configured_symbols);
that.init_ws(node, enabled_symbols);
});
},
symbols_map: {},
invert_map: {},
get_exchange_symbols(callback) {
request(this.gdax_rq_opts, function(error, response, body) {
if(body) {
var currencies = [];
try {
body = JSON.parse(body);
for (var i = 0; i < body.length; i++) {
currencies.push(body[i].id);
}
} catch(e) {
}
callback(currencies);
} else {
callback([]);
}
});
},
place_order: function(msg, obj, node) {
authedClient.placeOrder(obj, function (o_err, o_res, o_data) {
console.log('GDAX ORDER RESULT::');
console.log(o_data);
if(o_err) {
if(o_err.response.statusCode == 400 &&
o_err.response.body == '{"message":"Insufficient funds"}') {
node.status({ fill: 'red',shape:'ring', text: "Insufficient funds" });
var order_log_obj = {
op: 'order_log',
order_id: msg.payload.tx.acc,
status: 'ERROR',
msg: 'ORDER ERROR :: FUNDS',
data: { response: o_err.response.body, tx: msg.payload.tx }
}
node.send({ payload: order_log_obj });
} else {
var err = 'ORDER ERROR :: ' + o_err.response.statusCode + ' ' + o_err.response.statusMessage + ' ' + o_err.response.body;
node.status({ fill: 'red',shape:'ring', text: err });
var order_log_obj = {
op: 'order_log',
order_id: msg.payload.tx.acc,
status: 'ERROR',
msg: err,
data: { response: o_err.response.body, tx: msg.payload.tx }
}
node.send({ payload: order_log_obj });
}
return;
}
if(o_data.id && o_data.status == 'pending') {
node.send({ payload: { op: 'exchange_order', status: 'accepted', order: o_data, tx: msg.payload.tx } });
node.status({ fill: 'gray',shape:'ring', text: 'Order Pending' });
authedClient.getFills({ order_id: o_data.id }, function (f_err, f_res, f_data) {
if(f_err) {
var err = f_err.response.statusCode + ' ' + f_err.response.statusMessage + ' ' + f_err.response.body;
node.status({ fill: 'red',shape:'ring', text: err });
var order_log_obj = {
op: 'order_log',
order_id: msg.payload.tx.acc,
status: 'ERROR',
msg: 'FILL ERROR :: ' + err,
data: { response: o_err.response.body, tx: msg.payload.tx, order: o_data }
}
node.send({ payload: order_log_obj });
return;
}
if(f_data && f_data.length) {
console.log('GDAX FILL RESULT::');
console.log(f_data);
node.send({ payload: { op: 'exchange_order', status: 'fills', fills: f_data, tx: msg.payload.tx } });
var order_log_obj = {
op: 'order_log',
order_id: msg.payload.tx.acc,
status: 'FILL',
msg: 'ORDER FILL :: ' + JSON.stringify(f_data),
data: { response: o_err.response.body, tx: msg.payload.tx, order: o_data, fills: f_data }
}
node.send({ payload: order_log_obj });
node.status({ fill: 'orange',shape:'ring', text: 'Order Fill Data Received' });
}
authedClient.getOrder(o_data.id, function (c_err, c_res, c_data) {
node.status({ fill: 'green',shape:'ring', text: 'Order Checked' });
if(c_err) {
var err = f_err.response.statusCode + ' ' + f_err.response.statusMessage + ' ' + f_err.response.body;
var order_log_obj = {
op: 'order_log',
order_id: msg.payload.tx.acc,
status: 'ERROR',
msg: 'ORDER CHECK ERROR :: ' + c_err,
data: { response: o_err.response.body, tx: msg.payload.tx, order: o_data }
}
node.send({ payload: order_log_obj });
return;
}
console.log('GDAX ORDER CHECK RESULT::');
console.log(c_data);
node.send({ payload: { op: 'exchange_order', status: 'update', order: c_data, tx: msg.payload.tx } });
});
});
}
});
},
}
// VARS
var interval;
var symbols;
module.exports = function(RED) {
/*
function gdax_start(node, fees) {
gdax_get_symbols(function (symbols) {
if(symbols) {
console.log('GDAX :: SYMBOLS :: ' + symbols.join(','));
gdax_websocket_connect(node, symbols, fees);
} else {
// clearInterval(interval);
node.status({
fill: 'red',
shape: 'ring',
text: 'Error receiving symbols'
});
setTimeout(function () {
gdax_start(node, fees);
}, 1000);
}
});
}
*/
function gdaxConnector(config) {
RED.nodes.createNode(this, config);
var node = this;
node.on('input', function(msg) {
if(config.disabled) {
// Disable UI Updates
if(gdax_obj.interval) {
clearInterval(gdax_obj.interval);
}
// Set node status
node.status({ fill: 'red',shape:'ring', text: 'Disabled' });
// Emit flush orderbook
node.send({payload: { exchange: 'binance', op: 'flush' }});
return;
}
if(msg.payload && msg.payload.op == 'subscribe') {
gdax_obj.init(config, node, msg.payload.symbols);
}
});
node.on('close', function() {
// tidy up any async code here - shutdown connections and so on.
node.status({ fill: 'red',shape:'ring', text: 'Offline' });
node.send({payload: { exchange: 'gdax', op: 'flush' }});
});
}
function gdaxOrder(config) {
RED.nodes.createNode(this,config);
var node = this;
// Initialize our FIX library
gdax_fix.init(node, config);
node.on('input', function(msg) {
// Basic validate its an exchange order for this exchange
if(msg.payload &&
msg.payload.op &&
msg.payload.op == 'exchange_order' &&
msg.payload.exchange == exchange &&
msg.payload.tx) {
console.log('GDAX SENDING EXCHANGE ORDER ::');
console.log(msg.payload.tx);
gdax_fix.sendOrder(
msg.payload.tx.symbol.replace('/','-'),
msg.payload.tx.price + 5,
msg.payload.tx.qty,
msg.payload.tx.is_buy,
msg.payload.tx.acc
);
/*
console.log('GDAX SENDING EXCHANGE ORDER ::');
console.log(msg.payload.tx);
var obj = {
type: 'limit',
time_in_force: 'IOC',
side: msg.payload.tx.is_buy ? "buy" : "sell",
price: msg.payload.tx.price,
size: msg.payload.tx.qty,
product_id: msg.payload.tx.symbol.replace('/', '-'),
client_oid: msg.payload.tx.acc,
}
*/
}
});
}
function gdaxBalance(config) {
RED.nodes.createNode(this,config);
var node = this;
this.conf = RED.nodes.getNode(config.config);
// console.log(this.conf);
//client.addStream(false, 'wallet', function (data, symbol, tableName) {
// node.send({ payload: { exchange: exchange, op: 'get_balance', balances: out } });
// node.status({ fill: 'green',shape:'ring', text: text });
//});
node.on('input', function(msg) {
if(msg.payload && msg.payload.op && msg.payload.op == 'get_balance') {
if(msg.payload.exchange && (msg.payload.exchange == 'all' || msg.payload.exchange == exchange)) {
node.conf.balance(function (err, out) {
console.log('GDAX GET BALANCE ::');
console.log(out);
node.send({ payload: { exchange: exchange, op: 'get_balance', balances: out } });
var text = '';
for(x in out) {
text += x + ' = ' + out[x].balance + ', ';
}
text = text.substr(0, text.length - 2);
node.status({ fill: 'green',shape:'ring', text: 'Received balance data ' + text });
});
}
}
});
}
function gdaxConfig(n) {
RED.nodes.createNode(this,n);
var node = this;
// Copy config to node
var f = [ 'api_key','api_secret','api_pass','server_ws','server_api','server_fix','server_fix_port' ];
for(x in f) { node[f[x]] = n[f[x]]; }
if(this.api_key && this.api_secret && this.api_pass && this.server_api) {
this.authedClient = new Gdax.AuthenticatedClient(
this.api_key,
this.api_secret,
this.api_pass,
this.server_api
);
}
this.balance = function(callback) {
var out = {};
if(node.authedClient) {
node.authedClient.getCoinbaseAccounts(function (err, res, data) {
if(err) {
callback(err, out);
} else {
if(data && data.length) {
for(x in data) {
if(data[x].active && data[x].type == 'wallet') {
out[data[x].currency] = { balance: data[x].balance }; // .available, .balance , .hold
}
}
callback(null, out);
} else {
callback('GDAX - Bad balance data', out);
}
}
});
} else {
callback('GDAX - No Client', out);
}
}
}
RED.nodes.registerType("gdaxConfig", gdaxConfig);
RED.nodes.registerType("gdaxBalance", gdaxBalance);
RED.nodes.registerType("gdaxOrder", gdaxOrder);
RED.nodes.registerType("gdaxConnector", gdaxConnector);
}