From 6aac83e04ff7e47d066af4ab20a6555d7309186b Mon Sep 17 00:00:00 2001 From: Ferdinando Terranova Date: Mon, 17 Apr 2023 22:12:52 +0200 Subject: [PATCH 1/5] RSA Key added privateKey and privateKeyPassphrase for RSA Keys authentication --- index.d.ts | 40 ++++++++++++++------------ package.json | 2 +- src/http-client.js | 72 +++++++++++++++++++++++++++++----------------- 3 files changed, 68 insertions(+), 46 deletions(-) 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..10742f3e 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, -}) => (path, data = {}, method = 'GET', noData, noExtra) => { - if (!apiKey || !apiSecret) { + privateKey, + privateKeyPassphrase +}) => (path, data = {}, method = 'GET') => { + 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,32 @@ 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; + const queryString = makeQueryString({ ...data, timestamp }); + if (!privateKey) { + signature = crypto + .createHmac('sha256', apiSecret) + .update(queryString) + .digest('hex') + } else { + signature = crypto + .createSign('RSA-SHA256') + .update(queryString) + .sign({ + key: privateKey, + passphrase: privateKeyPassphrase + }, 'base64') + signature = encodeURIComponent(signature) + } 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}?${queryString}&signature=${signature}`, { method, headers: { 'X-MBX-APIKEY': apiKey }, From c6922667a455980b66e7c9ab00982e4760ff4f7d Mon Sep 17 00:00:00 2001 From: Ferdinando Terranova Date: Mon, 17 Apr 2023 22:12:58 +0200 Subject: [PATCH 2/5] update readme --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 From e6b63d5dab1f5e15751a4f633707c448e22efd64 Mon Sep 17 00:00:00 2001 From: Ferdinando Terranova Date: Mon, 24 Apr 2023 12:36:02 +0200 Subject: [PATCH 3/5] fix websocket --- src/http-client.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/http-client.js b/src/http-client.js index 10742f3e..ca95d343 100644 --- a/src/http-client.js +++ b/src/http-client.js @@ -183,7 +183,7 @@ const privateCall = ({ pubCall, privateKey, privateKeyPassphrase -}) => (path, data = {}, method = 'GET') => { +}) => (path, data = {}, method = 'GET', noData, noExtra) => { if ((!apiKey || !apiSecret) && (!apiKey || !privateKey || !privateKeyPassphrase)) { throw new Error('You need to pass an API key and secret to make authenticated calls.') } @@ -197,7 +197,7 @@ const privateCall = ({ } let signature; - const queryString = makeQueryString({ ...data, timestamp }); + let queryString = makeQueryString({ ...data, timestamp }); if (!privateKey) { signature = crypto .createHmac('sha256', apiSecret) @@ -213,7 +213,8 @@ const privateCall = ({ }, 'base64') signature = encodeURIComponent(signature) } - + const newData = noExtra ? data : queryString; + queryString = noData ? '' : makeQueryString(newData) return sendResult( fetch( `${path.includes('/fapi') || path.includes('/futures') @@ -221,7 +222,7 @@ const privateCall = ({ : path.includes('/dapi') ? endpoints.delivery : endpoints.base - }${path}?${queryString}&signature=${signature}`, + }${path}?${queryString}${noData ? '' : `&signature=+${signature}`}`, { method, headers: { 'X-MBX-APIKEY': apiKey }, From 04e1f5c18da1513956e4d44aa503b8c4a182979e Mon Sep 17 00:00:00 2001 From: Ferdinando Terranova Date: Mon, 24 Apr 2023 18:25:03 +0200 Subject: [PATCH 4/5] fix major issue on WebSocket --- src/http-client.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/http-client.js b/src/http-client.js index ca95d343..f036e56f 100644 --- a/src/http-client.js +++ b/src/http-client.js @@ -197,7 +197,8 @@ const privateCall = ({ } let signature; - let queryString = makeQueryString({ ...data, timestamp }); + const newData = noExtra ? data : { ...data, timestamp }; + const queryString = noData ? '' : makeQueryString(newData); if (!privateKey) { signature = crypto .createHmac('sha256', apiSecret) @@ -213,8 +214,6 @@ const privateCall = ({ }, 'base64') signature = encodeURIComponent(signature) } - const newData = noExtra ? data : queryString; - queryString = noData ? '' : makeQueryString(newData) return sendResult( fetch( `${path.includes('/fapi') || path.includes('/futures') @@ -222,7 +221,7 @@ const privateCall = ({ : path.includes('/dapi') ? endpoints.delivery : endpoints.base - }${path}?${queryString}${noData ? '' : `&signature=+${signature}`}`, + }${path}?${queryString}${noData ? '' : `&signature=${signature}`}`, { method, headers: { 'X-MBX-APIKEY': apiKey }, From 2ba0cb392c91bf359ed39624868f7d907ba5f7bd Mon Sep 17 00:00:00 2001 From: Ferdinando Terranova Date: Mon, 24 Apr 2023 22:04:06 +0200 Subject: [PATCH 5/5] fix RSA signature problems on WebSocket --- src/http-client.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/http-client.js b/src/http-client.js index f036e56f..de21730c 100644 --- a/src/http-client.js +++ b/src/http-client.js @@ -197,23 +197,28 @@ const privateCall = ({ } let signature; - const newData = noExtra ? data : { ...data, timestamp }; - const queryString = noData ? '' : makeQueryString(newData); + let newData; + let queryString; if (!privateKey) { signature = crypto .createHmac('sha256', apiSecret) - .update(queryString) + .update(makeQueryString({ ...data, timestamp })) .digest('hex') + newData = noExtra ? data : { ...data, timestamp, signature } + queryString = makeQueryString(newData) } else { signature = crypto .createSign('RSA-SHA256') - .update(queryString) + .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') @@ -221,7 +226,7 @@ const privateCall = ({ : path.includes('/dapi') ? endpoints.delivery : endpoints.base - }${path}?${queryString}${noData ? '' : `&signature=${signature}`}`, + }${path}${noData ? '' : `?${queryString}`} `, { method, headers: { 'X-MBX-APIKEY': apiKey },