diff --git a/README.md b/README.md index 405a5940..ec3d3f91 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,25 @@ const client2 = Binance({ client.time().then(time => console.log(time)) ``` +Can also use an RSA api key. You can create [here](https://www.binance.com/en/support/faq/how-to-generate-an-rsa-key-pair-to-send-api-requests-on-binance-2b79728f331e43079b27440d9d15c5db) + +```js +import Binance from 'binance-api-node' + +const client = Binance() + +// Authenticated client, can make signed calls +const client2 = Binance({ + apiKey: 'xxx', + apiSecret: 'xxx', + getTime: xxx, + privateKey: 'path of .pem file', + privateKeyPassphrase: 'password .pem' +}) + +client.time().then(time => console.log(time)) +``` + If you do not have an appropriate babel config, you will need to use the basic commonjs requires. ```js diff --git a/index.d.ts b/index.d.ts index 65f7d8f7..1914b54e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,6 @@ // tslint:disable:interface-name declare module 'binance-api-node' { - export default function(options?: { + export default function (options?: { apiKey?: string apiSecret?: string getTime?: () => number | Promise @@ -8,7 +8,9 @@ declare module 'binance-api-node' { httpFutures?: string wsBase?: string wsFutures?: string - proxy?: string + proxy?: string, + privateKey?: Buffer | string, + privateKeyPassphrase?: string }): Binance export type ErrorCodes_LT = @@ -339,11 +341,11 @@ declare module 'binance-api-node' { export type CancelOrderOptions = | { symbol: string; orderId: number; useServerTime?: boolean; newClientOrderId?: string } | { - symbol: string - origClientOrderId: string - useServerTime?: boolean - newClientOrderId?: string - } + symbol: string + origClientOrderId: string + useServerTime?: boolean + newClientOrderId?: string + } export type GetOrderOcoOptions = | { orderListId: number; useServerTime?: boolean } @@ -352,11 +354,11 @@ declare module 'binance-api-node' { export type CancelOrderOcoOptions = | { symbol: string; orderListId: number; useServerTime?: boolean; newClientOrderId?: string } | { - symbol: string - listClientOrderId: string - useServerTime?: boolean - newClientOrderId?: string - } + symbol: string + listClientOrderId: string + useServerTime?: boolean + newClientOrderId?: string + } export type cancelOpenOrdersOptions = { symbol: string @@ -599,10 +601,10 @@ declare module 'binance-api-node' { recvWindow?: number }): Promise dustLog(options: { - startTime?: number - endTime?: number - recvWindow?: number - timestamp: number + startTime?: number + endTime?: number + recvWindow?: number + timestamp: number }): DustLog universalTransfer(options: UniversalTransfer): Promise<{ tranId: number }> universalTransferHistory( @@ -1631,9 +1633,9 @@ declare module 'binance-api-node' { listClientOrderId: string transactionTime: number orders: Array<{ - symbol: string - orderId: number - clientOrderId: string + symbol: string + orderId: number + clientOrderId: string }> } diff --git a/package.json b/package.json index bd595de9..b45453e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "binance-api-node", - "version": "0.12.4", + "version": "0.12.5-RC1", "description": "A node API wrapper for Binance", "main": "dist", "files": [ diff --git a/src/http-client.js b/src/http-client.js index 7156d6a1..de21730c 100644 --- a/src/http-client.js +++ b/src/http-client.js @@ -20,12 +20,21 @@ const info = { /** * Build query string for uri encoded url based on json object */ -const makeQueryString = q => - q - ? `?${Object.keys(q) - .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(q[k])}`) - .join('&')}` - : '' +const makeQueryString = params => { + if (!params) return '' + return Object.entries(params) + .map(stringifyKeyValuePair) + .join('&') +} + +/** + * NOTE: The array conversion logic is different from usual query string. + * E.g. symbols=["BTCUSDT","BNBBTC"] instead of symbols[]=BTCUSDT&symbols[]=BNBBTC + */ +const stringifyKeyValuePair = ([key, value]) => { + const valueString = Array.isArray(value) ? `["${value.join('","')}"]` : value + return `${key}=${encodeURIComponent(valueString)}` +} /** * Get API limits info from headers @@ -47,8 +56,8 @@ const responseHandler = res => { const marketName = res.url.includes(FUTURES) ? 'futures' : res.url.includes(COIN_FUTURES) - ? 'delivery' - : 'spot' + ? 'delivery' + : 'spot' Object.keys(headersMapping).forEach(key => { const outKey = headersMapping[key] @@ -122,13 +131,12 @@ const checkParams = (name, payload, requires = []) => { const publicCall = ({ proxy, endpoints }) => (path, data, method = 'GET', headers = {}) => { return sendResult( fetch( - `${ - path.includes('/fapi') || path.includes('/futures') - ? endpoints.futures - : path.includes('/dapi') + `${path.includes('/fapi') || path.includes('/futures') + ? endpoints.futures + : path.includes('/dapi') ? endpoints.delivery : endpoints.base - }${path}${makeQueryString(data)}`, + }${path}?${makeQueryString(data)}`, { method, json: true, @@ -173,8 +181,10 @@ const privateCall = ({ endpoints, getTime = defaultGetTime, pubCall, + privateKey, + privateKeyPassphrase }) => (path, data = {}, method = 'GET', noData, noExtra) => { - if (!apiKey || !apiSecret) { + if ((!apiKey || !apiSecret) && (!apiKey || !privateKey || !privateKeyPassphrase)) { throw new Error('You need to pass an API key and secret to make authenticated calls.') } @@ -186,22 +196,37 @@ const privateCall = ({ delete data.useServerTime } - const signature = crypto - .createHmac('sha256', apiSecret) - .update(makeQueryString({ ...data, timestamp }).substr(1)) - .digest('hex') - - const newData = noExtra ? data : { ...data, timestamp, signature } + let signature; + let newData; + let queryString; + if (!privateKey) { + signature = crypto + .createHmac('sha256', apiSecret) + .update(makeQueryString({ ...data, timestamp })) + .digest('hex') + newData = noExtra ? data : { ...data, timestamp, signature } + queryString = makeQueryString(newData) + } else { + signature = crypto + .createSign('RSA-SHA256') + .update(makeQueryString({ ...data, timestamp })) + .sign({ + key: privateKey, + passphrase: privateKeyPassphrase + }, 'base64') + signature = encodeURIComponent(signature) + newData = noExtra ? makeQueryString(data) : `${makeQueryString({ ...data, timestamp })}&signature=${signature}`; + queryString = newData; + } return sendResult( fetch( - `${ - path.includes('/fapi') || path.includes('/futures') - ? endpoints.futures - : path.includes('/dapi') + `${path.includes('/fapi') || path.includes('/futures') + ? endpoints.futures + : path.includes('/dapi') ? endpoints.delivery : endpoints.base - }${path}${noData ? '' : makeQueryString(newData)}`, + }${path}${noData ? '' : `?${queryString}`} `, { method, headers: { 'X-MBX-APIKEY': apiKey },