import { toUtf8 } from '@cosmjs/encoding';
import { Coin } from '@cosmjs/launchpad';
import { Uint53 } from '@cosmjs/math';
import { CyberClient } from '@cybercongress/cyber-js';
import { QueryLiquidityPoolResponse } from '@cybercongress/cyber-js/build/codec/tendermint/liquidity/v1beta1/query';
import BigNumber from 'bignumber.js';
import { QueryValidatorsResponse } from 'cosmjs-types/cosmos/staking/v1beta1/query';
import Long from 'long';
import { calculatePairAmount } from 'src/pages/teleport/swap/utils';
import { ObjKeyValue } from 'src/types/data';
import { CYBER, DEFAULT_GAS_LIMITS } from 'src/utils/config';
import coinDecimalsConfig from 'src/utils/configToken';
import { authAccounts } from 'src/utils/search/utils';
import { reduceBalances } from 'src/utils/utils';
const coinFunc = (amount: number, denom: string): Coin => {
return { denom, amount: new BigNumber(amount).toString(10) };
};
const checkDenom = (denom: string) => {
if (denom === 'millivolt' || denom === 'milliampere') {
return coinDecimalsConfig[denom].coinDecimals;
}
return 0;
};
const MILLISECONDS_IN_SECOND = 1000;
const BASE_VESTING_TIME = 86401;
const BASE_INVESTMINT_AMOUNT_VOLT = 1 * 10 ** 9;
const BASE_INVESTMINT_AMOUNT_AMPERE = 100 * 10 ** 6;
const MAX_SLOTS = 16;
const POOL_TYPE_INDEX = 1;
const SWAP_FEE_RATE = 0.003;
class Soft3MessageFactory {
private readonly senderAddress: string;
protected readonly queryClient: CyberClient | undefined;
static denom() {
return CYBER.DENOM_CYBER;
}
static fee(fee: number | string | undefined = DEFAULT_GAS_LIMITS.toString()) {
let usedFee;
if (typeof fee === 'number') {
usedFee = new BigNumber(DEFAULT_GAS_LIMITS)
.multipliedBy(fee)
.dp(0, BigNumber.ROUND_CEIL)
.toString();
} else {
usedFee = fee;
}
return {
amount: [],
gas: usedFee,
};
}
static liquidDenom() {
return CYBER.DENOM_LIQUID_TOKEN;
}
constructor(senderAddress: string, queryClient?: CyberClient) {
this.senderAddress = senderAddress;
this.queryClient = queryClient;
}
private async checkFreeSlotMint() {
const HALF_MAX_SLOTS = MAX_SLOTS / 2;
const dataAuthAccounts = await authAccounts(this.senderAddress);
if (!dataAuthAccounts?.result?.value?.vesting_periods) {
return true;
}
const { vesting_periods: vestingPeriods, start_time: startTime } =
dataAuthAccounts.result.value;
if (vestingPeriods.length < HALF_MAX_SLOTS) {
return true;
}
const slotData = [];
let length = parseFloat(startTime);
vestingPeriods.forEach((item) => {
length += parseFloat(item.length);
const lengthMs = length * MILLISECONDS_IN_SECOND;
const d = new Date();
if (lengthMs >= Date.parse(d)) {
slotData.push(item);
}
});
if (slotData.length >= HALF_MAX_SLOTS) {
return false;
}
return true;
}
public async investmint(
amount: Coin,
resource: 'millivolt' | 'milliampere',
length: number
) {
const isMint = await this.checkFreeSlotMint();
if (!isMint) {
return undefined;
}
const minAmountMint =
resource === 'milliampere'
? BASE_INVESTMINT_AMOUNT_AMPERE
: BASE_INVESTMINT_AMOUNT_VOLT;
if (new BigNumber(amount.amount).comparedTo(minAmountMint) < 0) {
return undefined;
}
return {
typeUrl: '/cyber.resources.v1beta1.MsgInvestmint',
value: {
neuron: this.senderAddress,
amount,
resource,
length: Long.fromString(
new Uint53(length * BASE_VESTING_TIME).toString()
),
},
};
}
public execute(
contract: string,
msg: Record<string, unknown>,
funds?: readonly Coin[]
) {
return {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: {
sender: this.senderAddress,
contract,
msg: toUtf8(JSON.stringify(msg)),
funds: [...(funds || [])],
},
};
}
public async delegateTokens(amount: Coin) {
const address = await this.getValidatorAddress();
return {
typeUrl: '/cosmos.staking.v1beta1.MsgDelegate',
value: {
delegatorAddress: this.senderAddress,
validatorAddress: address,
amount,
},
};
}
private async getValidatorAddress() {
if (!this.queryClient) {
return undefined;
}
const result = (await this.queryClient.validators(
'BOND_STATUS_BONDED'
)) as QueryValidatorsResponse;
if (!result.validators.length) {
return undefined;
}
const sortedArray = result.validators
.sort(
(itemA, itemB) => parseFloat(itemB.tokens) - parseFloat(itemA.tokens)
)
.slice(10, result.validators.length - 1);
const randomIndex = Math.floor(Math.random() * sortedArray.length);
return sortedArray[randomIndex].operatorAddress;
}
private async getBalPool(poolId: number): Promise<ObjKeyValue | undefined> {
if (!this.queryClient) {
return undefined;
}
const resultPool = (await this.queryClient.pool(
poolId
)) as QueryLiquidityPoolResponse;
const { pool } = resultPool;
if (!pool) {
return undefined;
}
const resultBalances = await this.queryClient.getAllBalances(
pool.reserveAccountAddress
);
if (!resultBalances) {
return undefined;
}
const dataReduceBalances = reduceBalances(resultBalances);
return dataReduceBalances;
}
private async getPrice(
poolId: number,
token: {
offerCoin: Coin;
demandCoinDenom: string;
}
): Promise<string | undefined> {
const { offerCoin, demandCoinDenom } = token;
const getBalPool = await this.getBalPool(poolId);
if (!getBalPool) {
return undefined;
}
const tokenAPoolAmount = getBalPool[offerCoin.denom] || 0;
const tokenBPoolAmount = getBalPool[demandCoinDenom] || 0;
if (
!tokenAPoolAmount ||
!tokenBPoolAmount ||
Number(offerCoin.amount) === 0
) {
return undefined;
}
const state = {
tokenA: offerCoin.denom,
tokenB: demandCoinDenom,
tokenAPoolAmount,
tokenBPoolAmount,
coinDecimalsA: checkDenom(offerCoin.denom),
coinDecimalsB: checkDenom(demandCoinDenom),
isReverse: false,
};
const { price } = calculatePairAmount(Number(offerCoin.amount), state);
const exp = new BigNumber(10).pow(18).toString();
const convertSwapPrice = new BigNumber(price.toNumber())
.multipliedBy(exp)
.dp(0, BigNumber.ROUND_FLOOR)
.toString(10);
return convertSwapPrice;
}
public async swap(poolId: number, offerCoin: Coin, demandCoinDenom: string) {
const orderPrice = await this.getPrice(poolId, {
offerCoin,
demandCoinDenom,
});
if (!orderPrice) {
return {};
}
const amount = new BigNumber(offerCoin.amount)
.multipliedBy(1 - SWAP_FEE_RATE)
.dp(0, BigNumber.ROUND_CEIL);
const offerCoinFee = coinFunc(
new BigNumber(amount)
.multipliedBy(SWAP_FEE_RATE)
.multipliedBy(0.5)
.dp(0, BigNumber.ROUND_CEIL)
.toNumber(),
offerCoin.denom
);
return {
typeUrl: '/tendermint.liquidity.v1beta1.MsgSwapWithinBatch',
value: {
swapRequesterAddress: this.senderAddress,
poolId,
swapTypeId: POOL_TYPE_INDEX,
offerCoin: {
amount: amount.toString(),
denom: offerCoin.denom,
},
demandCoinDenom,
offerCoinFee,
orderPrice,
},
};
}
}
export default Soft3MessageFactory;