Swap limit added

This commit is contained in:
Aleksandr Kraiz
2023-02-23 17:53:20 +04:00
parent c67ff0725e
commit f97e10b591
7 changed files with 523 additions and 37 deletions

View File

@@ -37,6 +37,7 @@ Orions SDK is free to use and does not require an API key or registration. Re
- [Withdraw](#withdraw)
- [Deposit](#deposit)
- [Get swap info](#get-swap-info)
- [Make swap limit](#make-swap-limit)
- [Make swap market](#make-swap-market)
- [Add liquidity](#add-liquidity)
- [Remove all liquidity](#remove-all-liquidity)
@@ -223,6 +224,45 @@ console.log(fee);
// }
```
### Make swap limit
```ts
// Each trading pair has its own quantity precision
// You need to prepare (round) the quantity according to quantity precision
const pairConfig = await simpleFetch(orionAggregator.getPairConfig)("ORN-USDT");
if (!pairConfig) throw new Error(`Pair config ORN-USDT not found`);
const { qtyPrecision } = pairConfig;
const amount = 23.5346563;
const roundedAmount = new BigNumber(amount).decimalPlaces(
qtyPrecision,
BigNumber.ROUND_FLOOR
); // You can use your own Math lib
orionUnit.exchange
.swapLimit({
type: "exactSpend",
assetIn: "ORN",
assetOut: "USDT",
feeAsset: "ORN",
amount: roundedAmount.toNumber(),
price: 20,
signer: wallet, // or signer when UI
options: {
// All options are optional 🙂
poolOnly: true, // You can specify whether you want to perform the exchange only through the pool
instantSettlement: true, // Set true to ensure that funds can be instantly transferred to wallet (otherwise, there is a possibility of receiving funds to the balance of the exchange contract)
logger: console.log,
// Set it to true if you want the issues associated with
// the lack of allowance to be automatically corrected
autoApprove: true,
},
})
.then(console.log);
```
### Make swap market
```ts

View File

@@ -1,6 +1,6 @@
{
"name": "@orionprotocol/sdk",
"version": "0.17.19",
"version": "0.17.20",
"description": "Orion Protocol SDK",
"main": "./lib/esm/index.js",
"module": "./lib/esm/index.js",

View File

@@ -1,10 +1,13 @@
import type OrionUnit from '..';
import deposit, { type DepositParams } from './deposit';
import getSwapInfo, { type GetSwapInfoParams } from './getSwapInfo';
import type { SwapLimitParams } from './swapLimit';
import swapLimit from './swapLimit';
import swapMarket, { type SwapMarketParams } from './swapMarket';
import withdraw, { type WithdrawParams } from './withdraw';
type PureSwapMarketParams = Omit<SwapMarketParams, 'orionUnit'>
type PureSwapLimitParams = Omit<SwapLimitParams, 'orionUnit'>
type PureDepositParams = Omit<DepositParams, 'orionUnit'>
type PureWithdrawParams = Omit<WithdrawParams, 'orionUnit'>
type PureGetSwapMarketInfoParams = Omit<GetSwapInfoParams, 'orionBlockchain' | 'orionAggregator'>
@@ -16,6 +19,13 @@ export default class Exchange {
this.orionUnit = orionUnit;
}
public swapLimit(params: PureSwapLimitParams) {
return swapLimit({
...params,
orionUnit: this.orionUnit,
});
}
public swapMarket(params: PureSwapMarketParams) {
return swapMarket({
...params,

View File

@@ -0,0 +1,469 @@
import BigNumber from 'bignumber.js';
import { ethers } from 'ethers';
import { Exchange__factory } from '@orionprotocol/contracts';
import getBalances from '../../utils/getBalances';
import BalanceGuard from '../../BalanceGuard';
import getAvailableSources from '../../utils/getAvailableFundsSources';
import type OrionUnit from '..';
import { INTERNAL_ORION_PRECISION, NATIVE_CURRENCY_PRECISION, SWAP_THROUGH_ORION_POOL_GAS_LIMIT } from '../../constants';
import getNativeCryptocurrency from '../../utils/getNativeCryptocurrency';
import simpleFetch from '../../simpleFetch';
import { calculateFeeInFeeAsset, denormalizeNumber, normalizeNumber } from '../../utils';
import { signOrder } from '../../crypt';
import type orderSchema from '../../services/OrionAggregator/schemas/orderSchema';
import type { z } from 'zod';
export type SwapLimitParams = {
type: 'exactSpend' | 'exactReceive'
assetIn: string
assetOut: string
price: BigNumber.Value
amount: BigNumber.Value
feeAsset: string
signer: ethers.Signer
orionUnit: OrionUnit
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: 'orion_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,
orionUnit,
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 {
orionBlockchain, orionAggregator, provider, chainId,
} = orionUnit;
const {
exchangeContractAddress,
matcherAddress,
assetToAddress,
} = await simpleFetch(orionBlockchain.getInfo)();
const nativeCryptocurrency = getNativeCryptocurrency(assetToAddress);
const exchangeContract = Exchange__factory.connect(exchangeContractAddress, provider);
const feeAssets = await simpleFetch(orionBlockchain.getTokensFee)();
const pricesInOrn = await simpleFetch(orionBlockchain.getPrices)();
const gasPriceWei = await simpleFetch(orionBlockchain.getGasPriceWei)();
const { factories } = await simpleFetch(orionBlockchain.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,
},
orionAggregator,
walletAddress,
exchangeContract,
provider,
);
const balanceGuard = new BalanceGuard(
balances,
{
name: nativeCryptocurrency,
address: ethers.constants.AddressZero,
},
provider,
signer,
options?.logger,
);
const swapInfo = await simpleFetch(orionAggregator.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(orionAggregator.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, 'orion_pool'),
});
const amountReceive = swapInfo.type === 'exactReceive'
? swapInfo.amountOut
: new BigNumber(swapInfo.orderInfo.amount).multipliedBy(swapInfo.orderInfo.safePrice)
const amountSpendBlockchainParam = normalizeNumber(
amountSpend,
INTERNAL_ORION_PRECISION,
BigNumber.ROUND_CEIL,
);
const amountReceiveBlockchainParam = normalizeNumber(
amountReceive,
INTERNAL_ORION_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_ORION_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, 'orion_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, 'orion_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: 'orion_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 { orionFeeInFeeAsset, 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: orionFeeInFeeAsset,
spenderAddress: exchangeContractAddress,
sources: getAvailableSources('orion_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(orionAggregator.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(orionAggregator.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,
};
}

View File

@@ -4,7 +4,6 @@ import { Exchange__factory } from '@orionprotocol/contracts';
import getBalances from '../../utils/getBalances';
import BalanceGuard from '../../BalanceGuard';
import getAvailableSources from '../../utils/getAvailableFundsSources';
import type OrionUnit from '..';
import { INTERNAL_ORION_PRECISION, NATIVE_CURRENCY_PRECISION, SWAP_THROUGH_ORION_POOL_GAS_LIMIT } from '../../constants';
import getNativeCryptocurrency from '../../utils/getNativeCryptocurrency';
import simpleFetch from '../../simpleFetch';
@@ -12,26 +11,10 @@ import { calculateFeeInFeeAsset, denormalizeNumber, normalizeNumber } from '../.
import { signOrder } from '../../crypt';
import type orderSchema from '../../services/OrionAggregator/schemas/orderSchema';
import type { z } from 'zod';
import type { SwapLimitParams } from './swapLimit';
export type SwapMarketParams = {
type: 'exactSpend' | 'exactReceive'
assetIn: string
assetOut: string
amount: BigNumber.Value
feeAsset: string
export type SwapMarketParams = Omit<SwapLimitParams, 'price'> & {
slippagePercent: BigNumber.Value
signer: ethers.Signer
orionUnit: OrionUnit
options?: {
// rounding?: 'up' | 'down' TODO
poolOnly?: boolean
instantSettlement?: boolean
logger?: (message: string) => void
autoApprove?: boolean
developer?: {
route?: 'aggregator' | 'pool'
}
}
}
type AggregatorOrder = {

View File

@@ -101,22 +101,6 @@
},
"liquidityMigratorAddress": "0x01b10dds12478C88A5E18e2707E729906bC25CfF6"
},
"3": {
"api": "https://testing.orionprotocol.io/eth-ropsten",
"services": {
"aggregator": {
"http": "/backend",
"ws": "/v1"
},
"blockchain": {
"http": ""
},
"priceFeed": {
"all": "/price-feed"
}
},
"liquidityMigratorAddress": "0x36969a25622AE31bA9946e0c8151f0dc08b3A1c8"
},
"5": {
"api": "https://testing.orionprotocol.io/eth-goerli",
"services": {

View File

@@ -4,7 +4,7 @@
"./src/index.ts"
],
"include": [
"./src/**/*.ts",
"./src/**/*.ts"
],
"exclude": [
"node_modules",