TypeScript version of api.js (+ solution with node-fetch)
Maxim-Mazurok opened this issue · 3 comments
Maxim-Mazurok commented
It'd be great to have TS version of Api.js.
Here's my stab at it:
import CryptoJS from "crypto-js";
import fetch from "node-fetch";
import { parse, stringify } from "qs";
function createNonce() {
let s = "";
const length = 32;
do {
s += Math.random().toString(36).slice(2);
} while (s.length < length);
s = s.slice(0, Math.max(0, length));
return s;
}
const getAuthHeader = (
apiKey: string,
apiSecret: string,
time: string,
nonce: string,
organizationId = "",
request: {
method: string;
path: string;
query: Record<string, unknown> | string;
body: unknown;
},
) => {
const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, apiSecret);
hmac.update(apiKey);
hmac.update("\0");
hmac.update(time);
hmac.update("\0");
hmac.update(nonce);
hmac.update("\0");
hmac.update("\0");
if (organizationId) hmac.update(organizationId);
hmac.update("\0");
hmac.update("\0");
hmac.update(request.method);
hmac.update("\0");
hmac.update(request.path);
hmac.update("\0");
if (request.query)
hmac.update(
typeof request.query === "string"
? request.query
: stringify(request.query),
);
if (request.body) {
hmac.update("\0");
hmac.update(
typeof request.body === "string"
? request.body
: JSON.stringify(request.body),
);
}
return apiKey + ":" + hmac.finalize().toString(CryptoJS.enc.Hex);
};
export default class Api {
private readonly locale: string;
private readonly host: string;
private readonly key: string;
private readonly secret: string;
private readonly org: string;
private localTimeDiff?: number;
constructor({
locale,
apiHost,
apiKey,
apiSecret,
orgId,
}: {
locale?: string;
apiHost: string;
apiKey: string;
apiSecret: string;
orgId: string;
}) {
this.locale = locale || "en";
this.host = apiHost;
this.key = apiKey;
this.secret = apiSecret;
this.org = orgId;
this.getTime();
}
async getTime() {
const response = (await (
await fetch(this.host + "/api/v2/time")
).json()) as { serverTime: number };
this.localTimeDiff = response.serverTime - Date.now();
return response;
}
private async apiCall(
method: "GET" | "POST" | "PUT" | "DELETE",
path: string,
options?: {
query: Record<string, unknown>;
body?: unknown;
time?: number;
},
) {
let query = {},
body,
time;
if (options) ({ query, body, time } = options);
if (this.localTimeDiff === undefined) {
throw new Error("Get server time first .getTime()");
}
// query in path
const [pathOnly, pathQuery] = path.split("?");
if (pathQuery) query = { ...parse(pathQuery), ...query };
const nonce = createNonce();
const timestamp = (time || Date.now() + this.localTimeDiff).toString();
return (
await fetch(`${this.host}${pathOnly}?${stringify(query)}`, {
method: method,
headers: {
"X-Request-Id": nonce,
"X-User-Agent": "NHNodeClient",
"X-Time": timestamp,
"X-Nonce": nonce,
"X-User-Lang": this.locale,
"X-Organization-Id": this.org,
"X-Auth": getAuthHeader(
this.key,
this.secret,
timestamp,
nonce,
this.org,
{
method,
path: pathOnly,
query,
body,
},
),
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body: body as any,
})
).json();
}
get(
path: string,
options?: {
query: Record<string, unknown>;
body?: unknown;
time?: number;
},
) {
return this.apiCall("GET", path, options);
}
post(
path: string,
options?: {
query: Record<string, unknown>;
body?: unknown;
time?: number;
},
) {
return this.apiCall("POST", path, options);
}
put(
path: string,
options?: {
query: Record<string, unknown>;
body?: unknown;
time?: number;
},
) {
return this.apiCall("PUT", path, options);
}
delete(
path: string,
options?: {
query: Record<string, unknown>;
body?: unknown;
time?: number;
},
) {
return this.apiCall("DELETE", path, options);
}
}
Then I use it like so:
await api.getTime(); // get server time - required
const { totalBalance } = (await api.get(
"/main/api/v2/accounting/account2/BTC",
)) as { totalBalance: number }; // get balance settings
console.log(`NiceHash total balance: ${totalBalance} BTC`);
xrado commented
@Maxim-Mazurok thanks for your contribution, we will include it in the repo
Maxim-Mazurok commented
No worries, @xrado :) A bit concerned that it was closed without being included, in the same situation I will most likely forget to action on the issue that was closed, but it's up to you, I just wanted to put it somewhere so people can find it if need be. Cheers!
xrado commented
@Maxim-Mazurok it is included now. Btw in node.js 18+ fetch is already included, no need for import