I've spent several hours trying to get the correct response from the endpoint. For that purpose I need to send a signature string generated by a SHA-256 function, attached to the query string I send to the server.
I've tried different methods to get that signature as described here in the docummentation: https://developers.binance.com/docs/binance-api/spot/index/#signed-trade-user_data-and-margin-endpoint-security But nothing seems to work.
I've tried several approaches for generating valid signatures using different libraries and functions, but they're not working (I even tried using the Web Crypto API documentation)
I'm getting this error when I make the call:
{"code":-2014,"msg":"API-key format invalid."}
This is the call:
https://testnet.binance.vision/api/v3/account?timestamp=my_timestamp&signature=my_signature
I guessed it was a problem with Fetch, but in other custom functions I have in my app it causes no problems.
Here's my code:
export async function getAccountInfo() {
const apiSecret = pub.TESTNET_SECRETKEY; // Your secret key
const timestamp = await serverTimestamp()
.then(timestamp => {
return timestamp;
});
let signature = sha256(apiSecret, timestamp);
const testnet = 'https://testnet.binance.vision/api';
// {{url}}/api/v3/account?timestamp={{timestamp}}&signature={{signature}}
const fullUrl = testnet + '/v3/account?timestamp=' + timestamp + '&signature=' + signature;
retrieveInformation(fullUrl);
}
I was just sending the incorrect timestamp string to the hashing function, in this line:
let signature = sha256(apiSecret, timestamp); // Keep reading and you'll understand why.
Though I used a dependency, I think this is still a complete valid solution.
https://www.npmjs.com/package/jhash.js The functions are rather simple and straight forward to use.
The problem was in the queryString I was sending to the hash function.
As the Binance API documentation explains, though obscurely:
- Endpoints use
HMAC SHA256
signatures. TheHMAC SHA256
signature is a keyedHMAC SHA256
operation. Use yoursecretKey
as the key andtotalParams
as the value for the HMAC operation.totalParams
is defined as the query string concatenated with the request body.
The last point really puzzled me.
Now, the solution was to send the correct string (queryString) into the sha256 function. What it is required for the API is:
https://testnet.binance.vision/api/v3/account?timestamp=my_timestamp&signature=my_signature
The timestamp=
substring was the solution for my problem. I had to send that tiny piece of code into the hex_hmac_sha256
function, which is the format required for the Binance API.
async function serverTimestamp() {
const url = 'https://testnet.binance.vision/api/v3/time';
const timeServer = await getJson(url);
return timeServer.serverTime;
}
Not the local time, but the time server must be sent inside the signature. This was the solution to the problem.
export async function getAccountInfo() {
const apiSecret = pub.TESTNET_SECRETKEY; // Your secret key
const timestamp = await serverTimestamp()
.then(timestamp => {
return timestamp;
});
const queried_timestamp = 'timestamp=' + timestamp;
// https://www.npmjs.com/package/jhash.js
let signature = JHash.hex_hmac_sha256(apiSecret, queried_timestamp);
// let signature = await sha256(apiSecret, queried_timestamp); // This one is not library dependant.
const testnet = 'https://testnet.binance.vision/api';
// {{url}}/api/v3/account?timestamp={{timestamp}}&signature={{signature}}
const fullUrl = testnet + '/v3/account?timestamp=' + timestamp + '&signature=' + signature; // + '&recvWindow=60000';
retrieveInformation(fullUrl);
}
Notice in the following line of code, I'm sending the string contained in the URL as a queryString.
let signature = JHash.hex_hmac_sha256(apiSecret, queried_timestamp);
// This is the same line than the one I wrote above,
// but using another version of the function.
This is the example that led me in the right direction: https://developers.binance.com/docs/binance-api/spot/index/#example-1-as-a-request-body
As you can see in the official documentation example, they echoed the complete queryString(s) for making the signature.
Now, the other functions you may need to understand better the problem:
async function retrieveInformation(url = null) {
const apiKey = pub.TESTNET_APIKEY; // Your ApiKey
let httpHeaders = {
'Content-Type': 'application/x-www-form-urlencoded',
'X-MBX-APIKEY': apiKey
}
let myHeaders = new Headers(httpHeaders);
var requestOptions = {
headers: myHeaders
};
console.log(url);
console.log(requestOptions);
const data = await getJson(url, requestOptions);
console.log(data);
return data;
}
data
is displayed as the following JSON object:
{
"makerCommission": 15,
"takerCommission": 15,
"buyerCommission": 0,
"sellerCommission": 0,
"canTrade": true,
"canWithdraw": true,
"canDeposit": true,
"updateTime": 123456789,
"accountType": "SPOT",
"balances": [
{
"asset": "BTC",
"free": "4723846.89208129",
"locked": "0.00000000"
},
{
"asset": "LTC",
"free": "4763368.68006011",
"locked": "0.00000000"
}
],
"permissions": [
"SPOT"
]
}
You can see this same information shown here in the API Binance documentation: https://developers.binance.com/docs/binance-api/spot/index/#account-information-user_data
Here's the fetch
function I used:
async function getJson(url = null, requestOptions = null) {
return fetch(url, requestOptions)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
} else {
const jsoned = response.json();
return jsoned;
// NOTE:
// response.json().then(data => {
// → do something with your data
// });
//
}
})
.catch(function (error) {
console.log(error);
});
}
Here's the sha256 function I was able to make myself using some of the Mozilla documentation on the SubtleCrypto Object (Crypto Web API). It returns the same result than the one from the dependency.
async function sha256(key, message) {
// Step 1
// encode as (utf-8) Uint8Array
const msgUint8_key = new TextEncoder().encode(key);
// encode as (utf-8) Uint8Array
const msgUint8_message = new TextEncoder().encode(message);
// Step 2
const importedKey = await crypto.subtle.importKey('raw', msgUint8_key, {
name: 'HMAC',
hash: 'SHA-256'
}, true, ['sign']);
// Step 3
const signedKey = await crypto.subtle.sign('HMAC', importedKey, msgUint8_message);
// convert buffer to byte array
const hashArray = Array.from(new Uint8Array(signedKey));
// convert bytes to hex string
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
For those looking for a more 100 % Vanilla solution to this last function: