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.
Vanilla JS Solution
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
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. The HMAC SHA256
signature is a
keyed HMAC SHA256
operation. Use your secretKey
as the key and totalParams
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.
The Complete Solution.
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
Other functions I used (it's just a bonus to this answer, you may find them useful)
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: