Initial commit

This commit is contained in:
Aleksandr Kraiz
2022-04-20 23:41:04 +04:00
commit 106b702d21
118 changed files with 25394 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import BigNumber from 'bignumber.js';
import { FILL_ORDERS_GAS_LIMIT } from '../constants';
import calculateNetworkFeeInFeeAsset from './calculateNetworkFeeInFeeAsset';
import calculateOrionFeeInFeeAsset from './calculateOrionFeeInFeeAsset';
const calculateFeeInFeeAsset = (
amount: BigNumber.Value,
feeAssetPriceInOrn: BigNumber.Value,
baseAssetPriceInOrn: BigNumber.Value,
baseCurrencyPriceInOrn: BigNumber.Value,
gasPriceGwei: BigNumber.Value,
feePercent: BigNumber.Value,
) => {
const orionFeeInFeeAsset = calculateOrionFeeInFeeAsset(
amount,
feeAssetPriceInOrn,
baseAssetPriceInOrn,
feePercent,
);
const networkFeeInFeeAsset = calculateNetworkFeeInFeeAsset(
gasPriceGwei,
FILL_ORDERS_GAS_LIMIT,
baseCurrencyPriceInOrn,
feeAssetPriceInOrn,
);
return {
orionFeeInFeeAsset,
networkFeeInFeeAsset,
totalFeeInFeeAsset: new BigNumber(orionFeeInFeeAsset)
.plus(networkFeeInFeeAsset)
.toString(),
};
};
export default calculateFeeInFeeAsset;

View File

@@ -0,0 +1,13 @@
import BigNumber from 'bignumber.js';
import { ethers } from 'ethers';
import { NATIVE_CURRENCY_PRECISION } from '../constants/precisions';
export default function calculateNetworkFee(
gasPriceGwei: BigNumber.Value,
gasLimit: BigNumber.Value,
) {
const networkFeeGwei = new BigNumber(gasPriceGwei).multipliedBy(gasLimit);
const bn = new BigNumber(ethers.utils.parseUnits(networkFeeGwei.toString(), 'gwei').toString());
return bn.div(new BigNumber(10).pow(NATIVE_CURRENCY_PRECISION)).toString();
}

View File

@@ -0,0 +1,22 @@
import BigNumber from 'bignumber.js';
import calculateNetworkFee from './calculateNetworkFee';
const calculateNetworkFeeInFeeAsset = (
gasPriceGwei: BigNumber.Value,
gasLimit: BigNumber.Value,
baseCurrencyPriceInOrn: BigNumber.Value,
feeAssetPriceInOrn: BigNumber.Value,
) => {
const networkFee = calculateNetworkFee(gasPriceGwei, gasLimit);
const networkFeeInOrn = new BigNumber(networkFee).multipliedBy(baseCurrencyPriceInOrn);
const networkFeeInFeeAsset = networkFeeInOrn
.multipliedBy(
new BigNumber(1)
.div(feeAssetPriceInOrn),
);
return networkFeeInFeeAsset.toString();
};
export default calculateNetworkFeeInFeeAsset;

View File

@@ -0,0 +1,16 @@
import BigNumber from 'bignumber.js';
export default function calculateOrionFeeInFeeAsset(
amount: BigNumber.Value,
feeAssetPriceInOrn: BigNumber.Value,
baseAssetPriceInOrn: BigNumber.Value,
feePercent: BigNumber.Value,
) {
const result = new BigNumber(amount)
.multipliedBy(new BigNumber(feePercent).div(100))
.multipliedBy(baseAssetPriceInOrn)
.multipliedBy(new BigNumber(1).div(feeAssetPriceInOrn))
.toString();
return result;
}

26
src/utils/checkIsToken.ts Normal file
View File

@@ -0,0 +1,26 @@
/* eslint-disable camelcase */
import { ethers } from 'ethers';
import invariant from 'tiny-invariant';
import { ERC20__factory } from '../artifacts/contracts';
const checkIsToken = async (address: string, provider?: ethers.providers.Provider) => {
invariant(provider, 'No provider for token checking');
const tokenContract = ERC20__factory.connect(address, provider);
try {
const results = await Promise.all(
[
tokenContract.name(),
tokenContract.symbol(),
tokenContract.decimals(),
tokenContract.totalSupply(),
tokenContract.balanceOf(ethers.constants.AddressZero),
],
);
return Boolean(results);
} catch (err) {
return false;
}
};
export default checkIsToken;

View File

@@ -0,0 +1,14 @@
import BigNumber from 'bignumber.js';
import { ethers } from 'ethers';
/**
* Converts normalized blockchain ("machine-readable") number to denormalized ("human-readable") number.
* @param input Any blockchain-normalized numeric value
* @param decimals Blockchain asset precision
* @returns BigNumber
*/
export default function denormalizeNumber(input: ethers.BigNumber, decimals: BigNumber.Value) {
const decimalsBN = new BigNumber(decimals);
if (!decimalsBN.isInteger()) throw new Error(`Decimals '${decimals.toString()}' is not an integer`);
return new BigNumber(input.toString()).div(new BigNumber(10).pow(decimalsBN));
}

View File

@@ -0,0 +1,12 @@
import rand from 'csprng';
import { ethers } from 'ethers';
const generateSecret = () => {
const RANDOM_RADIX = 16;
const RANDOM_BITS = 256;
const random = rand(RANDOM_BITS, RANDOM_RADIX);
const secret = ethers.utils.keccak256(`0x${random}`);
return secret;
};
export default generateSecret;

View File

@@ -0,0 +1,19 @@
import { ethers } from 'ethers';
import { Source } from '../types';
export default function getAvailableFundsSources(
expenseType: 'amount' | 'network_fee' | 'orion_fee',
assetAddress: string,
route: 'aggregator' | 'orion_pool',
): Source[] {
switch (route) {
case 'aggregator':
if (assetAddress === ethers.constants.AddressZero) return ['exchange']; // We can't take native crypto from wallet
return ['exchange', 'wallet']; // We can take any token amount from exchange + wallet. Order is important!
case 'orion_pool':
if (expenseType === 'network_fee') return ['wallet']; // Network fee is always taken from wallet
return ['exchange', 'wallet']; // We can take any token amount from exchange + wallet (specify 'value' for 'orion_pool'). Order is important!
default:
throw new Error('Unknown route item');
}
}

57
src/utils/getBalance.ts Normal file
View File

@@ -0,0 +1,57 @@
import BigNumber from 'bignumber.js';
import { ethers } from 'ethers';
import { contracts, utils } from '..';
import { INTERNAL_ORION_PRECISION, NATIVE_CURRENCY_PRECISION } from '../constants';
import { OrionAggregator } from '../services/OrionAggregator';
export default async function getBalance(
orionAggregator: OrionAggregator,
asset: string,
assetAddress: string,
walletAddress: string,
exchangeContract: contracts.Exchange,
provider: ethers.providers.Provider,
) {
const assetIsNativeCryptocurrency = assetAddress === ethers.constants.AddressZero;
let assetWalletBalance: ethers.BigNumber | undefined;
let assetAllowance: ethers.BigNumber | undefined;
let denormalizedAssetInWalletBalance: BigNumber | undefined;
let denormalizedAssetInAllowance: BigNumber | undefined;
if (!assetIsNativeCryptocurrency) {
const assetContract = contracts.ERC20__factory.connect(assetAddress, provider);
const assetDecimals = await assetContract.decimals();
assetWalletBalance = await assetContract.balanceOf(walletAddress);
assetAllowance = await assetContract.allowance(walletAddress, exchangeContract.address);
denormalizedAssetInWalletBalance = utils.denormalizeNumber(assetWalletBalance, assetDecimals);
denormalizedAssetInAllowance = utils.denormalizeNumber(assetAllowance, assetDecimals);
} else {
assetWalletBalance = await provider.getBalance(walletAddress);
denormalizedAssetInWalletBalance = utils.denormalizeNumber(assetWalletBalance, NATIVE_CURRENCY_PRECISION);
denormalizedAssetInAllowance = denormalizedAssetInWalletBalance; // For native crypto no allowance is needed
}
const assetContractBalance = await exchangeContract.getBalance(assetAddress, walletAddress);
const denormalizedAssetInContractBalance = utils.denormalizeNumber(assetContractBalance, INTERNAL_ORION_PRECISION);
// const denormalizedAssetWalletBalanceAvailable = BigNumber.min(denormalizedAssetInAllowance, denormalizedAssetInWalletBalance);
const denormalizedAssetLockedBalance = await orionAggregator.getLockedBalance(walletAddress, asset);
// const denormalizedAssetAvailableBalance = denormalizedAssetInContractBalance
// .plus(denormalizedAssetWalletBalanceAvailable)
// .minus(denormalizedAssetLockedBalance[asset] ?? 0);
// const denormalizedAssetImaginaryBalance = denormalizedAssetInContractBalance
// .plus(denormalizedAssetInWalletBalance)
// .minus(denormalizedAssetLockedBalance[asset] ?? 0);
return {
exchange: denormalizedAssetInContractBalance.minus(denormalizedAssetLockedBalance[asset] ?? 0),
// imaginary: denormalizedAssetImaginaryBalance,
wallet: denormalizedAssetInWalletBalance,
allowance: denormalizedAssetInAllowance,
// approvedWalletBalance: denormalizedAssetWalletBalanceAvailable,
// available: denormalizedAssetAvailableBalance,
// locked: denormalizedAssetLockedBalance[asset],
};
}

41
src/utils/getBalances.ts Normal file
View File

@@ -0,0 +1,41 @@
import BigNumber from 'bignumber.js';
import { ethers } from 'ethers';
import { contracts } from '..';
import { OrionAggregator } from '../services/OrionAggregator';
import getBalance from './getBalance';
export default async (
balancesRequired: Partial<Record<string, string>>,
orionAggregator: OrionAggregator,
walletAddress: string,
exchangeContract: contracts.Exchange,
provider: ethers.providers.Provider,
) => {
const balances = await Promise.all(
Object.entries(balancesRequired)
.map(async ([asset, assetAddress]) => {
if (!assetAddress) throw new Error(`Asset address of ${asset} not found`);
const balance = await getBalance(
orionAggregator,
asset,
assetAddress,
walletAddress,
exchangeContract,
provider,
);
return {
asset,
amount: balance,
};
}),
);
return balances.reduce<Partial<Record<string, {
exchange: BigNumber,
wallet: BigNumber,
allowance: BigNumber,
}>>>((prev, curr) => ({
...prev,
[curr.asset]: curr.amount,
}), {});
};

26
src/utils/getSwapPair.ts Normal file
View File

@@ -0,0 +1,26 @@
// exact spend
// n X -> USDT - пара X-USDT (продажа амаунта на паре X-USDT)
// n USDT -> X - пара X-USDT (покупка на стоимость на паре X-USDT)
// n X -> Y - пара X-Y (продажа амаунта на паре X-Y)
// exact eceive
// X -> n USDT - пара X-USDT (продажа на стоимость на паре X-USDT)
// USDT -> n X - пара X-USDT (покупка амаунта на паре X-USDT)
// X -> n Y - пара Y-X (покупка амаунта на паре Y-X)
export default function getSwapPair(
assetIn: string | null | undefined,
assetOut: string | null | undefined,
type: 'exactSpend' | 'exactReceive',
) {
if (!assetIn || !assetOut) {
return undefined;
}
if (assetOut === 'USDT') return `${assetIn}-USDT`;
if (assetIn === 'USDT') return `${assetOut}-USDT`;
if (type === 'exactSpend') return `${assetIn}-${assetOut}`;
return `${assetOut}-${assetIn}`;
}

14
src/utils/getSwapSide.ts Normal file
View File

@@ -0,0 +1,14 @@
export default function getSwapSide(
assetIn: string | null | undefined,
assetOut: string | null | undefined,
type: 'exactSpend' | 'exactReceive',
) {
if (!assetIn || !assetOut) {
return undefined;
}
if (assetOut === 'USDT') return 'SELL';
if (assetIn === 'USDT') return 'BUY';
if (type === 'exactSpend') return 'SELL';
return 'BUY';
}

35
src/utils/hashOrder.ts Normal file
View File

@@ -0,0 +1,35 @@
import { ethers } from 'ethers';
import { Order } from '../types';
const hashOrder = (order: Order) => ethers.utils.solidityKeccak256(
[
'uint8',
'address',
'address',
'address',
'address',
'address',
'uint64',
'uint64',
'uint64',
'uint64',
'uint64',
'uint8',
],
[
'0x03',
order.senderAddress,
order.matcherAddress,
order.baseAsset,
order.quoteAsset,
order.matcherFeeAsset,
order.amount,
order.price,
order.matcherFee,
order.nonce,
order.expiration,
order.buySide ? '0x01' : '0x00',
],
);
export default hashOrder;

17
src/utils/httpError.ts Normal file
View File

@@ -0,0 +1,17 @@
export default class HttpError extends Error {
public code: number;
public errorMessage: string |null;
public statusText: string;
public type: string;
constructor(code:number, message:string|null, type: string, statusText:string) {
super(message || '');
this.errorMessage = message;
this.type = type;
this.statusText = statusText;
this.code = code;
}
}

17
src/utils/index.ts Normal file
View File

@@ -0,0 +1,17 @@
export { default as calculateFeeInFeeAsset } from './calculateFeeInFeeAsset';
export { default as calculateNetworkFee } from './calculateNetworkFee';
export { default as calculateNetworkFeeInFeeAsset } from './calculateNetworkFeeInFeeAsset';
export { default as calculateOrionFeeInFeeAsset } from './calculateOrionFeeInFeeAsset';
export { default as hashOrder } from './hashOrder';
export { default as checkIsToken } from './checkIsToken';
export { default as signOrderPersonal } from './signOrderPersonal';
export { default as generateSecret } from './generateSecret';
export { default as isValidChainId } from './isValidChainId';
export { default as denormalizeNumber } from './denormalizeNumber';
export { default as normalizeNumber } from './normalizeNumber';
export { default as getSwapPair } from './getSwapPair';
export { default as getSwapSide } from './getSwapSide';
export { default as HttpError } from './httpError';
export * from './typeHelpers';

View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
import { SupportedChainId } from '../types';
const isValidChainId = (chainId: string): chainId is SupportedChainId => {
const { success } = z.nativeEnum(SupportedChainId).safeParse(chainId);
return success;
};
export default isValidChainId;

View File

@@ -0,0 +1,25 @@
import BigNumber from 'bignumber.js';
import { ethers } from 'ethers';
/**
* Converts denormalized ("human-readable") number to normalized ("machine-readable") number.
* @param input Any numeric value
* @param decimals Blockchain asset precision
* @param roundingMode Rounding mode
* @returns ethers.BigNumber
*/
export default function normalizeNumber(
input: BigNumber.Value,
decimals: BigNumber.Value,
roundingMode: BigNumber.RoundingMode,
) {
const decimalsBN = new BigNumber(decimals);
if (!decimalsBN.isInteger()) throw new Error(`Decimals '${decimals.toString()}' is not an integer`);
const inputBN = new BigNumber(input);
return ethers.BigNumber.from(
inputBN
.multipliedBy(new BigNumber(10).pow(decimals))
.integerValue(roundingMode)
.toString(),
);
}

View File

@@ -0,0 +1,33 @@
import { ethers } from 'ethers';
import { Order } from '../types';
const { arrayify, joinSignature, splitSignature } = ethers.utils;
const signOrderPersonal = async (order: Order, signer: ethers.Signer) => {
const message = ethers.utils.solidityKeccak256(
[
'string', 'address', 'address', 'address', 'address',
'address', 'uint64', 'uint64', 'uint64', 'uint64', 'uint64', 'uint8',
],
[
'order',
order.senderAddress,
order.matcherAddress,
order.baseAsset,
order.quoteAsset,
order.matcherFeeAsset,
order.amount,
order.price,
order.matcherFee,
order.nonce,
order.expiration,
order.buySide,
],
);
const signature = await signer.signMessage(arrayify(message));
// NOTE: metamask broke sig.v value and we fix it in next line
return joinSignature(splitSignature(signature));
};
export default signOrderPersonal;

71
src/utils/typeHelpers.ts Normal file
View File

@@ -0,0 +1,71 @@
type WithReason = {
reason: string;
}
type WithCodeError = Error & {
code: number;
}
type WithMessage = {
message: string;
}
type WithDataError = Error & {
data: Record<string, unknown>;
}
type WithError ={
error: Record<string | number | symbol, unknown>;
}
export const makePartial = <Key extends string | number | symbol, Value>(value: Record<Key, Value>): Partial<Record<Key, Value>> => value;
export function isUnknownObject(x: unknown): x is {
[key in PropertyKey]: unknown
} {
return x !== null && typeof x === 'object';
}
export function isKeyOfObject<T>(
key: string | number | symbol,
obj: T,
): key is keyof T {
return key in obj;
}
export function hasProp<T extends Record<string, unknown>, K extends PropertyKey>(
obj: T,
key: K,
): obj is T & Record<K, unknown> {
return key in obj;
}
export function isWithCode(candidate: unknown): candidate is WithCodeError {
if (!isUnknownObject(candidate)) return false;
const hasCodeProperty = hasProp(candidate, 'code') && typeof candidate.code === 'number';
return hasCodeProperty;
}
export function isWithReason(candidate: unknown): candidate is WithReason {
if (!isUnknownObject(candidate)) return false;
const hasReasonProperty = hasProp(candidate, 'reason') && typeof candidate.reason === 'string';
return hasReasonProperty;
}
export function isWithMessage(candidate: unknown): candidate is WithMessage {
if (!isUnknownObject(candidate)) return false;
const hasMessageProperty = hasProp(candidate, 'message') && typeof candidate.message === 'string';
return hasMessageProperty;
}
export function isWithError(candidate: unknown): candidate is WithError {
if (!isUnknownObject(candidate)) return false;
const hasErrorProperty = hasProp(candidate, 'error') && isUnknownObject(candidate.error);
return hasErrorProperty;
}
export function isWithData(candidate: unknown): candidate is WithDataError {
if (!isUnknownObject(candidate)) return false;
const hasDataProperty = hasProp(candidate, 'data') && typeof candidate.data === 'object';
return hasDataProperty;
}