const assert = require('assert'); const querystring = require('querystring'); const { createHmac, createHash } = require('crypto'); const request = require('superagent'); const { reduce } = require('lodash'); const safely = require('./safely'); const withRetry = require('./withRetry'); const { KRAKEN_API_KEY, KRAKEN_API_SECRET, } = process.env; assert(KRAKEN_API_KEY, 'KRAKEN_API_KEY is required'); assert(KRAKEN_API_SECRET, 'KRAKEN_API_SECRET is required'); const KRAKEN_BASE_URL = 'https://api.kraken.com'; const isNonceError = error => error.message.match(/EAPI:Invalid nonce/); const withKrakenRetry = fn => { return withRetry(fn, 10, error => isNonceError(error)); }; const krakenGet = withKrakenRetry(async (path) => { console.log(path); const response = await request(`${KRAKEN_BASE_URL}${path}`).retry(); const { body } = response; const { error, result } = body; if (error && error.length) { const wrappedError = new Error(`Kraken returned error: ${error[0]}`); throw wrappedError; } return result; }); const apiSecretBuffer = new Buffer(KRAKEN_API_SECRET, 'base64'); const krakenPost = withKrakenRetry(async (path, fields) => { const nonce = (+new Date() * 1e3).toString(); const requestBody = querystring.stringify(Object.assign({ nonce }, fields)); const hash = createHash('sha256').update(nonce).update(requestBody).digest(); const signature = createHmac('sha512', apiSecretBuffer).update(path).update(hash).digest('base64'); const response = await request .post(`https://api.kraken.com${path}`) .set('API-Key', KRAKEN_API_KEY) .set('API-Sign', signature) .set('Content-Type', 'application/x-www-form-urlencoded') .set('Content-Length', requestBody.length) .send(requestBody) .retry(); const { body } = response; const { error } = body; if (error && error.length) { throw new Error(`Kraken error: ${error[0]}`); } const { result } = body; assert(result); return result; }); // returns { id, } async function placeOrder(order) { const { symbol, side, price, size, internalId, postOnly = true, expiresIn = 90, } = order; assert(side && size && price); assert(side === 'buy' || side === 'sell'); assert(symbol); assert(price); assert(size); const pair = symbol; assert(pair, `Unknown symbol ${symbol}`); try { const { txid } = await krakenPost('/0/private/AddOrder', { pair, type: side, ordertype: 'limit', price: parseFloat(price).toFixed(6), volume: size.toString(), ...(postOnly ? { oflags: 'post' } : {}), ...(expiresIn ? { expiretm: `+${expiresIn}` } : {}), }); if (!txid.length) { throw new Error('Failed to place order'); } return txid[0]; } catch (error) { // TODO: Error handling throw error; } } // returns true or false async function cancelOrder(id) { assert.equal(typeof id, 'string'); let result; try { result = await krakenPost('/0/private/CancelOrder', { txid: id, }); } catch (error) { if (error.message.match(/EOrder:Unknown order/)) { return false; } throw error; } const { count } = result; if (!count) { throw new Error(`Failed to cancel order ${id}`); } return !!count; } async function fetchMyOpenOrders() { const [error, result] = await safely(() => krakenPost('/0/private/OpenOrders', { trades: false, })); if (error) { throw error; } const orders = reduce(result.open || {}, (prev, item, id) => { const { pair } = item.descr; return { ...prev, [id]: { id, symbol: pair, side: item.descr.type, price: +item.descr.price, size: +item.vol - +item.vol_exec, createdAt: +new Date(item.opentm * 1e3), }, }; }, {}); return orders; } async function fetchOrderBook(pair, count) { assert.equal(typeof pair, 'string'); assert(count === undefined || typeof count === 'number'); const result = await krakenGet(`/0/public/Depth?pair=${pair}`); const { bids, asks } = result[Object.keys([result][0])]; return { bids, asks, }; } async function fetchMyBalances() { const [error, result] = await safely(() => krakenPost('/0/private/Balance')); if (error) { throw error; } return reduce(result, (prev, balance, currency) => { return { ...prev, [currency]: +balance, } }, {}); } module.exports = { placeOrder, cancelOrder, fetchMyBalances, fetchMyOpenOrders, fetchOrderBook, };