mirror of
https://github.com/orionprotocol/sdk.git
synced 2026-03-15 22:52:36 +03:00
Semantics improvements
This commit is contained in:
134
src/Unit/Exchange/deposit.ts
Normal file
134
src/Unit/Exchange/deposit.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
|
||||
import getBalances from '../../utils/getBalances.js';
|
||||
import BalanceGuard from '../../BalanceGuard.js';
|
||||
import type Unit from '../index.js';
|
||||
import {
|
||||
DEPOSIT_ERC20_GAS_LIMIT, DEPOSIT_ETH_GAS_LIMIT, INTERNAL_PROTOCOL_PRECISION, NATIVE_CURRENCY_PRECISION,
|
||||
} from '../../constants/index.js';
|
||||
import { denormalizeNumber, normalizeNumber } from '../../utils/index.js';
|
||||
import getNativeCryptocurrencyName from '../../utils/getNativeCryptocurrencyName.js';
|
||||
import { simpleFetch } from 'simple-typed-fetch';
|
||||
|
||||
export type DepositParams = {
|
||||
asset: string
|
||||
amount: BigNumber.Value
|
||||
signer: ethers.Signer
|
||||
unit: Unit
|
||||
}
|
||||
|
||||
export default async function deposit({
|
||||
asset,
|
||||
amount,
|
||||
signer,
|
||||
unit,
|
||||
}: DepositParams) {
|
||||
if (asset === '') throw new Error('Asset can not be empty');
|
||||
|
||||
const amountBN = new BigNumber(amount);
|
||||
if (amountBN.isNaN()) throw new Error(`Amount '${amountBN.toString()}' is not a number`);
|
||||
if (amountBN.lte(0)) throw new Error(`Amount '${amountBN.toString()}' should be greater than 0`);
|
||||
|
||||
const walletAddress = await signer.getAddress();
|
||||
|
||||
const {
|
||||
blockchainService, aggregator, provider, chainId,
|
||||
} = unit;
|
||||
const {
|
||||
exchangeContractAddress,
|
||||
assetToAddress,
|
||||
} = await simpleFetch(blockchainService.getInfo)();
|
||||
|
||||
const nativeCryptocurrency = getNativeCryptocurrencyName(assetToAddress);
|
||||
|
||||
const exchangeContract = Exchange__factory.connect(exchangeContractAddress, provider);
|
||||
const gasPriceWei = await simpleFetch(blockchainService.getGasPriceWei)();
|
||||
|
||||
const assetAddress = assetToAddress[asset];
|
||||
if (assetAddress === undefined) throw new Error(`Asset '${asset}' not found`);
|
||||
|
||||
const balances = await getBalances(
|
||||
{
|
||||
[asset]: assetAddress,
|
||||
[nativeCryptocurrency]: ethers.constants.AddressZero,
|
||||
},
|
||||
aggregator,
|
||||
walletAddress,
|
||||
exchangeContract,
|
||||
provider,
|
||||
);
|
||||
|
||||
const balanceGuard = new BalanceGuard(
|
||||
balances,
|
||||
{
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
provider,
|
||||
signer,
|
||||
);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Amount',
|
||||
asset: {
|
||||
name: asset,
|
||||
address: assetAddress,
|
||||
},
|
||||
amount: amountBN.toString(),
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: ['wallet'],
|
||||
});
|
||||
|
||||
let unsignedTx: ethers.PopulatedTransaction;
|
||||
if (asset === nativeCryptocurrency) {
|
||||
unsignedTx = await exchangeContract.populateTransaction.deposit();
|
||||
unsignedTx.value = normalizeNumber(amount, NATIVE_CURRENCY_PRECISION, BigNumber.ROUND_CEIL);
|
||||
unsignedTx.gasLimit = ethers.BigNumber.from(DEPOSIT_ETH_GAS_LIMIT);
|
||||
} else {
|
||||
unsignedTx = await exchangeContract.populateTransaction.depositAsset(
|
||||
assetAddress,
|
||||
normalizeNumber(amount, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_CEIL),
|
||||
);
|
||||
unsignedTx.gasLimit = ethers.BigNumber.from(DEPOSIT_ERC20_GAS_LIMIT);
|
||||
}
|
||||
|
||||
const transactionCost = ethers.BigNumber.from(unsignedTx.gasLimit).mul(gasPriceWei);
|
||||
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
amount: denormalizedTransactionCost.toString(),
|
||||
sources: ['wallet'],
|
||||
});
|
||||
|
||||
unsignedTx.chainId = parseInt(chainId, 10);
|
||||
unsignedTx.gasPrice = ethers.BigNumber.from(gasPriceWei);
|
||||
unsignedTx.from = walletAddress;
|
||||
|
||||
await balanceGuard.check(true);
|
||||
|
||||
const nonce = await provider.getTransactionCount(walletAddress, 'pending');
|
||||
unsignedTx.nonce = nonce;
|
||||
|
||||
const signedTx = await signer.signTransaction(unsignedTx);
|
||||
try {
|
||||
const txResponse = await provider.sendTransaction(signedTx);
|
||||
console.log(`Deposit tx sent: ${txResponse.hash}. Waiting for confirmation...`);
|
||||
const txReceipt = await txResponse.wait();
|
||||
if (txReceipt.status !== undefined) {
|
||||
console.log('Deposit tx confirmed');
|
||||
} else {
|
||||
console.log('Deposit tx failed');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) throw new Error('e is not an Error');
|
||||
console.error(`Deposit tx failed: ${e.message}`, {
|
||||
unsignedTx,
|
||||
});
|
||||
}
|
||||
}
|
||||
167
src/Unit/Exchange/getSwapInfo.ts
Normal file
167
src/Unit/Exchange/getSwapInfo.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { simpleFetch } from 'simple-typed-fetch';
|
||||
import { NATIVE_CURRENCY_PRECISION, SWAP_THROUGH_ORION_POOL_GAS_LIMIT } from '../../constants/index.js';
|
||||
import type { Aggregator } from '../../services/Aggregator/index.js';
|
||||
import type { BlockchainService } from '../../services/BlockchainService/index.js';
|
||||
|
||||
import { calculateFeeInFeeAsset, denormalizeNumber, getNativeCryptocurrencyName } from '../../utils/index.js';
|
||||
|
||||
export type GetSwapInfoParams = {
|
||||
type: 'exactSpend' | 'exactReceive'
|
||||
assetIn: string
|
||||
assetOut: string
|
||||
amount: BigNumber.Value
|
||||
feeAsset: string
|
||||
blockchainService: BlockchainService
|
||||
aggregator: Aggregator
|
||||
options?: {
|
||||
instantSettlement?: boolean
|
||||
poolOnly?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default async function getSwapInfo({
|
||||
type,
|
||||
assetIn,
|
||||
assetOut,
|
||||
amount,
|
||||
feeAsset,
|
||||
blockchainService,
|
||||
aggregator,
|
||||
options,
|
||||
}: GetSwapInfoParams) {
|
||||
if (amount === '') throw new Error('Amount can not be empty');
|
||||
if (assetIn === '') throw new Error('AssetIn can not be empty');
|
||||
if (assetOut === '') throw new Error('AssetOut can not be empty');
|
||||
if (feeAsset === '') throw new Error('Fee asset can not be empty');
|
||||
|
||||
const amountBN = new BigNumber(amount);
|
||||
if (amountBN.isNaN()) throw new Error(`Amount '${amountBN.toString()}' is not a number`);
|
||||
if (amountBN.lte(0)) throw new Error(`Amount '${amountBN.toString()}' should be greater than 0`);
|
||||
|
||||
const {
|
||||
assetToAddress,
|
||||
} = await simpleFetch(blockchainService.getInfo)();
|
||||
const nativeCryptocurrencyName = getNativeCryptocurrencyName(assetToAddress);
|
||||
|
||||
const feeAssets = await simpleFetch(blockchainService.getTokensFee)();
|
||||
const pricesInOrn = await simpleFetch(blockchainService.getPrices)();
|
||||
const gasPriceWei = await simpleFetch(blockchainService.getGasPriceWei)();
|
||||
|
||||
const gasPriceGwei = ethers.utils.formatUnits(gasPriceWei, 'gwei').toString();
|
||||
|
||||
const assetInAddress = assetToAddress[assetIn];
|
||||
if (assetInAddress === undefined) throw new Error(`Asset '${assetIn}' not found`);
|
||||
const feeAssetAddress = assetToAddress[feeAsset];
|
||||
if (feeAssetAddress === undefined) {
|
||||
throw new Error(`Fee asset '${feeAsset}' not found. Available assets: ${Object.keys(feeAssets).join(', ')}`);
|
||||
}
|
||||
|
||||
const swapInfo = await simpleFetch(aggregator.getSwapInfo)(
|
||||
type,
|
||||
assetIn,
|
||||
assetOut,
|
||||
amountBN.toString(),
|
||||
options?.instantSettlement,
|
||||
options?.poolOnly !== undefined && options.poolOnly
|
||||
? 'pools'
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const { exchanges: swapExchanges } = swapInfo;
|
||||
const { factories } = await simpleFetch(blockchainService.getPoolsConfig)();
|
||||
const poolExchangesList = factories !== undefined ? Object.keys(factories) : [];
|
||||
const [firstSwapExchange] = swapExchanges;
|
||||
|
||||
// if (swapInfo.type === 'exactReceive' && amountBN.lt(swapInfo.minAmountOut)) {
|
||||
// throw new Error(`Amount is too low. Min amountOut is ${swapInfo.minAmountOut} ${assetOut}`);
|
||||
// }
|
||||
|
||||
// if (swapInfo.type === 'exactSpend' && amountBN.lt(swapInfo.minAmountIn)) {
|
||||
// throw new Error(`Amount is too low. Min amountIn is ${swapInfo.minAmountIn} ${assetIn}`);
|
||||
// }
|
||||
|
||||
// if (swapInfo.orderInfo === null) throw new Error(swapInfo.executionInfo);
|
||||
|
||||
let route: 'pool' | 'aggregator';
|
||||
if (options?.poolOnly !== undefined && options.poolOnly) {
|
||||
route = 'pool';
|
||||
} else if (
|
||||
poolExchangesList.length > 0 &&
|
||||
swapExchanges.length === 1 &&
|
||||
firstSwapExchange !== undefined &&
|
||||
poolExchangesList.some((poolExchange) => poolExchange === firstSwapExchange)
|
||||
) {
|
||||
route = 'pool';
|
||||
} else {
|
||||
route = 'aggregator';
|
||||
}
|
||||
|
||||
if (route === 'pool') {
|
||||
const transactionCost = ethers.BigNumber.from(SWAP_THROUGH_ORION_POOL_GAS_LIMIT).mul(gasPriceWei);
|
||||
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
|
||||
|
||||
return {
|
||||
route,
|
||||
swapInfo,
|
||||
fee: {
|
||||
assetName: nativeCryptocurrencyName,
|
||||
assetAddress: ethers.constants.AddressZero,
|
||||
networkFeeInFeeAsset: denormalizedTransactionCost.toString(),
|
||||
protocolFeeInFeeAsset: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (swapInfo.orderInfo !== null) {
|
||||
const [baseAssetName] = swapInfo.orderInfo.assetPair.split('-');
|
||||
if (baseAssetName === undefined) throw new Error('Base asset name is undefined');
|
||||
const baseAssetAddress = assetToAddress[baseAssetName];
|
||||
if (baseAssetAddress === undefined) throw new Error(`No asset address for ${baseAssetName}`);
|
||||
|
||||
// Fee calculation
|
||||
const baseAssetPriceInOrn = pricesInOrn[baseAssetAddress];
|
||||
if (baseAssetPriceInOrn === undefined) throw new Error(`Base asset price ${baseAssetName} in ORN not found`);
|
||||
const baseCurrencyPriceInOrn = pricesInOrn[ethers.constants.AddressZero];
|
||||
if (baseCurrencyPriceInOrn === undefined) throw new Error('Base currency price in ORN not found');
|
||||
const feeAssetPriceInOrn = pricesInOrn[feeAssetAddress];
|
||||
if (feeAssetPriceInOrn === undefined) throw new Error(`Fee asset price ${feeAsset} in ORN not found`);
|
||||
const feePercent = feeAssets[feeAsset];
|
||||
if (feePercent === undefined) throw new Error(`Fee asset ${feeAsset} not available`);
|
||||
|
||||
const {
|
||||
serviceFeeInFeeAsset,
|
||||
networkFeeInFeeAsset,
|
||||
} = calculateFeeInFeeAsset(
|
||||
swapInfo.orderInfo.amount,
|
||||
feeAssetPriceInOrn,
|
||||
baseAssetPriceInOrn,
|
||||
baseCurrencyPriceInOrn,
|
||||
gasPriceGwei,
|
||||
feePercent,
|
||||
);
|
||||
|
||||
return {
|
||||
route,
|
||||
swapInfo,
|
||||
fee: {
|
||||
assetName: feeAsset,
|
||||
assetAddress: feeAssetAddress,
|
||||
networkFeeInFeeAsset,
|
||||
protocolFeeInFeeAsset: serviceFeeInFeeAsset,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
route,
|
||||
swapInfo,
|
||||
fee: {
|
||||
assetName: feeAsset,
|
||||
assetAddress: feeAssetAddress,
|
||||
networkFeeInFeeAsset: undefined,
|
||||
protocolFeeInFeeAsset: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
57
src/Unit/Exchange/index.ts
Normal file
57
src/Unit/Exchange/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type Unit from '../index.js';
|
||||
import deposit, { type DepositParams } from './deposit.js';
|
||||
import getSwapInfo, { type GetSwapInfoParams } from './getSwapInfo.js';
|
||||
import type { SwapLimitParams } from './swapLimit.js';
|
||||
import swapLimit from './swapLimit.js';
|
||||
import swapMarket, { type SwapMarketParams } from './swapMarket.js';
|
||||
import withdraw, { type WithdrawParams } from './withdraw.js';
|
||||
|
||||
type PureSwapMarketParams = Omit<SwapMarketParams, 'unit'>
|
||||
type PureSwapLimitParams = Omit<SwapLimitParams, 'unit'>
|
||||
type PureDepositParams = Omit<DepositParams, 'unit'>
|
||||
type PureWithdrawParams = Omit<WithdrawParams, 'unit'>
|
||||
type PureGetSwapMarketInfoParams = Omit<GetSwapInfoParams, 'blockchainService' | 'aggregator'>
|
||||
|
||||
export default class Exchange {
|
||||
private readonly unit: Unit;
|
||||
|
||||
constructor(unit: Unit) {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public swapLimit(params: PureSwapLimitParams) {
|
||||
return swapLimit({
|
||||
...params,
|
||||
unit: this.unit,
|
||||
});
|
||||
}
|
||||
|
||||
public swapMarket(params: PureSwapMarketParams) {
|
||||
return swapMarket({
|
||||
...params,
|
||||
unit: this.unit,
|
||||
});
|
||||
}
|
||||
|
||||
public getSwapInfo(params: PureGetSwapMarketInfoParams) {
|
||||
return getSwapInfo({
|
||||
aggregator: this.unit.aggregator,
|
||||
blockchainService: this.unit.blockchainService,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
public deposit(params: PureDepositParams) {
|
||||
return deposit({
|
||||
...params,
|
||||
unit: this.unit,
|
||||
});
|
||||
}
|
||||
|
||||
public withdraw(params: PureWithdrawParams) {
|
||||
return withdraw({
|
||||
...params,
|
||||
unit: this.unit,
|
||||
});
|
||||
}
|
||||
}
|
||||
469
src/Unit/Exchange/swapLimit.ts
Normal file
469
src/Unit/Exchange/swapLimit.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
|
||||
import getBalances from '../../utils/getBalances.js';
|
||||
import BalanceGuard from '../../BalanceGuard.js';
|
||||
import getAvailableSources from '../../utils/getAvailableFundsSources.js';
|
||||
import type Unit from '../index.js';
|
||||
import { INTERNAL_PROTOCOL_PRECISION, NATIVE_CURRENCY_PRECISION, SWAP_THROUGH_ORION_POOL_GAS_LIMIT } from '../../constants/index.js';
|
||||
import getNativeCryptocurrencyName from '../../utils/getNativeCryptocurrencyName.js';
|
||||
import { calculateFeeInFeeAsset, denormalizeNumber, normalizeNumber } from '../../utils/index.js';
|
||||
import { signOrder } from '../../crypt/index.js';
|
||||
import type orderSchema from '../../services/Aggregator/schemas/orderSchema.js';
|
||||
import type { z } from 'zod';
|
||||
import { simpleFetch } from 'simple-typed-fetch';
|
||||
|
||||
export type SwapLimitParams = {
|
||||
type: 'exactSpend' | 'exactReceive'
|
||||
assetIn: string
|
||||
assetOut: string
|
||||
price: BigNumber.Value
|
||||
amount: BigNumber.Value
|
||||
feeAsset: string
|
||||
signer: ethers.Signer
|
||||
unit: Unit
|
||||
options?: {
|
||||
poolOnly?: boolean
|
||||
instantSettlement?: boolean
|
||||
logger?: (message: string) => void
|
||||
autoApprove?: boolean
|
||||
developer?: {
|
||||
route?: 'aggregator' | 'pool'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AggregatorOrder = {
|
||||
amountOut: number
|
||||
through: 'aggregator'
|
||||
id: string
|
||||
wait: () => Promise<z.infer<typeof orderSchema>>
|
||||
}
|
||||
|
||||
type PoolSwap = {
|
||||
amountOut: number
|
||||
through: 'pool'
|
||||
txHash: string
|
||||
wait: (confirmations?: number | undefined) => Promise<ethers.providers.TransactionReceipt>
|
||||
}
|
||||
|
||||
export type Swap = AggregatorOrder | PoolSwap;
|
||||
|
||||
export default async function swapLimit({
|
||||
type,
|
||||
assetIn,
|
||||
assetOut,
|
||||
price,
|
||||
amount,
|
||||
feeAsset,
|
||||
signer,
|
||||
unit,
|
||||
options,
|
||||
}: SwapLimitParams): Promise<Swap> {
|
||||
if (options?.developer) options.logger?.('YOU SPECIFIED A DEVELOPER OPTIONS. BE CAREFUL!');
|
||||
if (amount === '') throw new Error('Amount can not be empty');
|
||||
if (assetIn === '') throw new Error('AssetIn can not be empty');
|
||||
if (assetOut === '') throw new Error('AssetOut can not be empty');
|
||||
if (feeAsset === '') throw new Error('Fee asset can not be empty');
|
||||
if (price === '') throw new Error('Price can not be empty');
|
||||
|
||||
const amountBN = new BigNumber(amount);
|
||||
if (amountBN.isNaN()) throw new Error(`Amount '${amountBN.toString()}' is not a number`);
|
||||
if (amountBN.lte(0)) throw new Error(`Amount '${amountBN.toString()}' should be greater than 0`);
|
||||
|
||||
const priceBN = new BigNumber(price);
|
||||
if (priceBN.isNaN()) throw new Error(`Price '${priceBN.toString()}' is not a number`);
|
||||
if (priceBN.lte(0)) throw new Error('Price should be greater than 0');
|
||||
|
||||
const walletAddress = await signer.getAddress();
|
||||
options?.logger?.(`Wallet address is ${walletAddress}`);
|
||||
|
||||
const {
|
||||
blockchainService, aggregator, provider, chainId,
|
||||
} = unit;
|
||||
const {
|
||||
exchangeContractAddress,
|
||||
matcherAddress,
|
||||
assetToAddress,
|
||||
} = await simpleFetch(blockchainService.getInfo)();
|
||||
const nativeCryptocurrency = getNativeCryptocurrencyName(assetToAddress);
|
||||
|
||||
const exchangeContract = Exchange__factory.connect(exchangeContractAddress, provider);
|
||||
const feeAssets = await simpleFetch(blockchainService.getTokensFee)();
|
||||
const pricesInOrn = await simpleFetch(blockchainService.getPrices)();
|
||||
const gasPriceWei = await simpleFetch(blockchainService.getGasPriceWei)();
|
||||
const { factories } = await simpleFetch(blockchainService.getPoolsConfig)();
|
||||
const poolExchangesList = factories !== undefined ? Object.keys(factories) : [];
|
||||
|
||||
const gasPriceGwei = ethers.utils.formatUnits(gasPriceWei, 'gwei').toString();
|
||||
|
||||
const assetInAddress = assetToAddress[assetIn];
|
||||
if (assetInAddress === undefined) throw new Error(`Asset '${assetIn}' not found`);
|
||||
const feeAssetAddress = assetToAddress[feeAsset];
|
||||
if (feeAssetAddress === undefined) {
|
||||
throw new Error(`Fee asset '${feeAsset}' not found. Available assets: ${Object.keys(feeAssets).join(', ')}`);
|
||||
}
|
||||
|
||||
const balances = await getBalances(
|
||||
{
|
||||
[assetIn]: assetInAddress,
|
||||
[feeAsset]: feeAssetAddress,
|
||||
[nativeCryptocurrency]: ethers.constants.AddressZero,
|
||||
},
|
||||
aggregator,
|
||||
walletAddress,
|
||||
exchangeContract,
|
||||
provider,
|
||||
);
|
||||
|
||||
const balanceGuard = new BalanceGuard(
|
||||
balances,
|
||||
{
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
provider,
|
||||
signer,
|
||||
options?.logger,
|
||||
);
|
||||
|
||||
const swapInfo = await simpleFetch(aggregator.getSwapInfo)(
|
||||
type,
|
||||
assetIn,
|
||||
assetOut,
|
||||
amountBN.toString(),
|
||||
options?.instantSettlement,
|
||||
options?.poolOnly !== undefined && options.poolOnly
|
||||
? 'pools'
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const { exchanges: swapExchanges } = swapInfo;
|
||||
|
||||
const [firstSwapExchange] = swapExchanges;
|
||||
|
||||
if (swapExchanges.length > 0) options?.logger?.(`Swap exchanges: ${swapExchanges.join(', ')}`);
|
||||
|
||||
if (swapInfo.type === 'exactReceive' && amountBN.lt(swapInfo.minAmountOut)) {
|
||||
throw new Error(`Amount is too low. Min amountOut is ${swapInfo.minAmountOut} ${assetOut}`);
|
||||
}
|
||||
|
||||
if (swapInfo.type === 'exactSpend' && amountBN.lt(swapInfo.minAmountIn)) {
|
||||
throw new Error(`Amount is too low. Min amountIn is ${swapInfo.minAmountIn} ${assetIn}`);
|
||||
}
|
||||
|
||||
if (swapInfo.orderInfo === null) throw new Error(swapInfo.executionInfo);
|
||||
|
||||
const [baseAssetName, quoteAssetName] = swapInfo.orderInfo.assetPair.split('-');
|
||||
if (baseAssetName === undefined) throw new Error('Base asset name is undefined');
|
||||
if (quoteAssetName === undefined) throw new Error('Quote asset name is undefined');
|
||||
|
||||
const pairConfig = await simpleFetch(aggregator.getPairConfig)(`${baseAssetName}-${quoteAssetName}`);
|
||||
const minQtyBN = new BigNumber(pairConfig.minQty);
|
||||
const qtyPrecisionBN = new BigNumber(pairConfig.qtyPrecision);
|
||||
const pricePrecisionBN = new BigNumber(pairConfig.pricePrecision);
|
||||
const minPrice = new BigNumber(pairConfig.minPrice);
|
||||
const maxPrice = new BigNumber(pairConfig.maxPrice);
|
||||
|
||||
const qtyDecimalPlaces = amountBN.dp();
|
||||
const priceDecimalPlaces = priceBN.dp();
|
||||
|
||||
if (qtyDecimalPlaces === null) throw new Error('Qty decimal places is null. Likely amount is -Infinity, +Infinity or NaN');
|
||||
if (qtyPrecisionBN.lt(qtyDecimalPlaces)) {
|
||||
throw new Error(
|
||||
`Actual amount decimal places (${qtyDecimalPlaces}) is greater than max allowed decimal places (${qtyPrecisionBN.toString()}) on pair ${baseAssetName}-${quoteAssetName}.`
|
||||
);
|
||||
}
|
||||
if (priceDecimalPlaces === null) throw new Error('Price decimal places is null. Likely price is -Infinity, +Infinity or NaN');
|
||||
if (pricePrecisionBN.lt(priceDecimalPlaces)) {
|
||||
throw new Error(
|
||||
`Actual price decimal places (${priceDecimalPlaces}) is greater than max allowed decimal places (${pricePrecisionBN.toString()}) on pair ${baseAssetName}-${quoteAssetName}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (priceBN.lt(minPrice)) {
|
||||
throw new Error(`Price is too low. Min price is ${minPrice.toString()} ${quoteAssetName}`);
|
||||
}
|
||||
if (priceBN.gt(maxPrice)) {
|
||||
throw new Error(`Price is too high. Max price is ${maxPrice.toString()} ${quoteAssetName}`);
|
||||
}
|
||||
|
||||
options?.logger?.(`Safe price is ${swapInfo.orderInfo.safePrice} ${quoteAssetName}`);
|
||||
// BTEMP — better than or equal market price
|
||||
const priceIsBTEMP = type === 'exactSpend'
|
||||
? priceBN.lte(swapInfo.orderInfo.safePrice)
|
||||
: priceBN.gte(swapInfo.orderInfo.safePrice);
|
||||
|
||||
options?.logger?.(`Your price ${priceBN.toString()} is ${priceIsBTEMP ? 'better than or equal' : 'worse than'} market price ${swapInfo.orderInfo.safePrice}`);
|
||||
|
||||
let route: 'aggregator' | 'pool';
|
||||
|
||||
if (options?.developer?.route !== undefined) {
|
||||
if (options.developer.route === 'pool' && !priceIsBTEMP) {
|
||||
throw new Error(
|
||||
'CONFLICT: Pool execution is not available for this swap.' +
|
||||
' Price is worse than market price. Please unset "route" option or set it to "aggregator"'
|
||||
);
|
||||
}
|
||||
route = options.developer.route;
|
||||
options.logger?.(`Swap is through ${route} (because route forced to ${route})`);
|
||||
} else if (options?.poolOnly !== undefined && options.poolOnly) {
|
||||
if (!priceIsBTEMP) {
|
||||
throw new Error(
|
||||
'CONFLICT: Pool execution is not available for this swap.' +
|
||||
' Price is worse than market price. Please disable "poolOnly" option'
|
||||
);
|
||||
}
|
||||
options.logger?.('Swap is through pool (because "poolOnly" option is true)');
|
||||
route = 'pool';
|
||||
} else if (
|
||||
poolExchangesList.length > 0 &&
|
||||
swapExchanges.length === 1 &&
|
||||
firstSwapExchange !== undefined &&
|
||||
poolExchangesList.some((poolExchange) => poolExchange === firstSwapExchange) &&
|
||||
priceIsBTEMP
|
||||
) {
|
||||
options?.logger?.(`Swap is through pool [via ${firstSwapExchange}] (detected by "exchanges" field)`);
|
||||
route = 'pool';
|
||||
} else {
|
||||
route = 'aggregator';
|
||||
}
|
||||
|
||||
if (route === 'pool') {
|
||||
let factoryAddress: string | undefined;
|
||||
if (factories && firstSwapExchange !== undefined) {
|
||||
factoryAddress = factories[firstSwapExchange];
|
||||
if (factoryAddress !== undefined) options?.logger?.(`Factory address is ${factoryAddress}. Exchange is ${firstSwapExchange}`);
|
||||
}
|
||||
|
||||
const pathAddresses = swapInfo.path.map((name) => {
|
||||
const assetAddress = assetToAddress[name];
|
||||
if (assetAddress === undefined) throw new Error(`No asset address for ${name}`);
|
||||
return assetAddress;
|
||||
});
|
||||
|
||||
const amountSpend = swapInfo.type === 'exactSpend'
|
||||
? swapInfo.amountIn
|
||||
: new BigNumber(swapInfo.orderInfo.amount).multipliedBy(swapInfo.orderInfo.safePrice)
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Amount spend',
|
||||
asset: {
|
||||
name: assetIn,
|
||||
address: assetInAddress,
|
||||
},
|
||||
amount: amountSpend.toString(),
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: getAvailableSources('amount', assetInAddress, 'pool'),
|
||||
});
|
||||
|
||||
const amountReceive = swapInfo.type === 'exactReceive'
|
||||
? swapInfo.amountOut
|
||||
: new BigNumber(swapInfo.orderInfo.amount).multipliedBy(swapInfo.orderInfo.safePrice)
|
||||
const amountSpendBlockchainParam = normalizeNumber(
|
||||
amountSpend,
|
||||
INTERNAL_PROTOCOL_PRECISION,
|
||||
BigNumber.ROUND_CEIL,
|
||||
);
|
||||
const amountReceiveBlockchainParam = normalizeNumber(
|
||||
amountReceive,
|
||||
INTERNAL_PROTOCOL_PRECISION,
|
||||
BigNumber.ROUND_FLOOR,
|
||||
);
|
||||
|
||||
const unsignedSwapThroughOrionPoolTx = await exchangeContract.populateTransaction.swapThroughOrionPool(
|
||||
amountSpendBlockchainParam,
|
||||
amountReceiveBlockchainParam,
|
||||
factoryAddress !== undefined
|
||||
? [factoryAddress, ...pathAddresses]
|
||||
: pathAddresses,
|
||||
type === 'exactSpend',
|
||||
);
|
||||
|
||||
unsignedSwapThroughOrionPoolTx.chainId = parseInt(chainId, 10);
|
||||
unsignedSwapThroughOrionPoolTx.gasPrice = ethers.BigNumber.from(gasPriceWei);
|
||||
|
||||
unsignedSwapThroughOrionPoolTx.from = walletAddress;
|
||||
const amountSpendBN = new BigNumber(amountSpend);
|
||||
|
||||
let value = new BigNumber(0);
|
||||
const denormalizedAssetInExchangeBalance = balances[assetIn]?.exchange;
|
||||
if (denormalizedAssetInExchangeBalance === undefined) throw new Error(`Asset '${assetIn}' exchange balance is not found`);
|
||||
if (assetIn === nativeCryptocurrency && amountSpendBN.gt(denormalizedAssetInExchangeBalance)) {
|
||||
value = amountSpendBN.minus(denormalizedAssetInExchangeBalance);
|
||||
}
|
||||
unsignedSwapThroughOrionPoolTx.value = normalizeNumber(
|
||||
value.dp(INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_CEIL),
|
||||
NATIVE_CURRENCY_PRECISION,
|
||||
BigNumber.ROUND_CEIL,
|
||||
);
|
||||
unsignedSwapThroughOrionPoolTx.gasLimit = ethers.BigNumber.from(SWAP_THROUGH_ORION_POOL_GAS_LIMIT);
|
||||
|
||||
const transactionCost = ethers.BigNumber.from(SWAP_THROUGH_ORION_POOL_GAS_LIMIT).mul(gasPriceWei);
|
||||
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
amount: denormalizedTransactionCost.toString(),
|
||||
sources: getAvailableSources('network_fee', ethers.constants.AddressZero, 'pool'),
|
||||
});
|
||||
|
||||
// if (value.gt(0)) {
|
||||
// balanceGuard.registerRequirement({
|
||||
// reason: 'Transaction value (extra amount)',
|
||||
// asset: {
|
||||
// name: nativeCryptocurrency,
|
||||
// address: ethers.constants.AddressZero,
|
||||
// },
|
||||
// amount: value.toString(),
|
||||
// sources: getAvailableSources('amount', ethers.constants.AddressZero, 'pool'),
|
||||
// });
|
||||
// }
|
||||
|
||||
await balanceGuard.check(options?.autoApprove);
|
||||
|
||||
const nonce = await provider.getTransactionCount(walletAddress, 'pending');
|
||||
unsignedSwapThroughOrionPoolTx.nonce = nonce;
|
||||
|
||||
options?.logger?.('Signing transaction...');
|
||||
const swapThroughOrionPoolTxResponse = await signer.sendTransaction(unsignedSwapThroughOrionPoolTx);
|
||||
options?.logger?.(`Transaction sent. Tx hash: ${swapThroughOrionPoolTxResponse.hash}`);
|
||||
return {
|
||||
amountOut: swapInfo.amountOut,
|
||||
wait: swapThroughOrionPoolTxResponse.wait,
|
||||
through: 'pool',
|
||||
txHash: swapThroughOrionPoolTxResponse.hash,
|
||||
};
|
||||
}
|
||||
options?.logger?.('Swap through aggregator');
|
||||
|
||||
if (amountBN.lt(minQtyBN)) {
|
||||
throw new Error(`Amount is too low. Min amount is ${minQtyBN.toString()} ${baseAssetName}`);
|
||||
}
|
||||
|
||||
const baseAssetAddress = assetToAddress[baseAssetName];
|
||||
if (baseAssetAddress === undefined) throw new Error(`No asset address for ${baseAssetName}`);
|
||||
const quoteAssetAddress = assetToAddress[quoteAssetName];
|
||||
if (quoteAssetAddress === undefined) throw new Error(`No asset address for ${quoteAssetName}`);
|
||||
|
||||
const safePriceWithAppliedPrecision = priceBN
|
||||
.decimalPlaces(
|
||||
pairConfig.pricePrecision,
|
||||
swapInfo.orderInfo.side === 'BUY'
|
||||
? BigNumber.ROUND_CEIL
|
||||
: BigNumber.ROUND_FLOOR,
|
||||
);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Amount',
|
||||
asset: {
|
||||
name: assetIn,
|
||||
address: assetInAddress,
|
||||
},
|
||||
amount: swapInfo.orderInfo.side === 'SELL'
|
||||
? swapInfo.orderInfo.amount.toString()
|
||||
: safePriceWithAppliedPrecision.multipliedBy(swapInfo.orderInfo.amount).toString(),
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: getAvailableSources('amount', assetInAddress, 'aggregator'),
|
||||
});
|
||||
|
||||
// Fee calculation
|
||||
const baseAssetPriceInOrn = pricesInOrn[baseAssetAddress];
|
||||
if (baseAssetPriceInOrn === undefined) throw new Error(`Base asset price ${baseAssetName} in ORN not found`);
|
||||
const baseCurrencyPriceInOrn = pricesInOrn[ethers.constants.AddressZero];
|
||||
if (baseCurrencyPriceInOrn === undefined) throw new Error('Base currency price in ORN not found');
|
||||
const feeAssetPriceInOrn = pricesInOrn[feeAssetAddress];
|
||||
if (feeAssetPriceInOrn === undefined) throw new Error(`Fee asset price ${feeAsset} in ORN not found`);
|
||||
const feePercent = feeAssets[feeAsset];
|
||||
if (feePercent === undefined) throw new Error(`Fee asset ${feeAsset} not available`);
|
||||
|
||||
const { serviceFeeInFeeAsset, networkFeeInFeeAsset, totalFeeInFeeAsset } = calculateFeeInFeeAsset(
|
||||
swapInfo.orderInfo.amount,
|
||||
feeAssetPriceInOrn,
|
||||
baseAssetPriceInOrn,
|
||||
baseCurrencyPriceInOrn,
|
||||
gasPriceGwei,
|
||||
feePercent,
|
||||
);
|
||||
|
||||
if (feeAsset === assetOut) {
|
||||
options?.logger?.('Fee asset equals received asset. The fee can be paid from the amount received');
|
||||
options?.logger?.(`Set extra balance: + ${swapInfo.amountOut} ${assetOut} to exchange`);
|
||||
|
||||
balanceGuard.setExtraBalance(feeAsset, swapInfo.amountOut, 'exchange');
|
||||
}
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: feeAsset,
|
||||
address: feeAssetAddress,
|
||||
},
|
||||
amount: networkFeeInFeeAsset,
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: getAvailableSources('network_fee', feeAssetAddress, 'aggregator'),
|
||||
});
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Orion fee',
|
||||
asset: {
|
||||
name: feeAsset,
|
||||
address: feeAssetAddress,
|
||||
},
|
||||
amount: serviceFeeInFeeAsset,
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: getAvailableSources('service_fee', feeAssetAddress, 'aggregator'),
|
||||
});
|
||||
|
||||
await balanceGuard.check(options?.autoApprove);
|
||||
|
||||
const signedOrder = await signOrder(
|
||||
baseAssetAddress,
|
||||
quoteAssetAddress,
|
||||
swapInfo.orderInfo.side,
|
||||
safePriceWithAppliedPrecision.toString(),
|
||||
swapInfo.orderInfo.amount,
|
||||
totalFeeInFeeAsset,
|
||||
walletAddress,
|
||||
matcherAddress,
|
||||
feeAssetAddress,
|
||||
false,
|
||||
signer,
|
||||
chainId,
|
||||
);
|
||||
const orderIsOk = await exchangeContract.validateOrder(signedOrder);
|
||||
if (!orderIsOk) throw new Error('Order is not valid');
|
||||
|
||||
const { orderId } = await simpleFetch(aggregator.placeOrder)(signedOrder, false);
|
||||
options?.logger?.(`Order placed. Order id: ${orderId}`);
|
||||
|
||||
return {
|
||||
amountOut: amountBN.multipliedBy(safePriceWithAppliedPrecision).toNumber(),
|
||||
wait: () => new Promise<z.infer<typeof orderSchema>>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Timeout'))
|
||||
}, 60000);
|
||||
const interval = setInterval(() => {
|
||||
simpleFetch(aggregator.getOrder)(orderId).then((data) => {
|
||||
if (data.order.status === 'SETTLED') {
|
||||
options?.logger?.(`Order ${orderId} settled`);
|
||||
clearTimeout(timeout);
|
||||
clearInterval(interval);
|
||||
resolve(data);
|
||||
} else {
|
||||
options?.logger?.(`Order ${orderId} status: ${data.order.status}`);
|
||||
}
|
||||
}).catch((e) => {
|
||||
if (!(e instanceof Error)) throw new Error('Not an error');
|
||||
options?.logger?.(`Error while getting order status: ${e.message}`);
|
||||
});
|
||||
}, 1000);
|
||||
}),
|
||||
through: 'aggregator',
|
||||
id: orderId,
|
||||
};
|
||||
}
|
||||
427
src/Unit/Exchange/swapMarket.ts
Normal file
427
src/Unit/Exchange/swapMarket.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
|
||||
import getBalances from '../../utils/getBalances.js';
|
||||
import BalanceGuard from '../../BalanceGuard.js';
|
||||
import getAvailableSources from '../../utils/getAvailableFundsSources.js';
|
||||
import { INTERNAL_PROTOCOL_PRECISION, NATIVE_CURRENCY_PRECISION, SWAP_THROUGH_ORION_POOL_GAS_LIMIT } from '../../constants/index.js';
|
||||
import getNativeCryptocurrencyName from '../../utils/getNativeCryptocurrencyName.js';
|
||||
import { calculateFeeInFeeAsset, denormalizeNumber, normalizeNumber } from '../../utils/index.js';
|
||||
import { signOrder } from '../../crypt/index.js';
|
||||
import type orderSchema from '../../services/Aggregator/schemas/orderSchema.js';
|
||||
import type { z } from 'zod';
|
||||
import type { SwapLimitParams } from './swapLimit.js';
|
||||
import { simpleFetch } from 'simple-typed-fetch';
|
||||
|
||||
export type SwapMarketParams = Omit<SwapLimitParams, 'price'> & {
|
||||
slippagePercent: BigNumber.Value
|
||||
}
|
||||
|
||||
type AggregatorOrder = {
|
||||
amountOut: number
|
||||
through: 'aggregator'
|
||||
id: string
|
||||
wait: () => Promise<z.infer<typeof orderSchema>>
|
||||
}
|
||||
|
||||
type PoolSwap = {
|
||||
amountOut: number
|
||||
through: 'pool'
|
||||
txHash: string
|
||||
wait: (confirmations?: number | undefined) => Promise<ethers.providers.TransactionReceipt>
|
||||
}
|
||||
|
||||
export type Swap = AggregatorOrder | PoolSwap;
|
||||
|
||||
export default async function swapMarket({
|
||||
type,
|
||||
assetIn,
|
||||
assetOut,
|
||||
amount,
|
||||
feeAsset,
|
||||
slippagePercent,
|
||||
signer,
|
||||
unit,
|
||||
options,
|
||||
}: SwapMarketParams): Promise<Swap> {
|
||||
if (options?.developer) options.logger?.('YOU SPECIFIED A DEVELOPER OPTIONS. BE CAREFUL!');
|
||||
if (amount === '') throw new Error('Amount can not be empty');
|
||||
if (assetIn === '') throw new Error('AssetIn can not be empty');
|
||||
if (assetOut === '') throw new Error('AssetOut can not be empty');
|
||||
if (feeAsset === '') throw new Error('Fee asset can not be empty');
|
||||
if (slippagePercent === '') throw new Error('Slippage percent can not be empty');
|
||||
|
||||
const amountBN = new BigNumber(amount);
|
||||
if (amountBN.isNaN()) throw new Error(`Amount '${amountBN.toString()}' is not a number`);
|
||||
if (amountBN.lte(0)) throw new Error(`Amount '${amountBN.toString()}' should be greater than 0`);
|
||||
|
||||
const slippagePercentBN = new BigNumber(slippagePercent);
|
||||
if (slippagePercentBN.isNaN()) throw new Error(`Slippage percent '${slippagePercentBN.toString()}' is not a number`);
|
||||
if (slippagePercentBN.lte(0)) throw new Error('Slippage percent should be greater than 0');
|
||||
if (slippagePercentBN.gte(50)) throw new Error('Slippage percent should be less than 50');
|
||||
|
||||
const walletAddress = await signer.getAddress();
|
||||
options?.logger?.(`Wallet address is ${walletAddress}`);
|
||||
|
||||
const {
|
||||
blockchainService, aggregator, provider, chainId,
|
||||
} = unit;
|
||||
const {
|
||||
exchangeContractAddress,
|
||||
matcherAddress,
|
||||
assetToAddress,
|
||||
} = await simpleFetch(blockchainService.getInfo)();
|
||||
const nativeCryptocurrency = getNativeCryptocurrencyName(assetToAddress);
|
||||
|
||||
const exchangeContract = Exchange__factory.connect(exchangeContractAddress, provider);
|
||||
const feeAssets = await simpleFetch(blockchainService.getTokensFee)();
|
||||
const pricesInOrn = await simpleFetch(blockchainService.getPrices)();
|
||||
const gasPriceWei = await simpleFetch(blockchainService.getGasPriceWei)();
|
||||
const { factories } = await simpleFetch(blockchainService.getPoolsConfig)();
|
||||
const poolExchangesList = factories !== undefined ? Object.keys(factories) : [];
|
||||
|
||||
const gasPriceGwei = ethers.utils.formatUnits(gasPriceWei, 'gwei').toString();
|
||||
|
||||
const assetInAddress = assetToAddress[assetIn];
|
||||
if (assetInAddress === undefined) throw new Error(`Asset '${assetIn}' not found`);
|
||||
const feeAssetAddress = assetToAddress[feeAsset];
|
||||
if (feeAssetAddress === undefined) {
|
||||
throw new Error(`Fee asset '${feeAsset}' not found. Available assets: ${Object.keys(feeAssets).join(', ')}`);
|
||||
}
|
||||
|
||||
const balances = await getBalances(
|
||||
{
|
||||
[assetIn]: assetInAddress,
|
||||
[feeAsset]: feeAssetAddress,
|
||||
[nativeCryptocurrency]: ethers.constants.AddressZero,
|
||||
},
|
||||
aggregator,
|
||||
walletAddress,
|
||||
exchangeContract,
|
||||
provider,
|
||||
);
|
||||
|
||||
const balanceGuard = new BalanceGuard(
|
||||
balances,
|
||||
{
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
provider,
|
||||
signer,
|
||||
options?.logger,
|
||||
);
|
||||
|
||||
const swapInfo = await simpleFetch(aggregator.getSwapInfo)(
|
||||
type,
|
||||
assetIn,
|
||||
assetOut,
|
||||
amountBN.toString(),
|
||||
options?.instantSettlement,
|
||||
options?.poolOnly !== undefined && options.poolOnly
|
||||
? 'pools'
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const { exchanges: swapExchanges } = swapInfo;
|
||||
|
||||
const [firstSwapExchange] = swapExchanges;
|
||||
|
||||
if (swapExchanges.length > 0) options?.logger?.(`Swap exchanges: ${swapExchanges.join(', ')}`);
|
||||
|
||||
if (swapInfo.type === 'exactReceive' && amountBN.lt(swapInfo.minAmountOut)) {
|
||||
throw new Error(`Amount is too low. Min amountOut is ${swapInfo.minAmountOut} ${assetOut}`);
|
||||
}
|
||||
|
||||
if (swapInfo.type === 'exactSpend' && amountBN.lt(swapInfo.minAmountIn)) {
|
||||
throw new Error(`Amount is too low. Min amountIn is ${swapInfo.minAmountIn} ${assetIn}`);
|
||||
}
|
||||
|
||||
if (swapInfo.orderInfo === null) throw new Error(swapInfo.executionInfo);
|
||||
|
||||
const [baseAssetName, quoteAssetName] = swapInfo.orderInfo.assetPair.split('-');
|
||||
if (baseAssetName === undefined) throw new Error('Base asset name is undefined');
|
||||
if (quoteAssetName === undefined) throw new Error('Quote asset name is undefined');
|
||||
|
||||
const pairConfig = await simpleFetch(aggregator.getPairConfig)(`${baseAssetName}-${quoteAssetName}`);
|
||||
const qtyPrecisionBN = new BigNumber(pairConfig.qtyPrecision);
|
||||
const qtyDecimalPlaces = amountBN.dp();
|
||||
|
||||
if (qtyDecimalPlaces === null) throw new Error('Qty decimal places is null. Likely amount is -Infinity, +Infinity or NaN');
|
||||
|
||||
if (qtyPrecisionBN.lt(qtyDecimalPlaces)) {
|
||||
throw new Error(
|
||||
`Actual amount decimal places (${qtyDecimalPlaces}) is greater than max allowed decimal places (${qtyPrecisionBN.toString()}) on pair ${baseAssetName}-${quoteAssetName}.`
|
||||
);
|
||||
}
|
||||
|
||||
let route: 'aggregator' | 'pool';
|
||||
|
||||
const percent = new BigNumber(slippagePercent).div(100);
|
||||
|
||||
if (options?.developer?.route !== undefined) {
|
||||
route = options.developer.route;
|
||||
options.logger?.(`Swap is through ${route} (because route forced to ${route})`);
|
||||
} else if (options?.poolOnly !== undefined && options.poolOnly) {
|
||||
options.logger?.('Swap is through pool (because "poolOnly" option is true)');
|
||||
route = 'pool';
|
||||
} else if (
|
||||
poolExchangesList.length > 0 &&
|
||||
swapExchanges.length === 1 &&
|
||||
firstSwapExchange !== undefined &&
|
||||
poolExchangesList.some((poolExchange) => poolExchange === firstSwapExchange)
|
||||
) {
|
||||
options?.logger?.(`Swap is through pool [via ${firstSwapExchange}] (detected by "exchanges" field)`);
|
||||
route = 'pool';
|
||||
} else {
|
||||
route = 'aggregator';
|
||||
}
|
||||
|
||||
if (route === 'pool') {
|
||||
let factoryAddress: string | undefined;
|
||||
if (factories && firstSwapExchange !== undefined) {
|
||||
factoryAddress = factories[firstSwapExchange];
|
||||
if (factoryAddress !== undefined) options?.logger?.(`Factory address is ${factoryAddress}. Exchange is ${firstSwapExchange}`);
|
||||
}
|
||||
|
||||
const pathAddresses = swapInfo.path.map((name) => {
|
||||
const assetAddress = assetToAddress[name];
|
||||
if (assetAddress === undefined) throw new Error(`No asset address for ${name}`);
|
||||
return assetAddress;
|
||||
});
|
||||
|
||||
const amountOutWithSlippage = new BigNumber(swapInfo.amountOut)
|
||||
.multipliedBy(new BigNumber(1).minus(percent))
|
||||
.toString();
|
||||
const amountInWithSlippage = new BigNumber(swapInfo.amountIn)
|
||||
.multipliedBy(new BigNumber(1).plus(percent))
|
||||
.toString();
|
||||
|
||||
const amountSpend = swapInfo.type === 'exactSpend' ? swapInfo.amountIn : amountInWithSlippage;
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Amount spend',
|
||||
asset: {
|
||||
name: assetIn,
|
||||
address: assetInAddress,
|
||||
},
|
||||
amount: amountSpend.toString(),
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: getAvailableSources('amount', assetInAddress, 'pool'),
|
||||
});
|
||||
|
||||
const amountReceive = swapInfo.type === 'exactReceive' ? swapInfo.amountOut : amountOutWithSlippage;
|
||||
const amountSpendBlockchainParam = normalizeNumber(
|
||||
amountSpend,
|
||||
INTERNAL_PROTOCOL_PRECISION,
|
||||
BigNumber.ROUND_CEIL,
|
||||
);
|
||||
const amountReceiveBlockchainParam = normalizeNumber(
|
||||
amountReceive,
|
||||
INTERNAL_PROTOCOL_PRECISION,
|
||||
BigNumber.ROUND_FLOOR,
|
||||
);
|
||||
const unsignedSwapThroughOrionPoolTx = await exchangeContract.populateTransaction.swapThroughOrionPool(
|
||||
amountSpendBlockchainParam,
|
||||
amountReceiveBlockchainParam,
|
||||
factoryAddress !== undefined
|
||||
? [factoryAddress, ...pathAddresses]
|
||||
: pathAddresses,
|
||||
type === 'exactSpend',
|
||||
);
|
||||
|
||||
unsignedSwapThroughOrionPoolTx.chainId = parseInt(chainId, 10);
|
||||
unsignedSwapThroughOrionPoolTx.gasPrice = ethers.BigNumber.from(gasPriceWei);
|
||||
|
||||
unsignedSwapThroughOrionPoolTx.from = walletAddress;
|
||||
const amountSpendBN = new BigNumber(amountSpend);
|
||||
|
||||
let value = new BigNumber(0);
|
||||
const denormalizedAssetInExchangeBalance = balances[assetIn]?.exchange;
|
||||
if (denormalizedAssetInExchangeBalance === undefined) throw new Error(`Asset '${assetIn}' exchange balance is not found`);
|
||||
if (assetIn === nativeCryptocurrency && amountSpendBN.gt(denormalizedAssetInExchangeBalance)) {
|
||||
value = amountSpendBN.minus(denormalizedAssetInExchangeBalance);
|
||||
}
|
||||
unsignedSwapThroughOrionPoolTx.value = normalizeNumber(
|
||||
value.dp(INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_CEIL),
|
||||
NATIVE_CURRENCY_PRECISION,
|
||||
BigNumber.ROUND_CEIL,
|
||||
);
|
||||
unsignedSwapThroughOrionPoolTx.gasLimit = ethers.BigNumber.from(SWAP_THROUGH_ORION_POOL_GAS_LIMIT);
|
||||
|
||||
const transactionCost = ethers.BigNumber.from(SWAP_THROUGH_ORION_POOL_GAS_LIMIT).mul(gasPriceWei);
|
||||
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
amount: denormalizedTransactionCost.toString(),
|
||||
sources: getAvailableSources('network_fee', ethers.constants.AddressZero, 'pool'),
|
||||
});
|
||||
|
||||
// if (value.gt(0)) {
|
||||
// balanceGuard.registerRequirement({
|
||||
// reason: 'Transaction value (extra amount)',
|
||||
// asset: {
|
||||
// name: nativeCryptocurrency,
|
||||
// address: ethers.constants.AddressZero,
|
||||
// },
|
||||
// amount: value.toString(),
|
||||
// sources: getAvailableSources('amount', ethers.constants.AddressZero, 'pool'),
|
||||
// });
|
||||
// }
|
||||
|
||||
await balanceGuard.check(options?.autoApprove);
|
||||
|
||||
const nonce = await provider.getTransactionCount(walletAddress, 'pending');
|
||||
unsignedSwapThroughOrionPoolTx.nonce = nonce;
|
||||
|
||||
options?.logger?.('Signing transaction...');
|
||||
const swapThroughOrionPoolTxResponse = await signer.sendTransaction(unsignedSwapThroughOrionPoolTx);
|
||||
options?.logger?.(`Transaction sent. Tx hash: ${swapThroughOrionPoolTxResponse.hash}`);
|
||||
return {
|
||||
amountOut: swapInfo.amountOut,
|
||||
wait: swapThroughOrionPoolTxResponse.wait,
|
||||
through: 'pool',
|
||||
txHash: swapThroughOrionPoolTxResponse.hash,
|
||||
};
|
||||
}
|
||||
options?.logger?.('Swap through aggregator');
|
||||
|
||||
const slippageMultiplier = new BigNumber(1).plus(
|
||||
swapInfo.orderInfo.side === 'SELL'
|
||||
? percent.negated() // e.g. -0.01
|
||||
: percent, // e.g. 0.01
|
||||
);
|
||||
|
||||
const safePriceWithDeviation = percent.isZero()
|
||||
? swapInfo.orderInfo.safePrice
|
||||
: new BigNumber(swapInfo.orderInfo.safePrice)
|
||||
.multipliedBy(slippageMultiplier)
|
||||
.toString();
|
||||
|
||||
const baseAssetAddress = assetToAddress[baseAssetName];
|
||||
if (baseAssetAddress === undefined) throw new Error(`No asset address for ${baseAssetName}`);
|
||||
const quoteAssetAddress = assetToAddress[quoteAssetName];
|
||||
if (quoteAssetAddress === undefined) throw new Error(`No asset address for ${quoteAssetName}`);
|
||||
|
||||
const safePriceWithAppliedPrecision = new BigNumber(safePriceWithDeviation)
|
||||
.decimalPlaces(
|
||||
pairConfig.pricePrecision,
|
||||
swapInfo.orderInfo.side === 'BUY'
|
||||
? BigNumber.ROUND_CEIL
|
||||
: BigNumber.ROUND_FLOOR,
|
||||
);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Amount',
|
||||
asset: {
|
||||
name: assetIn,
|
||||
address: assetInAddress,
|
||||
},
|
||||
amount: swapInfo.orderInfo.side === 'SELL'
|
||||
? swapInfo.orderInfo.amount.toString()
|
||||
: safePriceWithAppliedPrecision.multipliedBy(swapInfo.orderInfo.amount).toString(),
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: getAvailableSources('amount', assetInAddress, 'aggregator'),
|
||||
});
|
||||
|
||||
// Fee calculation
|
||||
const baseAssetPriceInOrn = pricesInOrn[baseAssetAddress];
|
||||
if (baseAssetPriceInOrn === undefined) throw new Error(`Base asset price ${baseAssetName} in ORN not found`);
|
||||
const baseCurrencyPriceInOrn = pricesInOrn[ethers.constants.AddressZero];
|
||||
if (baseCurrencyPriceInOrn === undefined) throw new Error('Base currency price in ORN not found');
|
||||
const feeAssetPriceInOrn = pricesInOrn[feeAssetAddress];
|
||||
if (feeAssetPriceInOrn === undefined) throw new Error(`Fee asset price ${feeAsset} in ORN not found`);
|
||||
const feePercent = feeAssets[feeAsset];
|
||||
if (feePercent === undefined) throw new Error(`Fee asset ${feeAsset} not available`);
|
||||
|
||||
const { serviceFeeInFeeAsset, networkFeeInFeeAsset, totalFeeInFeeAsset } = calculateFeeInFeeAsset(
|
||||
swapInfo.orderInfo.amount,
|
||||
feeAssetPriceInOrn,
|
||||
baseAssetPriceInOrn,
|
||||
baseCurrencyPriceInOrn,
|
||||
gasPriceGwei,
|
||||
feePercent,
|
||||
);
|
||||
|
||||
if (feeAsset === assetOut) {
|
||||
options?.logger?.('Fee asset equals received asset. The fee can be paid from the amount received');
|
||||
options?.logger?.(`Set extra balance: + ${swapInfo.amountOut} ${assetOut} to exchange`);
|
||||
|
||||
balanceGuard.setExtraBalance(feeAsset, swapInfo.amountOut, 'exchange');
|
||||
}
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: feeAsset,
|
||||
address: feeAssetAddress,
|
||||
},
|
||||
amount: networkFeeInFeeAsset,
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: getAvailableSources('network_fee', feeAssetAddress, 'aggregator'),
|
||||
});
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Service fee',
|
||||
asset: {
|
||||
name: feeAsset,
|
||||
address: feeAssetAddress,
|
||||
},
|
||||
amount: serviceFeeInFeeAsset,
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: getAvailableSources('service_fee', feeAssetAddress, 'aggregator'),
|
||||
});
|
||||
|
||||
await balanceGuard.check(options?.autoApprove);
|
||||
|
||||
const signedOrder = await signOrder(
|
||||
baseAssetAddress,
|
||||
quoteAssetAddress,
|
||||
swapInfo.orderInfo.side,
|
||||
safePriceWithAppliedPrecision.toString(),
|
||||
swapInfo.orderInfo.amount,
|
||||
totalFeeInFeeAsset,
|
||||
walletAddress,
|
||||
matcherAddress,
|
||||
feeAssetAddress,
|
||||
false,
|
||||
signer,
|
||||
chainId,
|
||||
);
|
||||
const orderIsOk = await exchangeContract.validateOrder(signedOrder);
|
||||
if (!orderIsOk) throw new Error('Order is not valid');
|
||||
|
||||
const { orderId } = await simpleFetch(aggregator.placeOrder)(signedOrder, false);
|
||||
options?.logger?.(`Order placed. Order id: ${orderId}`);
|
||||
|
||||
return {
|
||||
amountOut: swapInfo.amountOut,
|
||||
wait: () => new Promise<z.infer<typeof orderSchema>>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Timeout'))
|
||||
}, 60000);
|
||||
const interval = setInterval(() => {
|
||||
simpleFetch(aggregator.getOrder)(orderId).then((data) => {
|
||||
if (data.order.status === 'SETTLED') {
|
||||
options?.logger?.(`Order ${orderId} settled`);
|
||||
clearTimeout(timeout);
|
||||
clearInterval(interval);
|
||||
resolve(data);
|
||||
} else {
|
||||
options?.logger?.(`Order ${orderId} status: ${data.order.status}`);
|
||||
}
|
||||
}).catch((e) => {
|
||||
if (!(e instanceof Error)) throw new Error('Not an error');
|
||||
options?.logger?.(`Error while getting order status: ${e.message}`);
|
||||
});
|
||||
}, 1000);
|
||||
}),
|
||||
through: 'aggregator',
|
||||
id: orderId,
|
||||
};
|
||||
}
|
||||
125
src/Unit/Exchange/withdraw.ts
Normal file
125
src/Unit/Exchange/withdraw.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
|
||||
import getBalances from '../../utils/getBalances.js';
|
||||
import BalanceGuard from '../../BalanceGuard.js';
|
||||
import type Unit from '../index.js';
|
||||
import {
|
||||
INTERNAL_PROTOCOL_PRECISION, NATIVE_CURRENCY_PRECISION, WITHDRAW_GAS_LIMIT,
|
||||
} from '../../constants/index.js';
|
||||
import { denormalizeNumber, normalizeNumber } from '../../utils/index.js';
|
||||
import getNativeCryptocurrencyName from '../../utils/getNativeCryptocurrencyName.js';
|
||||
import { simpleFetch } from 'simple-typed-fetch';
|
||||
|
||||
export type WithdrawParams = {
|
||||
asset: string
|
||||
amount: BigNumber.Value
|
||||
signer: ethers.Signer
|
||||
unit: Unit
|
||||
}
|
||||
|
||||
export default async function withdraw({
|
||||
asset,
|
||||
amount,
|
||||
signer,
|
||||
unit,
|
||||
}: WithdrawParams) {
|
||||
if (asset === '') throw new Error('Asset can not be empty');
|
||||
|
||||
const amountBN = new BigNumber(amount);
|
||||
if (amountBN.isNaN()) throw new Error(`Amount '${amountBN.toString()}' is not a number`);
|
||||
if (amountBN.lte(0)) throw new Error(`Amount '${amountBN.toString()}' should be greater than 0`);
|
||||
|
||||
const walletAddress = await signer.getAddress();
|
||||
|
||||
const {
|
||||
blockchainService, aggregator, provider, chainId,
|
||||
} = unit;
|
||||
const {
|
||||
exchangeContractAddress,
|
||||
assetToAddress,
|
||||
} = await simpleFetch(blockchainService.getInfo)();
|
||||
|
||||
const nativeCryptocurrency = getNativeCryptocurrencyName(assetToAddress);
|
||||
const exchangeContract = Exchange__factory.connect(exchangeContractAddress, provider);
|
||||
const gasPriceWei = await simpleFetch(blockchainService.getGasPriceWei)();
|
||||
|
||||
const assetAddress = assetToAddress[asset];
|
||||
if (assetAddress === undefined) throw new Error(`Asset '${asset}' not found`);
|
||||
|
||||
const balances = await getBalances(
|
||||
{
|
||||
[asset]: assetAddress,
|
||||
[nativeCryptocurrency]: ethers.constants.AddressZero,
|
||||
},
|
||||
aggregator,
|
||||
walletAddress,
|
||||
exchangeContract,
|
||||
provider,
|
||||
);
|
||||
|
||||
const balanceGuard = new BalanceGuard(
|
||||
balances,
|
||||
{
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
provider,
|
||||
signer,
|
||||
);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Amount',
|
||||
asset: {
|
||||
name: asset,
|
||||
address: assetAddress,
|
||||
},
|
||||
amount: amountBN.toString(),
|
||||
sources: ['exchange'],
|
||||
});
|
||||
|
||||
const unsignedTx = await exchangeContract.populateTransaction.withdraw(
|
||||
assetAddress,
|
||||
normalizeNumber(amount, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR),
|
||||
);
|
||||
unsignedTx.gasLimit = ethers.BigNumber.from(WITHDRAW_GAS_LIMIT);
|
||||
|
||||
const transactionCost = ethers.BigNumber.from(unsignedTx.gasLimit).mul(gasPriceWei);
|
||||
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
amount: denormalizedTransactionCost.toString(),
|
||||
sources: ['wallet'],
|
||||
});
|
||||
|
||||
unsignedTx.chainId = parseInt(chainId, 10);
|
||||
unsignedTx.gasPrice = ethers.BigNumber.from(gasPriceWei);
|
||||
unsignedTx.from = walletAddress;
|
||||
|
||||
await balanceGuard.check(true);
|
||||
|
||||
const nonce = await provider.getTransactionCount(walletAddress, 'pending');
|
||||
unsignedTx.nonce = nonce;
|
||||
|
||||
const signedTx = await signer.signTransaction(unsignedTx);
|
||||
const txResponse = await provider.sendTransaction(signedTx);
|
||||
console.log(`Withdraw tx sent: ${txResponse.hash}. Waiting for confirmation...`);
|
||||
try {
|
||||
const txReceipt = await txResponse.wait();
|
||||
if (txReceipt.status !== undefined) {
|
||||
console.log('Withdraw tx confirmed');
|
||||
} else {
|
||||
console.log('Withdraw tx failed');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) throw new Error('e is not an Error');
|
||||
console.error(`Deposit tx failed: ${e.message}`, {
|
||||
unsignedTx,
|
||||
});
|
||||
}
|
||||
}
|
||||
410
src/Unit/FarmingManager/index.ts
Normal file
410
src/Unit/FarmingManager/index.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
import { Exchange__factory, IUniswapV2Pair__factory, IUniswapV2Router__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { simpleFetch } from 'simple-typed-fetch';
|
||||
import type Unit from '../index.js';
|
||||
import BalanceGuard from '../../BalanceGuard.js';
|
||||
import { ADD_LIQUIDITY_GAS_LIMIT, INTERNAL_PROTOCOL_PRECISION, NATIVE_CURRENCY_PRECISION } from '../../constants/index.js';
|
||||
import { denormalizeNumber, normalizeNumber } from '../../utils/index.js';
|
||||
import getBalances from '../../utils/getBalances.js';
|
||||
import getNativeCryptocurrencyName from '../../utils/getNativeCryptocurrencyName.js';
|
||||
|
||||
const ADD_LIQUIDITY_SLIPPAGE = 0.05;
|
||||
|
||||
export type AddLiquidityParams = {
|
||||
poolName: string
|
||||
amountAsset: string
|
||||
amount: BigNumber.Value
|
||||
signer: ethers.Signer
|
||||
}
|
||||
|
||||
export type RemoveAllLiquidityParams = {
|
||||
poolName: string
|
||||
signer: ethers.Signer
|
||||
}
|
||||
|
||||
export default class FarmingManager {
|
||||
private readonly unit: Unit;
|
||||
|
||||
constructor(unit: Unit) {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public async addLiquidity({
|
||||
poolName,
|
||||
amountAsset,
|
||||
amount,
|
||||
signer,
|
||||
}: AddLiquidityParams) {
|
||||
const amountBN = new BigNumber(amount);
|
||||
if (amountBN.isNaN()) throw new Error('Invalid amount');
|
||||
if (amountBN.lte(0)) throw new Error('Amount must be greater than 0');
|
||||
if (!poolName.includes('-')) throw new Error('Pool name must be in the format of "assetA-AssetB"');
|
||||
const [assetA, assetB] = poolName.split('-');
|
||||
if (assetA === undefined) throw new Error('Asset A undefined');
|
||||
if (assetB === undefined) throw new Error('Asset B undefined');
|
||||
if (amountAsset !== assetA && amountAsset !== assetB) throw new Error('Amount asset must be either assetA or assetB');
|
||||
|
||||
const {
|
||||
exchangeContractAddress,
|
||||
assetToAddress,
|
||||
assetToDecimals,
|
||||
} = await simpleFetch(this.unit.blockchainService.getInfo)();
|
||||
|
||||
const walletAddress = await signer.getAddress();
|
||||
|
||||
const exchangeContract = Exchange__factory
|
||||
.connect(exchangeContractAddress, this.unit.provider);
|
||||
|
||||
const assetAAddress = assetToAddress[assetA];
|
||||
if (assetAAddress === undefined) throw new Error(`Asset '${assetA}' not found`);
|
||||
const assetBAddress = assetToAddress[assetB];
|
||||
if (assetBAddress === undefined) throw new Error(`Asset '${assetB}' not found`);
|
||||
|
||||
const assetADecimals = assetToDecimals[assetA];
|
||||
if (assetADecimals === undefined) throw new Error(`Decimals for asset '${assetA}' not found`);
|
||||
const assetBDecimals = assetToDecimals[assetB];
|
||||
if (assetBDecimals === undefined) throw new Error(`Decimals for asset '${assetB}' not found`);
|
||||
|
||||
const nativeCryptocurrency = getNativeCryptocurrencyName(assetToAddress);
|
||||
const balances = await getBalances(
|
||||
{
|
||||
[assetA]: assetAAddress,
|
||||
[assetB]: assetBAddress,
|
||||
[nativeCryptocurrency]: ethers.constants.AddressZero,
|
||||
},
|
||||
this.unit.aggregator,
|
||||
walletAddress,
|
||||
exchangeContract,
|
||||
this.unit.provider,
|
||||
);
|
||||
const balanceGuard = new BalanceGuard(
|
||||
balances,
|
||||
{
|
||||
address: ethers.constants.AddressZero,
|
||||
name: nativeCryptocurrency,
|
||||
},
|
||||
this.unit.provider,
|
||||
signer,
|
||||
);
|
||||
|
||||
const poolsConfig = await simpleFetch(this.unit.blockchainService.getPoolsConfig)();
|
||||
const pool = poolsConfig.pools[poolName];
|
||||
if (!pool) throw new Error(`Pool ${poolName} not found`);
|
||||
|
||||
const pairContract = IUniswapV2Pair__factory
|
||||
.connect(pool.lpTokenAddress, this.unit.provider);
|
||||
const routerContract = IUniswapV2Router__factory
|
||||
.connect(poolsConfig.routerAddress, this.unit.provider);
|
||||
|
||||
let pairTokensIsInversed = false;
|
||||
const token0 = await pairContract.token0();
|
||||
const wrappedNativeAddress = await routerContract.WETH();
|
||||
|
||||
// const token1 = await pairContract.token1();
|
||||
if (token0.toLowerCase() !== wrappedNativeAddress.toLowerCase()) pairTokensIsInversed = true;
|
||||
|
||||
const { _reserve0, _reserve1 } = await pairContract.getReserves();
|
||||
|
||||
const assetAReserve = pairTokensIsInversed ? _reserve1 : _reserve0;
|
||||
const assetBReserve = pairTokensIsInversed ? _reserve0 : _reserve1;
|
||||
|
||||
const denormalizedAssetAReserve = denormalizeNumber(assetAReserve, assetADecimals);
|
||||
const denormalizedAssetBReserve = denormalizeNumber(assetBReserve, assetBDecimals);
|
||||
|
||||
const price = denormalizedAssetBReserve.div(denormalizedAssetAReserve);
|
||||
|
||||
const assetAIsNativeCurrency = assetAAddress === ethers.constants.AddressZero;
|
||||
const assetBIsNativeCurrency = assetBAddress === ethers.constants.AddressZero;
|
||||
|
||||
const assetAAmount = assetA === amountAsset ? amountBN : amountBN.div(price);
|
||||
const assetBAmount = assetA === amountAsset ? amountBN.multipliedBy(price) : amountBN;
|
||||
|
||||
const assetAAmountWithSlippage = assetAAmount.multipliedBy(1 - ADD_LIQUIDITY_SLIPPAGE);
|
||||
const assetBAmountWithSlippage = assetBAmount.multipliedBy(1 - ADD_LIQUIDITY_SLIPPAGE);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: `${assetA} liquidity`,
|
||||
amount: assetAAmount.toString(),
|
||||
asset: {
|
||||
name: assetA,
|
||||
address: assetAAddress,
|
||||
},
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: ['exchange', 'wallet'],
|
||||
});
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: `${assetB} liquidity`,
|
||||
amount: assetBAmount.toString(),
|
||||
asset: {
|
||||
name: assetB,
|
||||
address: assetBAddress,
|
||||
},
|
||||
spenderAddress: exchangeContractAddress,
|
||||
sources: ['exchange', 'wallet'],
|
||||
});
|
||||
|
||||
const unsignedTx = await exchangeContract.populateTransaction.withdrawToPool(
|
||||
assetBIsNativeCurrency ? assetBAddress : assetAAddress,
|
||||
assetBIsNativeCurrency ? assetAAddress : assetBAddress,
|
||||
assetBIsNativeCurrency
|
||||
? normalizeNumber(assetBAmount, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR)
|
||||
: normalizeNumber(assetAAmount, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR),
|
||||
assetBIsNativeCurrency
|
||||
? normalizeNumber(assetAAmount, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR)
|
||||
: normalizeNumber(assetBAmount, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR),
|
||||
assetBIsNativeCurrency
|
||||
? normalizeNumber(assetBAmountWithSlippage, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR)
|
||||
: normalizeNumber(assetAAmountWithSlippage, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR),
|
||||
assetBIsNativeCurrency
|
||||
? normalizeNumber(assetAAmountWithSlippage, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR)
|
||||
: normalizeNumber(assetBAmountWithSlippage, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR),
|
||||
);
|
||||
|
||||
const gasPrice = await this.unit.provider.getGasPrice();
|
||||
|
||||
const transactionCost = ethers.BigNumber.from(ADD_LIQUIDITY_GAS_LIMIT).mul(gasPrice);
|
||||
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
amount: denormalizedTransactionCost.toString(),
|
||||
sources: ['wallet'],
|
||||
});
|
||||
|
||||
const nonce = await this.unit.provider.getTransactionCount(walletAddress, 'pending');
|
||||
|
||||
const network = await this.unit.provider.getNetwork();
|
||||
|
||||
if (assetAIsNativeCurrency || assetBIsNativeCurrency) {
|
||||
const contractBalance = balances[nativeCryptocurrency]?.exchange;
|
||||
if (!contractBalance) throw new Error(`No balance for '${nativeCryptocurrency}'`);
|
||||
const nativeAssetAmount = assetBIsNativeCurrency ? assetBAmount : assetAAmount;
|
||||
|
||||
if (nativeAssetAmount.gt(contractBalance)) {
|
||||
unsignedTx.value = normalizeNumber(
|
||||
nativeAssetAmount.minus(contractBalance),
|
||||
NATIVE_CURRENCY_PRECISION,
|
||||
BigNumber.ROUND_CEIL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsignedTx.chainId = network.chainId;
|
||||
unsignedTx.gasPrice = gasPrice;
|
||||
unsignedTx.nonce = nonce;
|
||||
unsignedTx.from = walletAddress;
|
||||
const gasLimit = await this.unit.provider.estimateGas(unsignedTx);
|
||||
unsignedTx.gasLimit = gasLimit;
|
||||
|
||||
await balanceGuard.check(true);
|
||||
|
||||
const signedTx = await signer.signTransaction(unsignedTx);
|
||||
const txResponse = await this.unit.provider.sendTransaction(signedTx);
|
||||
console.log(`Add liquidity tx sent: ${txResponse.hash}. Waiting for confirmation...`);
|
||||
const txReceipt = await txResponse.wait();
|
||||
if (txReceipt.status === 1) {
|
||||
console.log(`Add liquidity tx confirmed: ${txReceipt.transactionHash}`);
|
||||
} else {
|
||||
console.log(`Add liquidity tx failed: ${txReceipt.transactionHash}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async removeAllLiquidity({
|
||||
poolName,
|
||||
signer,
|
||||
}: RemoveAllLiquidityParams) {
|
||||
if (!poolName.includes('-')) throw new Error('Pool name must be in the format of "assetA-AssetB"');
|
||||
const [assetA, assetB] = poolName.split('-');
|
||||
if (assetA === undefined) throw new Error('Asset A is not defined');
|
||||
if (assetB === undefined) throw new Error('Asset B is not defined');
|
||||
|
||||
const {
|
||||
assetToAddress,
|
||||
assetToDecimals,
|
||||
exchangeContractAddress,
|
||||
} = await simpleFetch(this.unit.blockchainService.getInfo)();
|
||||
|
||||
const assetAAddress = assetToAddress[assetA];
|
||||
if (assetAAddress === undefined) throw new Error(`Asset '${assetA}' not found`);
|
||||
const assetBAddress = assetToAddress[assetB];
|
||||
if (assetBAddress === undefined) throw new Error(`Asset '${assetB}' not found`);
|
||||
|
||||
const assetADecimals = assetToDecimals[assetA];
|
||||
if (assetADecimals === undefined) throw new Error(`Decimals for asset '${assetA}' not found`);
|
||||
const assetBDecimals = assetToDecimals[assetB];
|
||||
if (assetBDecimals === undefined) throw new Error(`Decimals for asset '${assetB}' not found`);
|
||||
|
||||
const poolsConfig = await simpleFetch(this.unit.blockchainService.getPoolsConfig)();
|
||||
const pool = poolsConfig.pools[poolName];
|
||||
if (!pool) throw new Error(`Pool ${poolName} not found`);
|
||||
|
||||
const walletAddress = await signer.getAddress();
|
||||
|
||||
const exchangeContract = Exchange__factory
|
||||
.connect(exchangeContractAddress, this.unit.provider);
|
||||
const nativeCryptocurrency = getNativeCryptocurrencyName(assetToAddress);
|
||||
const balances = await getBalances(
|
||||
{
|
||||
[assetA]: assetAAddress,
|
||||
[assetB]: assetBAddress,
|
||||
[`${poolName} LP Token`]: pool.lpTokenAddress,
|
||||
[nativeCryptocurrency]: ethers.constants.AddressZero,
|
||||
},
|
||||
this.unit.aggregator,
|
||||
walletAddress,
|
||||
exchangeContract,
|
||||
this.unit.provider,
|
||||
);
|
||||
|
||||
const balanceGuard = new BalanceGuard(
|
||||
balances,
|
||||
{
|
||||
address: ethers.constants.AddressZero,
|
||||
name: nativeCryptocurrency,
|
||||
},
|
||||
this.unit.provider,
|
||||
signer,
|
||||
);
|
||||
|
||||
const pairContract = IUniswapV2Pair__factory
|
||||
.connect(pool.lpTokenAddress, this.unit.provider);
|
||||
|
||||
const { _reserve0, _reserve1 } = await pairContract.getReserves();
|
||||
|
||||
const routerContract = IUniswapV2Router__factory
|
||||
.connect(poolsConfig.routerAddress, this.unit.provider);
|
||||
|
||||
let pairTokensIsInversed = false;
|
||||
|
||||
const lpTokenUserBalance = await pairContract.balanceOf(walletAddress);
|
||||
const lpTokenDecimals = await pairContract.decimals();
|
||||
|
||||
const token0 = await pairContract.token0();
|
||||
const totalSupply = await pairContract.totalSupply();
|
||||
const wrappedNativeAddress = await routerContract.WETH();
|
||||
if (token0.toLowerCase() !== wrappedNativeAddress.toLowerCase()) pairTokensIsInversed = true;
|
||||
|
||||
const denormalizedLpTokenUserBalance = denormalizeNumber(lpTokenUserBalance, lpTokenDecimals);
|
||||
const denormalizedLpTokenSupply = denormalizeNumber(totalSupply, lpTokenDecimals);
|
||||
|
||||
const userShare = denormalizedLpTokenUserBalance.div(denormalizedLpTokenSupply);
|
||||
|
||||
const assetAReserve = pairTokensIsInversed ? _reserve1 : _reserve0;
|
||||
const assetBReserve = pairTokensIsInversed ? _reserve0 : _reserve1;
|
||||
|
||||
const denormalizedAssetAReserve = denormalizeNumber(assetAReserve, assetADecimals);
|
||||
const denormalizedAssetBReserve = denormalizeNumber(assetBReserve, assetBDecimals);
|
||||
|
||||
const denormalizedUserPooledAssetA = denormalizedAssetAReserve.multipliedBy(userShare);
|
||||
const denormalizedUserPooledAssetB = denormalizedAssetBReserve.multipliedBy(userShare);
|
||||
|
||||
const denormalizedUserPooledAssetAWithSlippage = denormalizedUserPooledAssetA.multipliedBy(1 - ADD_LIQUIDITY_SLIPPAGE);
|
||||
const denormalizedUserPooledAssetBWithSlippage = denormalizedUserPooledAssetB.multipliedBy(1 - ADD_LIQUIDITY_SLIPPAGE);
|
||||
|
||||
const assetAIsNativeCurrency = assetAAddress === ethers.constants.AddressZero;
|
||||
const assetBIsNativeCurrency = assetBAddress === ethers.constants.AddressZero;
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: `${poolName} liquidity`,
|
||||
asset: {
|
||||
name: `${poolName} LP Token`,
|
||||
address: pool.lpTokenAddress,
|
||||
},
|
||||
spenderAddress: poolsConfig.routerAddress,
|
||||
amount: denormalizedLpTokenUserBalance.toString(),
|
||||
sources: ['wallet'],
|
||||
});
|
||||
|
||||
let unsignedTx: ethers.PopulatedTransaction;
|
||||
if (assetAIsNativeCurrency || assetBIsNativeCurrency) {
|
||||
unsignedTx = await routerContract.populateTransaction.removeLiquidityETH(
|
||||
assetBIsNativeCurrency ? assetAAddress : assetBAddress, // token
|
||||
lpTokenUserBalance,
|
||||
assetBIsNativeCurrency
|
||||
? normalizeNumber(
|
||||
denormalizedUserPooledAssetAWithSlippage,
|
||||
assetADecimals,
|
||||
BigNumber.ROUND_FLOOR,
|
||||
)
|
||||
: normalizeNumber(
|
||||
denormalizedUserPooledAssetBWithSlippage,
|
||||
assetBDecimals,
|
||||
BigNumber.ROUND_FLOOR,
|
||||
), // token min
|
||||
assetBIsNativeCurrency
|
||||
? normalizeNumber(
|
||||
denormalizedUserPooledAssetBWithSlippage,
|
||||
assetBDecimals,
|
||||
BigNumber.ROUND_FLOOR,
|
||||
)
|
||||
: normalizeNumber(
|
||||
denormalizedUserPooledAssetAWithSlippage,
|
||||
assetADecimals,
|
||||
BigNumber.ROUND_FLOOR,
|
||||
), // eth min
|
||||
walletAddress,
|
||||
Math.floor(Date.now() / 1000) + 60 * 20,
|
||||
);
|
||||
} else {
|
||||
unsignedTx = await routerContract.populateTransaction.removeLiquidity(
|
||||
assetAAddress,
|
||||
assetBAddress,
|
||||
lpTokenUserBalance,
|
||||
normalizeNumber(
|
||||
denormalizedUserPooledAssetAWithSlippage,
|
||||
assetADecimals,
|
||||
BigNumber.ROUND_FLOOR,
|
||||
),
|
||||
normalizeNumber(
|
||||
denormalizedUserPooledAssetBWithSlippage,
|
||||
assetBDecimals,
|
||||
BigNumber.ROUND_FLOOR,
|
||||
),
|
||||
walletAddress,
|
||||
Math.floor(Date.now() / 1000) + 60 * 20,
|
||||
);
|
||||
}
|
||||
|
||||
const gasPrice = await this.unit.provider.getGasPrice();
|
||||
|
||||
const transactionCost = ethers.BigNumber.from(ADD_LIQUIDITY_GAS_LIMIT).mul(gasPrice);
|
||||
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
|
||||
|
||||
balanceGuard.registerRequirement({
|
||||
reason: 'Network fee',
|
||||
asset: {
|
||||
name: nativeCryptocurrency,
|
||||
address: ethers.constants.AddressZero,
|
||||
},
|
||||
amount: denormalizedTransactionCost.toString(),
|
||||
sources: ['wallet'],
|
||||
});
|
||||
|
||||
await balanceGuard.check(true);
|
||||
const nonce = await this.unit.provider.getTransactionCount(walletAddress, 'pending');
|
||||
const network = await this.unit.provider.getNetwork();
|
||||
|
||||
unsignedTx.chainId = network.chainId;
|
||||
unsignedTx.gasPrice = gasPrice;
|
||||
unsignedTx.nonce = nonce;
|
||||
unsignedTx.from = walletAddress;
|
||||
const gasLimit = await this.unit.provider.estimateGas(unsignedTx);
|
||||
unsignedTx.gasLimit = gasLimit;
|
||||
|
||||
const signedTx = await signer.signTransaction(unsignedTx);
|
||||
const txResponse = await this.unit.provider.sendTransaction(signedTx);
|
||||
console.log(`Remove all liquidity tx sent: ${txResponse.hash}. Waiting for confirmation...`);
|
||||
const txReceipt = await txResponse.wait();
|
||||
if (txReceipt.status === 1) {
|
||||
console.log(`Remove all liquidity tx confirmed: ${txReceipt.transactionHash}`);
|
||||
} else {
|
||||
console.log(`Remove all liquidity tx failed: ${txReceipt.transactionHash}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/Unit/index.ts
Normal file
84
src/Unit/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { ethers } from 'ethers';
|
||||
import { Aggregator } from '../services/Aggregator/index.js';
|
||||
import { BlockchainService } from '../services/BlockchainService/index.js';
|
||||
import { PriceFeed } from '../services/PriceFeed/index.js';
|
||||
import type { KnownEnv, SupportedChainId, VerboseUnitConfig } from '../types.js';
|
||||
import Exchange from './Exchange/index.js';
|
||||
import FarmingManager from './FarmingManager/index.js';
|
||||
import { chains, envs } from '../config/index.js';
|
||||
import type { networkCodes } from '../constants/index.js';
|
||||
|
||||
type KnownConfig = {
|
||||
env: KnownEnv
|
||||
chainId: SupportedChainId
|
||||
}
|
||||
|
||||
export default class Unit {
|
||||
public readonly networkCode: typeof networkCodes[number];
|
||||
|
||||
public readonly chainId: SupportedChainId;
|
||||
|
||||
public readonly provider: ethers.providers.StaticJsonRpcProvider;
|
||||
|
||||
public readonly blockchainService: BlockchainService;
|
||||
|
||||
public readonly aggregator: Aggregator;
|
||||
|
||||
public readonly priceFeed: PriceFeed;
|
||||
|
||||
public readonly exchange: Exchange;
|
||||
|
||||
public readonly farmingManager: FarmingManager;
|
||||
|
||||
public readonly config: VerboseUnitConfig;
|
||||
|
||||
constructor(config: KnownConfig | VerboseUnitConfig) {
|
||||
if ('env' in config) {
|
||||
const staticConfig = envs[config.env];
|
||||
if (!staticConfig) throw new Error(`Invalid environment: ${config.env}. Available environments: ${Object.keys(envs).join(', ')}`);
|
||||
|
||||
const chainConfig = chains[config.chainId];
|
||||
if (!chainConfig) throw new Error(`Invalid chainId: ${config.chainId}. Available chainIds: ${Object.keys(chains).join(', ')}`);
|
||||
|
||||
const networkConfig = staticConfig.networks[config.chainId];
|
||||
if (!networkConfig) throw new Error(`Invalid chainId: ${config.chainId}. Available chainIds: ${Object.keys(staticConfig.networks).join(', ')}`);
|
||||
|
||||
this.config = {
|
||||
chainId: config.chainId,
|
||||
nodeJsonRpc: networkConfig.rpc ?? chainConfig.rpc,
|
||||
services: {
|
||||
blockchainService: {
|
||||
http: networkConfig.api + networkConfig.services.blockchain.http,
|
||||
},
|
||||
aggregator: {
|
||||
http: networkConfig.api + networkConfig.services.aggregator.http,
|
||||
ws: networkConfig.api + networkConfig.services.aggregator.ws,
|
||||
},
|
||||
priceFeed: {
|
||||
api: networkConfig.api + networkConfig.services.priceFeed.all,
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
this.config = config;
|
||||
}
|
||||
const chainInfo = chains[config.chainId];
|
||||
if (!chainInfo) throw new Error('Chain info is required');
|
||||
|
||||
this.chainId = config.chainId;
|
||||
this.networkCode = chainInfo.code;
|
||||
const intNetwork = parseInt(this.chainId, 10);
|
||||
if (Number.isNaN(intNetwork)) throw new Error('Invalid chainId (not a number)' + this.chainId);
|
||||
this.provider = new ethers.providers.StaticJsonRpcProvider(this.config.nodeJsonRpc, intNetwork);
|
||||
this.provider.pollingInterval = 1000;
|
||||
|
||||
this.blockchainService = new BlockchainService(this.config.services.blockchainService.http);
|
||||
this.aggregator = new Aggregator(
|
||||
this.config.services.aggregator.http,
|
||||
this.config.services.aggregator.ws,
|
||||
);
|
||||
this.priceFeed = new PriceFeed(this.config.services.priceFeed.api);
|
||||
this.exchange = new Exchange(this);
|
||||
this.farmingManager = new FarmingManager(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user