@qso-soft/shared
Version:
Shared library for QSO-soft
229 lines • 8.87 kB
JavaScript
import { createHmac } from 'crypto';
import axios from 'axios';
import { okx } from 'ccxt';
import { AMOUNT_IS_TOO_LOW_ERROR, OKX_WL_ERROR } from '../../constants';
import { getRandomNumber, prepareProxy, retry, sleep } from '../../helpers';
import { OKX_FEE_NETWORK_MAP, OKX_NETWORK_MAP } from '../../modules/okx-withdraw/constants';
const ON_CHAIN = '4';
const INTERNAL_TRANSFER = '6';
const OKX_DOMAIN = 'https://www.okx.com';
export class Okx {
static { this.instance = null; }
constructor({ logger, random, amount, hideExtraLogs }) {
this.logger = logger;
this.secrets = this.getAccountOKX();
this.random = random;
this.amount = amount;
this.hideExtraLogs = hideExtraLogs || false;
this.okxController = this.setOkxController();
}
static getInstance(props) {
if (!Okx.instance) {
Okx.instance = new Okx(props);
}
return Okx.instance;
}
getChainName(token, network) {
return `${token}-${network}`;
}
getWithdrawAmount() {
if (this.amount) {
return `${this.amount}`;
}
if (!this.random) {
return '';
}
const [randomFrom, randomTo] = this.random.withdrawToAmount;
return getRandomNumber([randomFrom, randomTo]).toFixed(5);
}
setOkxController() {
const okxController = new okx({
...this.secrets,
enableRateLimit: true,
});
const proxy = QsoGlobal.OKX.proxy;
if (proxy && QsoGlobal.settings.useProxy) {
const proxyObject = prepareProxy({
proxy,
proxy_type: 'HTTP',
});
if (proxyObject) {
okxController.https_proxy = proxyObject.url;
}
}
return okxController;
}
async authGuard() {
try {
this.okxController.checkRequiredCredentials();
}
catch (error) {
const e = error;
this.logger?.info(e.message, {
action: 'authGuard',
status: 'failed',
});
throw new Error(e.message);
}
}
async checkNetConnection(token, network, sleepTime = 300000) {
let isAvailable = false;
while (!isAvailable) {
const networkInfo = await this.okxController.fetchCurrencies();
isAvailable = networkInfo?.[token]?.networks?.[network]?.info.canWd;
if (!isAvailable) {
this.logger?.error(`Withdraw is unavailable for this moment. Next attempt will be in ${sleepTime / 60} minutes`, {
action: 'checkNetConnection',
status: 'failed',
});
await sleep(sleepTime);
}
}
}
async getWithdrawFee(token, network, inputNetwork) {
try {
const feesData = (await this.okxController.fetchDepositWithdrawFees([token]));
const tokenNetworks = feesData[token]?.networks;
const feeNetwork = OKX_FEE_NETWORK_MAP[inputNetwork];
const feeInfo = tokenNetworks?.[feeNetwork] || tokenNetworks?.[network] || tokenNetworks?.[token];
if (!feeInfo) {
throw new Error('No fee info');
}
return feeInfo.withdraw.fee;
}
catch (error) {
const withdrawFees = this.random?.withdrawFees;
if (!withdrawFees) {
this.logger?.info(`${error}. withdrawFees can not be empty.`, {
action: 'getWithdrawFee',
status: 'failed',
});
}
this.logger?.info(`${error}. Script will use withdraw fee from config - ${withdrawFees}.`, {
action: 'getWithdrawFee',
status: 'in progress',
});
return withdrawFees;
}
}
async execWithdraw({ walletAddress, token, network, minAmount }) {
const logTemplate = {
action: 'execWithdraw',
status: 'in progress',
};
const lowAmountErrMessage = 'Withdrawal amount is lower than the lower limit';
try {
await this.authGuard();
const okxNetwork = OKX_NETWORK_MAP[network];
const chainName = this.getChainName(token, okxNetwork);
const withdrawFee = await this.getWithdrawFee(token, okxNetwork, network);
const amount = `${+this.getWithdrawAmount() + withdrawFee}`;
if (minAmount && +amount < minAmount) {
throw new Error(lowAmountErrMessage);
}
this.logger?.info(`Withdrawing ${amount} ${token} in ${chainName}`, logTemplate);
await retry({
callback: () => this.okxController.withdraw(token, amount, walletAddress, {
toAddress: walletAddress,
chainName,
dest: ON_CHAIN,
fee: withdrawFee,
pwd: '-',
amt: amount,
network: okxNetwork,
}),
baseDelayMs: 5,
maxAttempts: 1,
});
this.logger?.success(`${amount} ${token} were send. We are waiting for the withdrawal from OKX, relax...`, {
...logTemplate,
status: 'succeeded',
});
}
catch (e) {
const errorMessage = e?.message ?? 'unknown error';
const whiteListError = errorMessage.includes('address is not allowlisted');
if (whiteListError) {
throw new Error(OKX_WL_ERROR);
}
if (errorMessage.includes(lowAmountErrMessage)) {
throw new Error(AMOUNT_IS_TOO_LOW_ERROR);
}
throw new Error(errorMessage);
}
}
getAccountOKX() {
const accountSecret = QsoGlobal.OKX.accounts[QsoGlobal.OKX.accountName];
if (!accountSecret) {
throw new Error('OKX account was not found');
}
return accountSecret;
}
getSignature({ timeStamp, method, requestPath, body = '' }) {
const message = timeStamp + method.toUpperCase() + requestPath + body;
const hmac = createHmac('sha256', this.secrets.secret);
hmac.update(message);
return hmac.digest('base64');
}
getAuthHeaders({ requestPath, method = 'GET', body = '' }) {
const timeStamp = new Date().toISOString();
return {
'OK-ACCESS-TIMESTAMP': timeStamp,
'OK-ACCESS-KEY': this.secrets.apiKey,
'OK-ACCESS-SIGN': this.getSignature({ timeStamp, method, requestPath, body }),
'OK-ACCESS-PASSPHRASE': this.secrets.password,
};
}
async transferBalanceFromSubToMain({ symbol, amount, subAccName }) {
const requestPath = '/api/v5/asset/transfer';
const body = {
ccy: symbol,
amt: amount,
from: INTERNAL_TRANSFER,
to: INTERNAL_TRANSFER,
type: '2',
subAcct: subAccName,
};
const headers = this.getAuthHeaders({ requestPath, method: 'POST', body: JSON.stringify(body) });
const response = await axios.post(`${OKX_DOMAIN}${requestPath}`, body, {
headers,
});
const { amt, ccy } = response.data.data[0];
this.logger?.success(`${amt} ${ccy} has been sent from sub-account [${subAccName}] to main account`, {
status: 'succeeded',
});
}
async getSubAccountBalaces(subAccName) {
const requestPath = `/api/v5/asset/subaccount/balances?subAcct=${subAccName}`;
const response = await axios.get(`${OKX_DOMAIN}${requestPath}`, {
headers: this.getAuthHeaders({ requestPath }),
});
return response.data.data;
}
async transferFromSubAccs() {
const subAccounts = await this.okxController.privateGetUsersSubaccountList();
if (!subAccounts.data) {
throw new Error('We can not get sub accounts from OKX');
}
if (subAccounts.data.length === 0) {
return;
}
for (const { label: subAccName } of subAccounts.data) {
try {
const balances = await this.getSubAccountBalaces(subAccName);
if (!balances.length) {
continue;
}
for (const { availBal, ccy } of balances) {
await this.transferBalanceFromSubToMain({ amount: availBal, subAccName, symbol: ccy });
await sleep(1);
}
}
catch (error) {
const err = error;
this.logger?.error(`${err.message}`);
}
}
}
}
//# sourceMappingURL=okx.js.map