From d26d41ce3bd1e41f5ec2b2273c56be9045805c1a Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:09:03 +0000 Subject: [PATCH 1/5] feat(client): add RSA/ECDSA support --- README.md | 13 +++++++++++++ src/http-client.js | 29 +++++++++++++++++++++-------- src/signature.js | 23 +++++++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a7ff3ad4..bffddd79 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,20 @@ const client = Binance({ This library works in both browsers and Node.js environments: +### RSA/ECDSA support +This library supports RSA and ED25519 keys out of the box. The usage is straightforward, just provide `privateKey` instead of `apiSecret`. +```js + +const apiKey = "ZymCbCxu1LiYIW8IcYSbXQOaAtHaeW3ioCU5qFf5QvUyTfP1runCaY8AwzCaoOaq" +const privateKey = "-----BEGIN PRIVATE KEY-----\ndMC4CAfAwafYDK2cwaCIEIa+Ax8dMK50wcIcD0Zdf2jaCDoRdaoc7KaadRUh+aLdt\n-----END PRIVATE KEY-----" +const client = Binance({ + privateKey, + apiKey, +}) + +``` ### Proxy Support (Node.js only) @@ -261,6 +273,7 @@ Following examples will use the `await` form, which requires some configuration | ----------- | -------- | -------- | -------------------------------------------- | | apiKey | String | false | Required when making private calls | | apiSecret | String | false | Required when making private calls | +| privateKey | String | false | Required when using RSA/Ed25519 calls | | getTime | Function | false | Time generator, defaults to () => Date.now() | | httpBase | String | false | Changes the default endpoint | | httpFutures | String | false | Changes the default endpoint | diff --git a/src/http-client.js b/src/http-client.js index 3e26237d..6e20041d 100644 --- a/src/http-client.js +++ b/src/http-client.js @@ -1,6 +1,6 @@ import zip from 'lodash.zipobject' import JSONbig from 'json-bigint' -import { createHmacSignature } from './signature' +import { createHmacSignature, createAsymmetricSignature } from './signature' // Robust environment detection for Node.js vs Browser const isNode = (() => { @@ -230,10 +230,10 @@ const keyCall = * @returns {object} The api response */ const privateCall = - ({ apiKey, apiSecret, proxy, endpoints, getTime = defaultGetTime, pubCall, testnet }) => + ({ apiKey, apiSecret, privateKey, proxy, endpoints, getTime = defaultGetTime, pubCall, testnet }) => (path, data = {}, method = 'GET', noData, noExtra) => { - if (!apiKey || !apiSecret) { - throw new Error('You need to pass an API key and secret to make authenticated calls.') + if (!apiKey || (!apiSecret && !privateKey)) { + throw new Error('You need to pass an API key and secret/privateKey to make authenticated calls.') } return ( @@ -250,10 +250,23 @@ const privateCall = const dataToSign = queryString.substr(1) // Create signature (async in browser, sync in Node.js) - return createHmacSignature(dataToSign, apiSecret).then(signature => ({ - timestamp, - signature, - })) + if (apiSecret) { + return createHmacSignature(dataToSign, apiSecret).then(signature => ({ + timestamp, + signature, + })) + } else if (privateKey) { + const sig = createAsymmetricSignature(dataToSign, privateKey) + // .then(signature => ({ + // timestamp, + // signature, + // })) + return { + timestamp, + signature: sig, + } + } + }) .then(({ timestamp, signature }) => { const newData = noExtra ? data : { ...data, timestamp, signature } diff --git a/src/signature.js b/src/signature.js index 76a34e37..73a697c3 100644 --- a/src/signature.js +++ b/src/signature.js @@ -1,3 +1,5 @@ +import { sign } from 'crypto' + // Robust environment detection for Node.js vs Browser const isNode = (() => { // Check for Node.js specific features @@ -59,3 +61,24 @@ export const createHmacSignature = async (data, secret) => { .join('') /* eslint-enable no-undef */ } + +export const createAsymmetricSignature = (data, privateKey) => { + // Handles RSA and ECDASA (Ed25519) private keys + const privateKeyObj = { key: privateKey }; + const keyObject = nodeCrypto.createPrivateKey(privateKeyObj) + + let signature = ''; + + if (privateKey.length > 120) { + // RSA key + signature = nodeCrypto + .sign('RSA-SHA256', Buffer.from(data), keyObject) + .toString('base64'); + // if (encode) signature = encodeURIComponent(signature); + } else { + + // Ed25519 key + signature = nodeCrypto.sign(null, Buffer.from(data), keyObject).toString('base64'); + } + return signature; +} \ No newline at end of file From c9b8ee996bfeb621d81661f8be194b7a08bcfc6c Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:11:13 +0000 Subject: [PATCH 2/5] rm import --- src/signature.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/signature.js b/src/signature.js index 73a697c3..200f445b 100644 --- a/src/signature.js +++ b/src/signature.js @@ -1,5 +1,3 @@ -import { sign } from 'crypto' - // Robust environment detection for Node.js vs Browser const isNode = (() => { // Check for Node.js specific features From a714d45845352a96a87601e7ec1b0cdeeac21cc2 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:12:40 +0000 Subject: [PATCH 3/5] fix linting --- src/http-client.js | 16 +++++++++++++--- src/signature.js | 15 ++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/http-client.js b/src/http-client.js index 6e20041d..9602caa5 100644 --- a/src/http-client.js +++ b/src/http-client.js @@ -230,10 +230,21 @@ const keyCall = * @returns {object} The api response */ const privateCall = - ({ apiKey, apiSecret, privateKey, proxy, endpoints, getTime = defaultGetTime, pubCall, testnet }) => + ({ + apiKey, + apiSecret, + privateKey, + proxy, + endpoints, + getTime = defaultGetTime, + pubCall, + testnet, + }) => (path, data = {}, method = 'GET', noData, noExtra) => { if (!apiKey || (!apiSecret && !privateKey)) { - throw new Error('You need to pass an API key and secret/privateKey to make authenticated calls.') + throw new Error( + 'You need to pass an API key and secret/privateKey to make authenticated calls.', + ) } return ( @@ -266,7 +277,6 @@ const privateCall = signature: sig, } } - }) .then(({ timestamp, signature }) => { const newData = noExtra ? data : { ...data, timestamp, signature } diff --git a/src/signature.js b/src/signature.js index 200f445b..2e43ee53 100644 --- a/src/signature.js +++ b/src/signature.js @@ -62,21 +62,18 @@ export const createHmacSignature = async (data, secret) => { export const createAsymmetricSignature = (data, privateKey) => { // Handles RSA and ECDASA (Ed25519) private keys - const privateKeyObj = { key: privateKey }; + const privateKeyObj = { key: privateKey } const keyObject = nodeCrypto.createPrivateKey(privateKeyObj) - let signature = ''; + let signature = '' if (privateKey.length > 120) { // RSA key - signature = nodeCrypto - .sign('RSA-SHA256', Buffer.from(data), keyObject) - .toString('base64'); + signature = nodeCrypto.sign('RSA-SHA256', Buffer.from(data), keyObject).toString('base64') // if (encode) signature = encodeURIComponent(signature); } else { - // Ed25519 key - signature = nodeCrypto.sign(null, Buffer.from(data), keyObject).toString('base64'); + signature = nodeCrypto.sign(null, Buffer.from(data), keyObject).toString('base64') } - return signature; -} \ No newline at end of file + return signature +} From 3d4d5e90d293b13483fc79a5ef272d05c25291c1 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:16:37 +0000 Subject: [PATCH 4/5] fix msg --- test/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/index.js b/test/index.js index 3ab35f5e..1b4ade41 100644 --- a/test/index.js +++ b/test/index.js @@ -120,7 +120,7 @@ test('[REST] Signed call without creds', async t => { try { await client.accountInfo() } catch (e) { - t.is(e.message, 'You need to pass an API key and secret to make authenticated calls.') + t.is(e.message, 'You need to pass an API key and secret/privateKey to make authenticated calls.') } }) @@ -128,7 +128,7 @@ test('[REST] Signed call without creds - attempt getting tradeFee', async t => { try { await client.tradeFee() } catch (e) { - t.is(e.message, 'You need to pass an API key and secret to make authenticated calls.') + t.is(e.message, 'You need to pass an API key and secret/privateKey to make authenticated calls.') } }) From f129bea0bdf3245f29fec2d28728233d329c425c Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:22:59 +0000 Subject: [PATCH 5/5] fix test --- test/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/index.js b/test/index.js index 1b4ade41..6c71fe07 100644 --- a/test/index.js +++ b/test/index.js @@ -120,7 +120,10 @@ test('[REST] Signed call without creds', async t => { try { await client.accountInfo() } catch (e) { - t.is(e.message, 'You need to pass an API key and secret/privateKey to make authenticated calls.') + t.is( + e.message, + 'You need to pass an API key and secret/privateKey to make authenticated calls.', + ) } }) @@ -128,7 +131,10 @@ test('[REST] Signed call without creds - attempt getting tradeFee', async t => { try { await client.tradeFee() } catch (e) { - t.is(e.message, 'You need to pass an API key and secret/privateKey to make authenticated calls.') + t.is( + e.message, + 'You need to pass an API key and secret/privateKey to make authenticated calls.', + ) } })