Merge remote-tracking branch 'origin/main' into op-4438/pf-cex-prices

This commit is contained in:
Kirill Litvinov
2024-03-07 16:12:21 +03:00
102 changed files with 11453 additions and 1715 deletions

View File

@@ -1,7 +1,7 @@
import { BigNumber } from 'bignumber.js';
import { ethers } from 'ethers';
import clone from 'just-clone';
import { ERC20__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { ERC20__factory } from '@orionprotocol/contracts/lib/ethers-v6/index.js';
import { APPROVE_ERC20_GAS_LIMIT, NATIVE_CURRENCY_PRECISION } from './constants/index.js';
import type {
AggregatedBalanceRequirement, ApproveFix, Asset, BalanceIssue, BalanceRequirement, Source,
@@ -9,6 +9,8 @@ import type {
import { denormalizeNumber } from './utils/index.js';
import arrayEquals from './utils/arrayEquals.js';
// BalanceGuard helps to check if there is enough balance to perform a swap
// Also it can fix some balance issues (e.g. approve tokens)
export default class BalanceGuard {
private readonly balances: Partial<
Record<
@@ -24,7 +26,7 @@ export default class BalanceGuard {
private readonly nativeCryptocurrency: Asset;
private readonly provider: ethers.providers.Provider;
private readonly provider: ethers.Provider;
private readonly signer: ethers.Signer;
@@ -33,7 +35,7 @@ export default class BalanceGuard {
constructor(
balances: Partial<Record<string, Record<'exchange' | 'wallet', BigNumber>>>,
nativeCryptocurrency: Asset,
provider: ethers.providers.Provider,
provider: ethers.Provider,
signer: ethers.Signer,
logger?: (message: string) => void,
) {
@@ -55,6 +57,8 @@ export default class BalanceGuard {
assetBalance[source] = assetBalance[source].plus(amount);
}
// Reset is approve some token to zero
// This operation may be required by some tokens, e.g. USDT
private async checkResetRequired(
assetAddress: string,
spenderAddress: string,
@@ -62,10 +66,10 @@ export default class BalanceGuard {
const walletAddress = await this.signer.getAddress();
const tokenContract = ERC20__factory
.connect(assetAddress, this.provider);
const unsignedTx = await tokenContract.populateTransaction
.approve(
const unsignedTx = await tokenContract.approve
.populateTransaction(
spenderAddress,
ethers.constants.MaxUint256,
ethers.MaxUint256,
);
unsignedTx.from = walletAddress;
let resetRequired = false;
@@ -109,6 +113,7 @@ export default class BalanceGuard {
}, []);
}
// This method can fix some balance issues (e.g. approve tokens)
private readonly fixAllAutofixableBalanceIssues = async (
balanceIssues: BalanceIssue[],
) => {
@@ -117,21 +122,23 @@ export default class BalanceGuard {
const approve = async ({ spenderAddress, targetAmount }: ApproveFix) => {
const bnTargetAmount = new BigNumber(targetAmount);
const unsignedApproveTx = await tokenContract
.populateTransaction
.approve(
.approve.populateTransaction(
spenderAddress,
bnTargetAmount.isZero()
? '0' // Reset
: ethers.constants.MaxUint256, // Infinite approve
: ethers.MaxUint256, // Infinite approve
);
const walletAddress = await this.signer.getAddress();
const nonce = await this.provider.getTransactionCount(walletAddress, 'pending');
const gasPrice = await this.provider.getGasPrice();
const { gasPrice, maxFeePerGas } = await this.provider.getFeeData();
const network = await this.provider.getNetwork();
if (gasPrice !== null && maxFeePerGas !== null) {
unsignedApproveTx.gasPrice = gasPrice;
unsignedApproveTx.maxFeePerGas = maxFeePerGas;
}
unsignedApproveTx.chainId = network.chainId;
unsignedApproveTx.gasPrice = gasPrice;
unsignedApproveTx.nonce = nonce;
unsignedApproveTx.from = walletAddress;
const gasLimit = await this.provider.estimateGas(unsignedApproveTx);
@@ -139,7 +146,7 @@ export default class BalanceGuard {
this.logger?.('Approve transaction signing...');
const signedTx = await this.signer.signTransaction(unsignedApproveTx);
const txResponse = await this.provider.sendTransaction(signedTx);
const txResponse = await this.provider.broadcastTransaction(signedTx);
this.logger?.(`${issue.asset.name} approve transaction sent ${txResponse.hash}. Waiting for confirmation...`);
await txResponse.wait();
this.logger?.(`${issue.asset.name} approve transaction confirmed.`);
@@ -161,6 +168,7 @@ export default class BalanceGuard {
return balanceIssues.filter((item) => !autofixableBalanceIssues.includes(item));
};
// Check that all balance requirements are fulfilled
async check(fixAutofixable?: boolean) {
this.logger?.(`Balance requirements: ${this.requirements
.map((requirement) => `${requirement.amount} ${requirement.asset.name} ` +
@@ -171,6 +179,7 @@ export default class BalanceGuard {
const remainingBalances = clone(this.balances);
const aggregatedRequirements = BalanceGuard.aggregateBalanceRequirements(this.requirements);
// DO NOT IGNORE THIS COMMENT. THIS IS VERY IMPORTANT TO UNDERSTAND THIS CODE
// Balance absorption order is important!
// 1. Exchange-contract only
// 2. Exchange + wallet (can produce approves requirements)
@@ -230,7 +239,7 @@ export default class BalanceGuard {
const lackAmount = remainingBalance.exchange.abs(); // e.g. -435.234234 to 434.234234
let denormalizedAllowance: BigNumber;
if (asset.address === ethers.constants.AddressZero) {
if (asset.address === ethers.ZeroAddress) {
denormalizedAllowance = remainingBalance.wallet;
} else {
if (spenderAddress === undefined) throw new Error(`Spender address is required for ${asset.name}`);
@@ -272,13 +281,11 @@ export default class BalanceGuard {
asset.address,
spenderAddress,
);
const gasPriceWei = await this.provider.getGasPrice();
const approveTransactionCost = ethers.BigNumber
.from(APPROVE_ERC20_GAS_LIMIT)
.mul(gasPriceWei);
const { gasPrice: gasPriceWei } = await this.provider.getFeeData();
const approveTransactionCost = BigInt(APPROVE_ERC20_GAS_LIMIT) * (gasPriceWei ?? 0n);
const denormalizedApproveTransactionCost = denormalizeNumber(
approveTransactionCost,
NATIVE_CURRENCY_PRECISION
BigInt(NATIVE_CURRENCY_PRECISION)
);
requiredApproves.items = {
@@ -321,6 +328,7 @@ export default class BalanceGuard {
const walletTokensAggregatedRequirements = flattedAggregatedRequirements
.filter(({ sources, asset }) => sources[0] === 'wallet' && asset.name !== this.nativeCryptocurrency.name);
// This requirements can be fulfilled by wallet
await Promise.all(walletTokensAggregatedRequirements
.map(async ({ asset, spenderAddress, items }) => {
const remainingBalance = remainingBalances[asset.name];
@@ -329,7 +337,7 @@ export default class BalanceGuard {
.reduce<BigNumber>((p, c) => (c !== undefined ? p.plus(c) : p), new BigNumber(0));
let denormalizedAllowance: BigNumber;
if (asset.address === ethers.constants.AddressZero) {
if (asset.address === ethers.ZeroAddress) {
denormalizedAllowance = remainingBalance.wallet;
} else {
if (spenderAddress === undefined) throw new Error(`Spender address is required for ${asset.name}`);
@@ -365,13 +373,11 @@ export default class BalanceGuard {
asset.address,
spenderAddress,
);
const gasPriceWei = await this.provider.getGasPrice();
const approveTransactionCost = ethers.BigNumber
.from(APPROVE_ERC20_GAS_LIMIT)
.mul(gasPriceWei);
const { gasPrice: gasPriceWei } = await this.provider.getFeeData();
const approveTransactionCost = BigInt(APPROVE_ERC20_GAS_LIMIT) * (gasPriceWei ?? 0n);
const denormalizedApproveTransactionCost = denormalizeNumber(
approveTransactionCost,
NATIVE_CURRENCY_PRECISION
BigInt(NATIVE_CURRENCY_PRECISION)
);
requiredApproves.items = {

View File

@@ -5,10 +5,10 @@ import { isValidChainId } from '../../utils/index.js';
import { simpleFetch } from 'simple-typed-fetch';
import bsonObjectId from 'bson-objectid';
const ObjectID = bsonObjectId.default
const ObjectID = bsonObjectId;
const getHistory = async (units: Unit[], address: string, limit = 1000) => {
if (!ethers.utils.isAddress(address)) throw new Error(`Invalid address: ${address}`);
if (!ethers.isAddress(address)) throw new Error(`Invalid address: ${address}`);
const data = await Promise.all(units.map(async ({ blockchainService, aggregator, chainId }) => {
const sourceNetworkHistory = await simpleFetch(blockchainService.getSourceAtomicSwapHistory)({
limit,

View File

@@ -229,7 +229,7 @@ export default class Bridge {
env?: string | undefined,
) {
const secret = generateSecret();
const secretHash = ethers.utils.keccak256(secret);
const secretHash = ethers.keccak256(secret);
const lockExpiration = Date.now() + SECONDS_IN_DAY * EXPIRATION_DAYS * 1000;
return {

View File

@@ -1,6 +1,6 @@
import { BigNumber } from 'bignumber.js';
import { ethers } from 'ethers';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v6/index.js';
import getBalances from '../../utils/getBalances.js';
import BalanceGuard from '../../BalanceGuard.js';
import getAvailableSources from '../../utils/getAvailableFundsSources.js';
@@ -123,7 +123,7 @@ export default async function swap({
const sourceChainBalances = await getBalances(
{
[assetName]: sourceChainAssetAddress,
[sourceChainNativeCryptocurrency]: ethers.constants.AddressZero,
[sourceChainNativeCryptocurrency]: ethers.ZeroAddress,
},
sourceAggregator,
walletAddress,
@@ -134,7 +134,7 @@ export default async function swap({
const targetChainBalances = await getBalances(
{
[assetName]: targetChainAssetAddress,
[targetChainNativeCryptocurrency]: ethers.constants.AddressZero,
[targetChainNativeCryptocurrency]: ethers.ZeroAddress,
},
targetAggregator,
walletAddress,
@@ -146,7 +146,7 @@ export default async function swap({
sourceChainBalances,
{
name: sourceChainNativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
sourceProvider,
signer,
@@ -157,7 +157,7 @@ export default async function swap({
targetChainBalances,
{
name: targetChainNativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
targetProvider,
signer,
@@ -181,33 +181,33 @@ export default async function swap({
BigNumber.ROUND_FLOOR,
);
const secret = generateSecret();
const secretHash = ethers.utils.keccak256(secret);
const secretHash = ethers.keccak256(secret);
options?.logger?.(`Secret is ${secret}`);
options?.logger?.(`Secret hash is ${secretHash}`);
const secondsInDay = 60 * 60 * 24;
const expirationDays = 4;
const expirationEtherBN = ethers.BigNumber.from(
const expirationEtherBN = BigInt(
Date.now() + (secondsInDay * expirationDays * 1000),
);
const unsignedLockAtomicTx = await sourceExchangeContract.populateTransaction.lockAtomic({
const unsignedLockAtomicTx = await sourceExchangeContract.lockAtomic.populateTransaction({
amount: amountBlockchainParam,
asset: sourceChainAssetAddress,
expiration: expirationEtherBN,
secretHash,
sender: walletAddress,
targetChainId: ethers.BigNumber.from(targetChain),
targetChainId: BigInt(targetChain),
});
let sourceChainGasPrice: ethers.BigNumber;
let sourceChainGasPrice: bigint;
const sourceChainFeeData = await sourceChainUnit.provider.getFeeData();
if (ethers.BigNumber.isBigNumber(sourceChainFeeData.gasPrice)) { //
if (sourceChainFeeData.gasPrice !== null) { //
unsignedLockAtomicTx.gasPrice = sourceChainFeeData.gasPrice;
sourceChainGasPrice = sourceChainFeeData.gasPrice;
} else if (
ethers.BigNumber.isBigNumber(sourceChainFeeData.maxFeePerGas) &&
ethers.BigNumber.isBigNumber(sourceChainFeeData.maxPriorityFeePerGas)
sourceChainFeeData.maxFeePerGas !== null &&
sourceChainFeeData.maxPriorityFeePerGas !== null
) { // EIP-1559
unsignedLockAtomicTx.maxFeePerGas = sourceChainFeeData.maxFeePerGas;
unsignedLockAtomicTx.maxPriorityFeePerGas = sourceChainFeeData.maxPriorityFeePerGas;
@@ -216,7 +216,7 @@ export default async function swap({
throw new Error('Can\'t get gas price');
}
unsignedLockAtomicTx.chainId = parseInt(chainId, 10);
unsignedLockAtomicTx.chainId = BigInt(chainId);
unsignedLockAtomicTx.from = walletAddress;
let value = new BigNumber(0);
@@ -230,16 +230,16 @@ export default async function swap({
NATIVE_CURRENCY_PRECISION,
BigNumber.ROUND_CEIL,
);
unsignedLockAtomicTx.gasLimit = ethers.BigNumber.from(LOCKATOMIC_GAS_LIMIT);
unsignedLockAtomicTx.gasLimit = BigInt(LOCKATOMIC_GAS_LIMIT);
const transactionCost = ethers.BigNumber.from(LOCKATOMIC_GAS_LIMIT).mul(sourceChainGasPrice);
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
const transactionCost = BigInt(LOCKATOMIC_GAS_LIMIT) * sourceChainGasPrice;
const denormalizedTransactionCost = denormalizeNumber(transactionCost, BigInt(NATIVE_CURRENCY_PRECISION));
sourceChainBalanceGuard.registerRequirement({
reason: 'Network fee',
asset: {
name: sourceChainNativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
amount: denormalizedTransactionCost.toString(),
sources: ['wallet']
@@ -252,7 +252,7 @@ export default async function swap({
options?.logger?.('Signing lock tx transaction...');
const signedTransaction = await signer.signTransaction(unsignedLockAtomicTx);
const lockAtomicTxResponse = await sourceChainUnit.provider.sendTransaction(signedTransaction);
const lockAtomicTxResponse = await sourceChainUnit.provider.broadcastTransaction(signedTransaction);
options?.logger?.(`Lock tx sent. Tx hash: ${lockAtomicTxResponse.hash}. Waiting for tx to be mined...`);
await lockAtomicTxResponse.wait();
options?.logger?.('Lock tx mined.');
@@ -280,7 +280,7 @@ export default async function swap({
options?.logger?.('Atomic swap placed.');
// const targetChainGasPriceWei = await simpleFetch(targetBlockchainService.getGasPriceWei)();
const unsignedRedeemAtomicTx = await targetExchangeContract.populateTransaction.redeemAtomic(
const unsignedRedeemAtomicTx = await targetExchangeContract.redeemAtomic.populateTransaction(
{
amount: amountBlockchainParam,
asset: targetChainAssetAddress,
@@ -294,14 +294,14 @@ export default async function swap({
secret
)
let targetChainGasPrice: ethers.BigNumber;
let targetChainGasPrice: bigint;
const targetChainFeeData = await targetChainUnit.provider.getFeeData();
if (ethers.BigNumber.isBigNumber(targetChainFeeData.gasPrice)) { //
if (targetChainFeeData.gasPrice !== null) { //
unsignedRedeemAtomicTx.gasPrice = targetChainFeeData.gasPrice;
targetChainGasPrice = targetChainFeeData.gasPrice;
} else if (
ethers.BigNumber.isBigNumber(targetChainFeeData.maxFeePerGas) &&
ethers.BigNumber.isBigNumber(targetChainFeeData.maxPriorityFeePerGas)
targetChainFeeData.maxFeePerGas !== null &&
targetChainFeeData.maxPriorityFeePerGas !== null
) { // EIP-1559
unsignedRedeemAtomicTx.maxFeePerGas = targetChainFeeData.maxFeePerGas;
unsignedRedeemAtomicTx.maxPriorityFeePerGas = targetChainFeeData.maxPriorityFeePerGas;
@@ -310,18 +310,18 @@ export default async function swap({
throw new Error('Can\'t get gas price');
}
unsignedRedeemAtomicTx.chainId = parseInt(targetChain, 10);
unsignedRedeemAtomicTx.chainId = BigInt(parseInt(targetChain, 10));
unsignedRedeemAtomicTx.from = walletAddress;
unsignedRedeemAtomicTx.gasLimit = ethers.BigNumber.from(REDEEMATOMIC_GAS_LIMIT);
unsignedRedeemAtomicTx.gasLimit = BigInt(REDEEMATOMIC_GAS_LIMIT);
const redeemAtomicTransactionCost = ethers.BigNumber.from(REDEEMATOMIC_GAS_LIMIT).mul(targetChainGasPrice);
const targetDenormalizedTransactionCost = denormalizeNumber(redeemAtomicTransactionCost, NATIVE_CURRENCY_PRECISION);
const redeemAtomicTransactionCost = BigInt(REDEEMATOMIC_GAS_LIMIT) * targetChainGasPrice;
const targetDenormalizedTransactionCost = denormalizeNumber(redeemAtomicTransactionCost, BigInt(NATIVE_CURRENCY_PRECISION));
targetChainBalanceGuard.registerRequirement({
reason: 'Network fee',
asset: {
name: targetChainNativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
amount: targetDenormalizedTransactionCost.toString(),
sources: ['wallet']
@@ -334,7 +334,7 @@ export default async function swap({
options?.logger?.('Signing redeem tx transaction...');
const targetSignedTransaction = await signer.signTransaction(unsignedRedeemAtomicTx);
const targetLockAtomicTxResponse = await targetChainUnit.provider.sendTransaction(targetSignedTransaction);
const targetLockAtomicTxResponse = await targetChainUnit.provider.broadcastTransaction(targetSignedTransaction);
options?.logger?.(`Redeem tx sent. Tx hash: ${targetLockAtomicTxResponse.hash}. Waiting for tx to be mined...`);
await targetLockAtomicTxResponse.wait();
@@ -343,16 +343,16 @@ export default async function swap({
if (options?.withdrawToWallet !== undefined && options.withdrawToWallet) {
options.logger?.('Withdrawing to wallet...');
const unsignedWithdrawTx = await targetExchangeContract.populateTransaction.withdraw(
const unsignedWithdrawTx = await targetExchangeContract.withdraw.populateTransaction(
targetChainAssetAddress,
amountBlockchainParam,
);
if (ethers.BigNumber.isBigNumber(targetChainFeeData.gasPrice)) { //
if (targetChainFeeData.gasPrice !== null) { //
unsignedWithdrawTx.gasPrice = targetChainFeeData.gasPrice;
targetChainGasPrice = targetChainFeeData.gasPrice;
} else if (
ethers.BigNumber.isBigNumber(targetChainFeeData.maxFeePerGas) &&
ethers.BigNumber.isBigNumber(targetChainFeeData.maxPriorityFeePerGas)
targetChainFeeData.maxFeePerGas !== null &&
targetChainFeeData.maxPriorityFeePerGas !== null
) { // EIP-1559
unsignedWithdrawTx.maxFeePerGas = targetChainFeeData.maxFeePerGas;
unsignedWithdrawTx.maxPriorityFeePerGas = targetChainFeeData.maxPriorityFeePerGas;
@@ -360,12 +360,12 @@ export default async function swap({
} else {
throw new Error('Can\'t get gas price');
}
unsignedWithdrawTx.chainId = parseInt(targetChain, 10);
unsignedWithdrawTx.gasLimit = ethers.BigNumber.from(WITHDRAW_GAS_LIMIT);
unsignedWithdrawTx.chainId = BigInt(parseInt(targetChain, 10));
unsignedWithdrawTx.gasLimit = BigInt(WITHDRAW_GAS_LIMIT);
unsignedWithdrawTx.from = walletAddress;
unsignedWithdrawTx.nonce = await targetProvider.getTransactionCount(walletAddress, 'pending');
const signedTx = await signer.signTransaction(unsignedWithdrawTx);
const withdrawTx = await targetProvider.sendTransaction(signedTx);
const withdrawTx = await targetProvider.broadcastTransaction(signedTx);
options.logger?.(`Withdraw tx sent. Tx hash: ${withdrawTx.hash}. Waiting for tx to be mined...`);
await withdrawTx.wait();
options.logger?.('Withdraw tx mined.');

View File

@@ -58,6 +58,9 @@ export default class Orion {
priceFeed: {
api: networkConfig.api + networkConfig.services.priceFeed.all,
},
indexer: {
api: networkConfig.api + networkConfig.services.indexer?.http,
}
},
};
})

View File

@@ -0,0 +1,49 @@
import {
SwapExecutor__factory,
CurveRegistry__factory,
ERC20__factory,
} from "@orionprotocol/contracts/lib/ethers-v6/index.js";
import { MaxUint256, type BigNumberish, type JsonRpcProvider } from "ethers";
import { addCallParams, pathCallWithBalance } from "./utils.js";
import type { SingleSwap } from "../../../types.js";
import { generateApproveCall } from "./erc20.js";
import type { BytesLike } from "ethers";
export async function generateCurveStableSwapCall(
amount: BigNumberish,
to: string,
swap: SingleSwap,
provider: JsonRpcProvider,
swapExecutorContractAddress: string,
curveRegistry: string,
pathWithBalance = false
) {
const executorInterface = SwapExecutor__factory.createInterface();
const registry = CurveRegistry__factory.connect(curveRegistry, provider);
const { pool, assetIn, assetOut } = swap;
const firstToken = ERC20__factory.connect(assetIn, provider)
const executorAllowance = await firstToken.allowance(swapExecutorContractAddress, pool)
const calls: BytesLike[] = []
if (executorAllowance <= BigInt(amount)) {
const approveCall = await generateApproveCall(
assetIn,
pool,
MaxUint256
);
calls.push(approveCall);
}
const [i, j] = await registry.get_coin_indices(pool, assetIn, assetOut);
let calldata = executorInterface.encodeFunctionData(
"curveSwapStableAmountIn",
[pool, assetOut, i, j, to, amount]
);
calldata = addCallParams(calldata)
if (pathWithBalance) {
calldata = pathCallWithBalance(calldata, swap.assetIn)
}
calls.push(calldata)
return calls
}

View File

@@ -0,0 +1,36 @@
import { SwapExecutor__factory } from "@orionprotocol/contracts/lib/ethers-v6/index.js"
import type { BigNumberish, AddressLike } from "ethers"
import { type CallParams, addCallParams } from "./utils.js"
export function generateTransferCall(
token: AddressLike,
target: AddressLike,
amount: BigNumberish,
callParams?: CallParams
) {
const executorInterface = SwapExecutor__factory.createInterface()
const calldata = executorInterface.encodeFunctionData('safeTransfer', [
token,
target,
amount
])
return addCallParams(calldata, callParams)
}
export function generateApproveCall(
token: AddressLike,
target: AddressLike,
amount: BigNumberish,
callParams?: CallParams
) {
const executorInterface = SwapExecutor__factory.createInterface()
const calldata = executorInterface.encodeFunctionData('safeApprove', [
token,
target,
amount
])
return addCallParams(calldata, callParams)
}

View File

@@ -0,0 +1,21 @@
import { SwapExecutor__factory } from "@orionprotocol/contracts/lib/ethers-v6/index.js"
import type { BigNumberish, AddressLike } from "ethers"
import { type CallParams, addCallParams } from "./utils.js"
export function generateFeePaymentCall(
matcher: AddressLike,
token: AddressLike,
amount: BigNumberish,
callParams?: CallParams
) {
const executorInterface = SwapExecutor__factory.createInterface()
const calldata = executorInterface.encodeFunctionData('payFeeToMatcher', [
matcher,
token,
amount
])
return addCallParams(calldata, callParams)
}

View File

@@ -0,0 +1,54 @@
import { SwapExecutor__factory } from "@orionprotocol/contracts/lib/ethers-v6/index.js"
import { SafeArray } from "../../../utils/safeGetters.js"
import { type BytesLike, type BigNumberish, concat, ethers, toBeHex } from "ethers"
import { addCallParams } from "./utils.js"
import type { SingleSwap } from "../../../types.js"
export async function generateUni2Calls(
path: SafeArray<SingleSwap>,
recipient: string
) {
const executorInterface = SwapExecutor__factory.createInterface()
const calls: BytesLike[] = []
if (path.length > 1) {
for (let i = 0; i < path.length - 1; ++i) {
const currentSwap = path.get(i)
const nextSwap = path.get(i + 1)
const call = await generateUni2Call(
currentSwap.pool,
currentSwap.assetIn,
currentSwap.assetOut,
nextSwap.pool
)
calls.push(call)
}
}
const lastSwap = path.last();
const calldata = executorInterface.encodeFunctionData('swapUniV2', [
lastSwap.pool,
lastSwap.assetIn,
lastSwap.assetOut,
ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [concat(['0x03', recipient])]),
])
calls.push(addCallParams(calldata))
return calls
}
export function generateUni2Call(
pool: string,
assetIn: string,
assetOut: string,
recipient: string,
fee: BigNumberish = 3,
) {
const executorInterface = SwapExecutor__factory.createInterface()
const calldata = executorInterface.encodeFunctionData('swapUniV2', [
pool,
assetIn,
assetOut,
ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [concat([toBeHex(fee), recipient])]),
])
return addCallParams(calldata)
}

View File

@@ -0,0 +1,92 @@
import { SwapExecutor__factory, UniswapV3Pool__factory } from "@orionprotocol/contracts/lib/ethers-v6/index.js"
import { type BigNumberish , ethers, JsonRpcProvider } from "ethers"
import { SafeArray } from "../../../utils/safeGetters.js"
import { addCallParams } from "./utils.js"
import type { SingleSwap } from "../../../types.js"
export async function generateUni3Call(
swap: SingleSwap,
amount: BigNumberish | undefined,
recipient: string,
provider: JsonRpcProvider
) {
if (typeof amount === 'undefined') amount = 0
const encodedPool = await encodePoolV3(swap.pool, swap.assetIn, swap.assetOut, provider)
const executorInterface = SwapExecutor__factory.createInterface()
let calldata = executorInterface.encodeFunctionData('uniswapV3SingleSwapTo', [encodedPool, recipient, amount])
return addCallParams(calldata)
}
export async function generateOrion3Call(
swap: SingleSwap,
amount: BigNumberish | undefined,
recipient: string,
provider: JsonRpcProvider
) {
if (amount === undefined) amount = 0
const encodedPool = await encodePoolV3(swap.pool, swap.assetIn, swap.assetOut, provider)
const executorInterface = SwapExecutor__factory.createInterface()
const calldata = executorInterface.encodeFunctionData('orionV3SingleSwapTo', [encodedPool, recipient, amount])
return addCallParams(calldata)
}
export async function generateUni3Calls(
path: SafeArray<SingleSwap>,
amount: BigNumberish,
recipient: string,
provider: JsonRpcProvider
) {
const encodedPools: BigNumberish[] = []
for (const swap of path) {
const encodedPool = await encodePoolV3(swap.pool, swap.assetIn, swap.assetOut, provider)
encodedPools.push(encodedPool)
}
const executorInterface = SwapExecutor__factory.createInterface()
let calldata = executorInterface.encodeFunctionData('uniswapV3SwapTo', [encodedPools, recipient, amount])
calldata = addCallParams(calldata)
return [calldata]
}
export async function generateOrion3Calls(
path: SafeArray<SingleSwap>,
amount: BigNumberish,
recipient: string,
provider: JsonRpcProvider
) {
const encodedPools: BigNumberish[] = []
for (const swap of path) {
const encodedPool = await encodePoolV3(swap.pool, swap.assetIn, swap.assetOut, provider)
encodedPools.push(encodedPool)
}
const executorInterface = SwapExecutor__factory.createInterface()
let calldata = executorInterface.encodeFunctionData('orionV3SwapTo', [encodedPools, recipient, amount])
calldata = addCallParams(calldata)
return [calldata]
}
export async function encodePoolV3(
poolAddress: string,
assetInAddress: string,
assetOutAddress: string,
provider: JsonRpcProvider
) {
const pool = UniswapV3Pool__factory.connect(poolAddress, provider)
const token0 = await pool.token0()
const zeroForOne = token0.toLowerCase() === assetInAddress.toLowerCase()
const unwrapWETH = assetOutAddress === ethers.ZeroAddress
let encodedPool = ethers.solidityPacked(['uint256'], [await pool.getAddress()])
encodedPool = ethers.dataSlice(encodedPool, 1)
let firstByte = 0
if (unwrapWETH) firstByte += 32
if (!zeroForOne) firstByte += 128
const encodedFirstByte = ethers.solidityPacked(['uint8'], [firstByte])
encodedPool = ethers.hexlify(ethers.concat([encodedFirstByte, encodedPool]))
return encodedPool
}

View File

@@ -0,0 +1,103 @@
import { ERC20__factory, SwapExecutor__factory } from "@orionprotocol/contracts/lib/ethers-v6/index.js"
import type { AddressLike } from "ethers"
import { type BytesLike, ethers, type BigNumberish } from "ethers"
const EXECUTOR_SWAP_FUNCTION = 'func_70LYiww'
export type CallParams = {
isMandatory?: boolean,
target?: string,
gaslimit?: BigNumberish,
value?: BigNumberish
}
export type PatchParams = {
skipOnZeroAmount?: boolean,
skipCallDataPatching?: boolean,
skipValuePatching?: boolean
}
export function pathCallWithBalance(
calldata: BytesLike,
tokenAddress: AddressLike,
patchParams: PatchParams = { skipCallDataPatching: false, skipValuePatching: true }
) {
const executorInterface = SwapExecutor__factory.createInterface()
const skipMaskAndOffset = createPatchMask(calldata, patchParams)
calldata = executorInterface.encodeFunctionData("patchCallWithTokenBalance", [
calldata,
skipMaskAndOffset,
tokenAddress,
ethers.MaxUint256])
return addCallParams(calldata)
}
export function addCallParams(
calldata: BytesLike,
callParams?: CallParams
) {
let firstByte = 0
if (callParams) {
if (callParams.value !== undefined) {
firstByte += 16 // 00010000
const encodedValue = ethers.solidityPacked(['uint128'], [callParams.value])
calldata = ethers.hexlify(ethers.concat([encodedValue, calldata]))
}
if (callParams.target !== undefined) {
firstByte += 32 // 00100000
const encodedAddress = ethers.solidityPacked(['address'], [callParams.target])
calldata = ethers.hexlify(ethers.concat([encodedAddress, calldata]))
}
if (callParams.gaslimit !== undefined) {
firstByte += 64 // 01000000
const encodedGaslimit = ethers.solidityPacked(['uint32'], [callParams.gaslimit])
calldata = ethers.hexlify(ethers.concat([encodedGaslimit, calldata]))
}
if (callParams.isMandatory !== undefined) firstByte += 128 // 10000000
}
const encodedFirstByte = ethers.solidityPacked(['uint8'], [firstByte])
calldata = ethers.hexlify(ethers.concat([encodedFirstByte, calldata]))
return calldata
}
export function createPatchMask(calldata: BytesLike, patchParams?: PatchParams) {
let firstByte = 0
let mask = ethers.solidityPacked(["uint256"], [(calldata.length - 4) / 2 - 32]) //finding offset of last 32 bytes slot in calldata
mask = ethers.dataSlice(mask, 1)
if (patchParams) {
if (patchParams.skipOnZeroAmount !== undefined && patchParams.skipOnZeroAmount === false) {
firstByte += 32
}
if (patchParams.skipCallDataPatching !== undefined && patchParams.skipCallDataPatching) {
firstByte += 64
}
if (patchParams.skipValuePatching !== undefined && patchParams.skipValuePatching) {
firstByte += 128
}
}
const encodedFirstByte = ethers.solidityPacked(["uint8"], [firstByte])
mask = ethers.hexlify(ethers.concat([encodedFirstByte, mask]))
return mask
}
export function generateCalls(calls: BytesLike[]) {
const executorInterface = SwapExecutor__factory.createInterface()
return '0x' + executorInterface.encodeFunctionData(EXECUTOR_SWAP_FUNCTION, [ethers.ZeroAddress, calls]).slice(74)
}
export async function exchangeToNativeDecimals(token: AddressLike, amount: BigNumberish, provider: ethers.JsonRpcProvider) {
return await toNativeDecimals(token, amount, provider) / (BigInt(10) ** 8n)
}
export async function toNativeDecimals(token: AddressLike, amount: BigNumberish, provider: ethers.JsonRpcProvider) {
token = await token
if (typeof token !== "string") token = await token.getAddress()
let decimals = 18n
if (token !== ethers.ZeroAddress) {
const contract = ERC20__factory.connect(token, provider)
decimals = BigInt(await contract.decimals())
}
return BigInt(amount) * (BigInt(10) ** decimals)
}

View File

@@ -0,0 +1,32 @@
import { SwapExecutor__factory } from "@orionprotocol/contracts/lib/ethers-v6/index.js"
import type { BigNumberish } from "ethers"
import { type CallParams, addCallParams } from "./utils.js"
import type { AddressLike } from "ethers"
export function generateWrapAndTransferCall(
target: AddressLike,
callParams?: CallParams
) {
const executorInterface = SwapExecutor__factory.createInterface()
const calldata = executorInterface.encodeFunctionData('wrapAndTransfer', [
target,
])
return addCallParams(calldata, callParams)
}
export function generateUnwrapAndTransferCall(
target: AddressLike,
amount: BigNumberish,
callParams?: CallParams
) {
const executorInterface = SwapExecutor__factory.createInterface()
const calldata = executorInterface.encodeFunctionData('unwrapAndTransfer', [
target,
amount
])
return addCallParams(calldata, callParams)
}

View File

@@ -1,6 +1,6 @@
import { BigNumber } from 'bignumber.js';
import { ethers } from 'ethers';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v6/index.js';
import getBalances from '../../utils/getBalances.js';
import BalanceGuard from '../../BalanceGuard.js';
import type Unit from '../index.js';
@@ -51,7 +51,7 @@ export default async function deposit({
const balances = await getBalances(
{
[asset]: assetAddress,
[nativeCryptocurrency]: ethers.constants.AddressZero,
[nativeCryptocurrency]: ethers.ZeroAddress,
},
aggregator,
walletAddress,
@@ -63,7 +63,7 @@ export default async function deposit({
balances,
{
name: nativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
provider,
signer,
@@ -80,34 +80,34 @@ export default async function deposit({
sources: ['wallet'],
});
let unsignedTx: ethers.PopulatedTransaction;
let unsignedTx: ethers.TransactionLike;
if (asset === nativeCryptocurrency) {
unsignedTx = await exchangeContract.populateTransaction.deposit();
unsignedTx = await exchangeContract.deposit.populateTransaction();
unsignedTx.value = normalizeNumber(amount, NATIVE_CURRENCY_PRECISION, BigNumber.ROUND_CEIL);
unsignedTx.gasLimit = ethers.BigNumber.from(DEPOSIT_ETH_GAS_LIMIT);
unsignedTx.gasLimit = BigInt(DEPOSIT_ETH_GAS_LIMIT);
} else {
unsignedTx = await exchangeContract.populateTransaction.depositAsset(
unsignedTx = await exchangeContract.depositAsset.populateTransaction(
assetAddress,
normalizeNumber(amount, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_CEIL),
);
unsignedTx.gasLimit = ethers.BigNumber.from(DEPOSIT_ERC20_GAS_LIMIT);
unsignedTx.gasLimit = BigInt(DEPOSIT_ERC20_GAS_LIMIT);
}
const transactionCost = ethers.BigNumber.from(unsignedTx.gasLimit).mul(gasPriceWei);
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
const transactionCost = BigInt(unsignedTx.gasLimit) * BigInt(gasPriceWei);
const denormalizedTransactionCost = denormalizeNumber(transactionCost, BigInt(NATIVE_CURRENCY_PRECISION));
balanceGuard.registerRequirement({
reason: 'Network fee',
asset: {
name: nativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
amount: denormalizedTransactionCost.toString(),
sources: ['wallet'],
});
unsignedTx.chainId = parseInt(chainId, 10);
unsignedTx.gasPrice = ethers.BigNumber.from(gasPriceWei);
unsignedTx.gasPrice = BigInt(gasPriceWei);
unsignedTx.from = walletAddress;
await balanceGuard.check(true);
@@ -117,10 +117,10 @@ export default async function deposit({
const signedTx = await signer.signTransaction(unsignedTx);
try {
const txResponse = await provider.sendTransaction(signedTx);
const txResponse = await provider.broadcastTransaction(signedTx);
console.log(`Deposit tx sent: ${txResponse.hash}. Waiting for confirmation...`);
const txReceipt = await txResponse.wait();
if (txReceipt.status !== undefined) {
if (txReceipt?.status !== undefined) {
console.log('Deposit tx confirmed');
} else {
console.log('Deposit tx failed');

View File

@@ -1,315 +1,412 @@
import type { ExchangeWithGenericSwap } from '@orionprotocol/contracts/lib/ethers-v5/Exchange.js';
import { UniswapV3Pool__factory, ERC20__factory, SwapExecutor__factory, CurveRegistry__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { BigNumber, ethers, type BigNumberish } from 'ethers';
import { concat, defaultAbiCoder, type BytesLike } from 'ethers/lib/utils.js';
import { safeGet, SafeArray } from '../../utils/safeGetters.js';
import type Unit from '../index.js';
import { simpleFetch } from 'simple-typed-fetch';
import type { PromiseOrValue } from '@orionprotocol/contracts/lib/ethers-v5/common.js';
import type { LibValidator } from "@orionprotocol/contracts/lib/ethers-v6/Exchange.js";
import { ethers, ZeroAddress } from "ethers";
import type { AddressLike, JsonRpcProvider, BigNumberish, BytesLike } from "ethers";
import cloneDeep from "lodash.clonedeep";
import { safeGet, SafeArray } from "../../utils/safeGetters.js";
import { simpleFetch } from "simple-typed-fetch";
import type Unit from "../index.js";
import { generateUni2Calls, generateUni2Call } from "./callGenerators/uniswapV2.js";
import {
generateUni3Calls,
generateOrion3Calls,
generateUni3Call,
generateOrion3Call,
} from "./callGenerators/uniswapV3.js";
import { exchangeToNativeDecimals, generateCalls, pathCallWithBalance } from "./callGenerators/utils.js";
import { generateTransferCall } from "./callGenerators/erc20.js";
import { generateCurveStableSwapCall } from "./callGenerators/curve.js";
import type { SingleSwap } from "../../types.js";
import { addressLikeToString } from "../../utils/addressLikeToString.js";
import { generateUnwrapAndTransferCall, generateWrapAndTransferCall } from "./callGenerators/weth.js";
import { getExchangeAllowance, getTotalBalance } from "../../utils/getBalance.js";
import { generateFeePaymentCall } from "./callGenerators/feePayment.js";
const EXECUTOR_SWAP_FUNCTION = "func_70LYiww"
export type Factory = "UniswapV2" | "UniswapV3" | "Curve" | "OrionV2" | "OrionV3";
export type Factory = "UniswapV2" | "UniswapV3" | "Curve" | "OrionV2" | "OrionV3"
const exchangeToType: Partial<Record<string, Factory>> = {
'SPOOKYSWAP': 'UniswapV2',
'PANCAKESWAP': 'UniswapV2',
'UNISWAP': 'UniswapV2',
'QUICKSWAP': 'UniswapV2',
'ORION_POOL': 'UniswapV2',
'CHERRYSWAP': 'UniswapV2',
'OKXSWAP': 'UniswapV2',
'INTERNAL_POOL_V2': 'UniswapV2',
'INTERNAL_POOL_V3': "OrionV3",
'INTERNAL_POOL_V3_0_01': "OrionV3",
'INTERNAL_POOL_V3_0_05': "OrionV3",
'INTERNAL_POOL_V3_0_3': "OrionV3",
'INTERNAL_POOL_V3_1_0': "OrionV3",
'CURVE': "Curve",
'CURVE_FACTORY': "Curve",
type BaseGenerateSwapCalldataParams = {
amount: BigNumberish;
minReturnAmount: BigNumberish;
initiatorAddress: string;
receiverAddress: string;
path: ArrayLike<SingleSwap>;
matcher?: AddressLike,
feeToken?: AddressLike,
fee?: BigNumberish;
}
export type SwapInfo = {
pool: string,
assetIn: string,
assetOut: string,
factory: string
}
export type GenerateSwapCalldataWithUnitParams = BaseGenerateSwapCalldataParams & {
unit: Unit;
};
export type CallParams = {
isMandatory?: boolean,
target?: string,
gaslimit?: BigNumber,
value?: BigNumber
}
export type GenerateSwapCalldataParams = BaseGenerateSwapCalldataParams & {
exchangeContractAddress: AddressLike;
wethAddress: AddressLike;
curveRegistryAddress: AddressLike;
swapExecutorContractAddress: AddressLike;
provider: JsonRpcProvider;
};
export type GenerateSwapCalldataParams = {
amount: BigNumberish,
minReturnAmount: BigNumberish,
receiverAddress: string,
path: ArrayLike<SwapInfo>,
unit: Unit
}
export default async function generateSwapCalldata({
export async function generateSwapCalldataWithUnit({
amount,
minReturnAmount,
initiatorAddress,
receiverAddress,
path: path_,
unit
}: GenerateSwapCalldataParams
): Promise<{ calldata: string, swapDescription: ExchangeWithGenericSwap.SwapDescriptionStruct }> {
if (path_ == undefined || path_.length == 0) {
throw new Error(`Empty path`);
}
const wethAddress = safeGet(unit.contracts, "WETH")
const curveRegistryAddress = safeGet(unit.contracts, "curveRegistry")
const { assetToAddress, swapExecutorContractAddress, exchangeContractAddress } = await simpleFetch(unit.blockchainService.getInfo)();
let path = SafeArray.from(path_).map((swapInfo) => {
swapInfo.assetIn = safeGet(assetToAddress, swapInfo.assetIn);
swapInfo.assetOut = safeGet(assetToAddress, swapInfo.assetOut);
return swapInfo;
})
const factory = path.first().factory
if (!path.every(swapInfo => swapInfo.factory === factory)) {
throw new Error(`Supporting only swaps with single factory`);
}
const swapDescription: ExchangeWithGenericSwap.SwapDescriptionStruct = {
srcToken: path.first().assetIn,
dstToken: path.last().assetOut,
srcReceiver: swapExecutorContractAddress ?? '',
dstReceiver: receiverAddress,
amount: amount,
minReturnAmount: minReturnAmount,
flags: 0
path: arrayLikePath,
matcher = ZeroAddress,
feeToken = ZeroAddress,
fee = 0,
unit,
}: GenerateSwapCalldataWithUnitParams): Promise<{
calldata: string;
swapDescription: LibValidator.SwapDescriptionStruct;
value: bigint;
}> {
if (arrayLikePath == undefined || arrayLikePath.length == 0) {
throw new Error("Empty path");
}
const wethAddress = safeGet(unit.contracts, "WETH");
const curveRegistryAddress = safeGet(unit.contracts, "curveRegistry");
const { assetToAddress, swapExecutorContractAddress, exchangeContractAddress } = await simpleFetch(
unit.blockchainService.getInfo
)();
const exchangeToNativeDecimals = async (token: PromiseOrValue<string>) => {
token = await token
let decimals = 18
if (token !== ethers.constants.AddressZero) {
const contract = ERC20__factory.connect(token, unit.provider)
decimals = await contract.decimals()
}
return BigNumber.from(amount).mul(BigNumber.from(10).pow(decimals)).div(BigNumber.from(10).pow(8))
}
const amountNativeDecimals = await exchangeToNativeDecimals(swapDescription.srcToken);
const arrayLikePathCopy = cloneDeep(arrayLikePath);
let path = SafeArray.from(arrayLikePathCopy);
path = SafeArray.from(path_).map((swapInfo) => {
if (swapInfo.assetIn == ethers.constants.AddressZero) swapInfo.assetIn = wethAddress
if (swapInfo.assetOut == ethers.constants.AddressZero) swapInfo.assetOut = wethAddress
path = SafeArray.from(arrayLikePathCopy).map((swapInfo) => {
swapInfo.assetIn = assetToAddress[swapInfo.assetIn] ?? swapInfo.assetIn
swapInfo.assetOut = assetToAddress[swapInfo.assetOut] ?? swapInfo.assetOut
swapInfo.assetIn = swapInfo.assetIn.toLowerCase()
swapInfo.assetOut = swapInfo.assetOut.toLowerCase()
return swapInfo;
});
let calldata: string
switch (exchangeToType[factory]) {
return await generateSwapCalldata({
amount,
minReturnAmount,
receiverAddress,
initiatorAddress,
path,
matcher,
feeToken,
fee,
exchangeContractAddress,
wethAddress,
curveRegistryAddress,
swapExecutorContractAddress,
provider: unit.provider,
});
}
export async function generateSwapCalldata({
amount,
minReturnAmount,
initiatorAddress,
receiverAddress,
path: arrayLikePath,
matcher: matcherAddressLike = ZeroAddress,
feeToken: feeTokenAddressLike = ZeroAddress,
fee = 0,
exchangeContractAddress,
wethAddress: wethAddressLike,
curveRegistryAddress: curveRegistryAddressLike,
swapExecutorContractAddress: swapExecutorContractAddressLike,
provider,
}: GenerateSwapCalldataParams): Promise<{
calldata: string;
swapDescription: LibValidator.SwapDescriptionStruct;
value: bigint;
}> {
const wethAddress = await addressLikeToString(wethAddressLike);
const curveRegistryAddress = await addressLikeToString(curveRegistryAddressLike);
const swapExecutorContractAddress = await addressLikeToString(swapExecutorContractAddressLike);
const feeToken = await addressLikeToString(feeTokenAddressLike);
const matcher = await addressLikeToString(matcherAddressLike);
let path = SafeArray.from(arrayLikePath).map((swapInfo) => {
swapInfo.assetIn = swapInfo.assetIn.toLowerCase()
swapInfo.assetOut = swapInfo.assetOut.toLowerCase()
return swapInfo;
});
const { assetIn: srcToken } = path.first();
const { assetOut: dstToken } = path.last();
let swapDescription: LibValidator.SwapDescriptionStruct = {
srcToken,
dstToken,
srcReceiver: swapExecutorContractAddress,
dstReceiver: receiverAddress,
amount,
minReturnAmount,
flags: 0,
};
const amountNativeDecimals = await exchangeToNativeDecimals(srcToken, amount, provider);
const feeNativeDecimals = await exchangeToNativeDecimals(feeToken, fee, provider)
path = SafeArray.from(arrayLikePath).map((singleSwap) => {
if (singleSwap.assetIn == ethers.ZeroAddress) singleSwap.assetIn = wethAddress;
if (singleSwap.assetOut == ethers.ZeroAddress) singleSwap.assetOut = wethAddress;
return singleSwap;
});
let calls: BytesLike[];
({ swapDescription, calls } = await processSwaps(
swapDescription,
path,
amountNativeDecimals,
matcher,
feeToken,
feeNativeDecimals,
wethAddress,
swapExecutorContractAddress,
curveRegistryAddress,
provider
));
const calldata = generateCalls(calls);
const { useExchangeBalance, additionalTransferAmount } = await shouldUseExchangeBalance(
srcToken,
initiatorAddress,
exchangeContractAddress,
amountNativeDecimals,
provider
);
if (useExchangeBalance) {
swapDescription.flags = 1n << 255n;
}
const value = srcToken == ZeroAddress ? additionalTransferAmount : 0n;
return { swapDescription, calldata, value };
}
async function processSwaps(
swapDescription: LibValidator.SwapDescriptionStruct,
path: SafeArray<SingleSwap>,
amount: BigNumberish,
matcher: string,
feeToken: string,
fee: BigNumberish,
wethAddress: string,
swapExecutorContractAddress: string,
curveRegistryAddress: string,
provider: JsonRpcProvider
) {
const { factory: firstSwapFactory } = path.first();
const isSingleFactorySwap = path.every((singleSwap) => singleSwap.factory === firstSwapFactory);
let calls: BytesLike[];
if (isSingleFactorySwap) {
({ swapDescription, calls } = await processSingleFactorySwaps(
firstSwapFactory,
swapDescription,
path,
amount,
swapExecutorContractAddress,
curveRegistryAddress,
provider
));
} else {
({ swapDescription, calls } = await processMultiFactorySwaps(
swapDescription,
path,
amount,
swapExecutorContractAddress,
curveRegistryAddress,
provider
));
}
({swapDescription, calls} = await payFeeToMatcher(matcher, feeToken, fee, calls, swapDescription));
({ swapDescription, calls } = wrapOrUnwrapIfNeeded(
amount,
swapDescription,
calls,
swapExecutorContractAddress,
wethAddress
));
return { swapDescription, calls };
}
async function processSingleFactorySwaps(
factory: Factory,
swapDescription: LibValidator.SwapDescriptionStruct,
path: SafeArray<SingleSwap>,
amount: BigNumberish,
swapExecutorContractAddress: string,
curveRegistryAddress: string,
provider: JsonRpcProvider
) {
let calls: BytesLike[] = [];
switch (factory) {
case "OrionV2": {
swapDescription.srcReceiver = path.first().pool
calldata = await generateUni2Calls(exchangeContractAddress, path);
swapDescription.srcReceiver = path.first().pool;
calls = await generateUni2Calls(path, swapExecutorContractAddress);
break;
}
case "UniswapV2": {
swapDescription.srcReceiver = path.first().pool
calldata = await generateUni2Calls(exchangeContractAddress, path);
swapDescription.srcReceiver = path.first().pool;
calls = await generateUni2Calls(path, swapExecutorContractAddress);
break;
}
case "UniswapV3": {
calldata = await generateUni3Calls(amountNativeDecimals, exchangeContractAddress, path, unit.provider)
calls = await generateUni3Calls(path, amount, swapExecutorContractAddress, provider);
break;
}
case "OrionV3": {
calldata = await generateOrion3Calls(amountNativeDecimals, exchangeContractAddress, path, unit.provider)
calls = await generateOrion3Calls(path, amount, swapExecutorContractAddress, provider);
break;
}
case "Curve": {
calldata = await generateCurveStableSwapCalls(
amountNativeDecimals,
exchangeContractAddress,
swapExecutorContractAddress ?? '',
path,
unit.provider,
if (path.length > 1) {
throw new Error("Supporting only single stable swap on curve");
}
calls = await generateCurveStableSwapCall(
amount,
swapExecutorContractAddress,
path.first(),
provider,
swapExecutorContractAddress,
curveRegistryAddress
);
break;
}
default: {
throw new Error(`Factory ${factory} is not supported`)
throw new Error(`Factory ${factory} is not supported`);
}
}
return { swapDescription, calldata }
return { swapDescription, calls };
}
export async function generateUni2Calls(
exchangeAddress: string,
path: SafeArray<SwapInfo>
) {
const executorInterface = SwapExecutor__factory.createInterface()
const calls: BytesLike[] = []
if (path.length > 1) {
for (let i = 0; i < path.length - 1; ++i) {
const currentSwap = path.get(i)
const nextSwap = path.get(i + 1)
const calldata = executorInterface.encodeFunctionData("swapUniV2", [
currentSwap.pool,
currentSwap.assetIn,
currentSwap.assetOut,
defaultAbiCoder.encode(["uint256"], [concat(["0x03", nextSwap.pool])]),
]
)
calls.push(addCallParams(calldata))
}
}
const lastSwap = path.last();
const calldata = executorInterface.encodeFunctionData("swapUniV2", [
lastSwap.pool,
lastSwap.assetIn,
lastSwap.assetOut,
defaultAbiCoder.encode(["uint256"], [concat(["0x03", exchangeAddress])]),
])
calls.push(addCallParams(calldata))
return generateCalls(calls)
}
async function generateUni3Calls(
async function processMultiFactorySwaps(
swapDescription: LibValidator.SwapDescriptionStruct,
path: SafeArray<SingleSwap>,
amount: BigNumberish,
exchangeContractAddress: string,
path: SafeArray<SwapInfo>,
provider: ethers.providers.JsonRpcProvider
swapExecutorContractAddress: string,
curveRegistryAddress: string,
provider: JsonRpcProvider
) {
const encodedPools: BytesLike[] = []
let calls: BytesLike[] = [];
for (const swap of path) {
const pool = UniswapV3Pool__factory.connect(swap.pool, provider)
const token0 = await pool.token0()
const zeroForOne = token0.toLowerCase() === swap.assetIn.toLowerCase()
const unwrapWETH = swap.assetOut === ethers.constants.AddressZero
let encodedPool = ethers.utils.solidityPack(["uint256"], [pool.address])
encodedPool = ethers.utils.hexDataSlice(encodedPool, 1)
let firstByte = 0
if (unwrapWETH) firstByte += 32
if (!zeroForOne) firstByte += 128
const encodedFirstByte = ethers.utils.solidityPack(["uint8"], [firstByte])
encodedPool = ethers.utils.hexlify(ethers.utils.concat([encodedFirstByte, encodedPool]))
encodedPools.push(encodedPool)
switch (swap.factory) {
case "OrionV2": {
let transferCall = generateTransferCall(swap.assetIn, swap.pool, 0);
transferCall = pathCallWithBalance(transferCall, swap.assetIn);
const uni2Call = generateUni2Call(swap.pool, swap.assetIn, swap.assetOut, swapExecutorContractAddress);
calls.push(transferCall, uni2Call);
break;
}
case "UniswapV2": {
let transferCall = generateTransferCall(swap.assetIn, swap.pool, 0);
transferCall = pathCallWithBalance(transferCall, swap.assetIn);
const uni2Call = generateUni2Call(swap.pool, swap.assetIn, swap.assetOut, swapExecutorContractAddress);
calls.push(transferCall, uni2Call);
break;
}
case "UniswapV3": {
let uni3Call = await generateUni3Call(swap, 0, swapExecutorContractAddress, provider);
uni3Call = pathCallWithBalance(uni3Call, swap.assetIn);
calls.push(uni3Call);
break;
}
case "OrionV3": {
let orion3Call = await generateOrion3Call(swap, 0, swapExecutorContractAddress, provider);
orion3Call = pathCallWithBalance(orion3Call, swap.assetIn);
calls.push(orion3Call);
break;
}
case "Curve": {
let curveCalls = await generateCurveStableSwapCall(
amount,
swapExecutorContractAddress,
swap,
provider,
swapExecutorContractAddress,
curveRegistryAddress,
true
);
calls.push(...curveCalls);
break;
}
default: {
throw new Error(`Factory ${swap.factory} is not supported`);
}
}
}
const executorInterface = SwapExecutor__factory.createInterface()
let calldata = executorInterface.encodeFunctionData("uniswapV3SwapTo", [encodedPools, exchangeContractAddress, amount])
calldata = addCallParams(calldata)
return generateCalls([calldata])
return { swapDescription, calls };
}
async function generateOrion3Calls(
async function payFeeToMatcher(
matcher: string,
feeToken: string,
feeAmount: BigNumberish,
calls: BytesLike[],
swapDescription: LibValidator.SwapDescriptionStruct,
) {
if (BigInt(feeAmount) !== 0n && feeToken === swapDescription.dstToken) {
const feePaymentCall = generateFeePaymentCall(matcher, feeToken, feeAmount)
calls.push(feePaymentCall)
}
return {swapDescription, calls}
}
function wrapOrUnwrapIfNeeded(
amount: BigNumberish,
exchangeContractAddress: string,
path: SafeArray<SwapInfo>,
provider: ethers.providers.JsonRpcProvider
swapDescription: LibValidator.SwapDescriptionStruct,
calls: BytesLike[],
swapExecutorContractAddress: string,
wethAddress: string
) {
const encodedPools: BytesLike[] = []
for (const swap of path) {
const pool = UniswapV3Pool__factory.connect(swap.pool, provider)
const token0 = await pool.token0()
const zeroForOne = token0.toLowerCase() === swap.assetIn.toLowerCase()
const unwrapWETH = swap.assetOut === ethers.constants.AddressZero
let encodedPool = ethers.utils.solidityPack(["uint256"], [pool.address])
encodedPool = ethers.utils.hexDataSlice(encodedPool, 1)
let firstByte = 0
if (unwrapWETH) firstByte += 32
if (!zeroForOne) firstByte += 128
const encodedFirstByte = ethers.utils.solidityPack(["uint8"], [firstByte])
encodedPool = ethers.utils.hexlify(ethers.utils.concat([encodedFirstByte, encodedPool]))
encodedPools.push(encodedPool)
const {dstReceiver, srcReceiver, srcToken, dstToken} = swapDescription;
if (srcToken === ZeroAddress) {
const wrapCall = generateWrapAndTransferCall(srcReceiver, { value: amount });
swapDescription.srcReceiver = swapExecutorContractAddress;
calls = ([wrapCall] as BytesLike[]).concat(calls);
}
const executorInterface = SwapExecutor__factory.createInterface()
let calldata = executorInterface.encodeFunctionData("orionV3SwapTo", [encodedPools, exchangeContractAddress, amount])
calldata = addCallParams(calldata)
return generateCalls([calldata])
if (dstToken === ZeroAddress) {
let unwrapCall = generateUnwrapAndTransferCall(dstReceiver, 0);
unwrapCall = pathCallWithBalance(unwrapCall, wethAddress);
calls.push(unwrapCall);
} else {
let transferCall = generateTransferCall(dstToken, dstReceiver, 0);
transferCall = pathCallWithBalance(transferCall, dstToken);
calls.push(transferCall);
}
return { swapDescription, calls };
}
async function generateCurveStableSwapCalls(
amount: BigNumberish,
exchangeContractAddress: string,
executorAddress: string,
path: SafeArray<SwapInfo>,
provider: ethers.providers.JsonRpcProvider,
curveRegistry: string
async function shouldUseExchangeBalance(
srcToken: AddressLike,
initiatorAddress: AddressLike,
exchangeContractAddress: AddressLike,
amount: bigint,
provider: JsonRpcProvider
) {
if (path.length > 1) {
throw new Error("Supporting only single stable swap on curve")
const { walletBalance, exchangeBalance } = await getTotalBalance(
srcToken,
initiatorAddress,
exchangeContractAddress,
provider
);
const exchangeAllowance = await getExchangeAllowance(srcToken, initiatorAddress, exchangeContractAddress, provider);
if (walletBalance + exchangeBalance < amount) {
throw new Error(
`Not enough balance to make swap, totalBalance - ${walletBalance + exchangeBalance} swapAmount - ${amount}`
);
}
const executorInterface = SwapExecutor__factory.createInterface()
const registry = CurveRegistry__factory.connect(curveRegistry, provider)
let useExchangeBalance = true;
let additionalTransferAmount = 0n;
const swap = path.first()
const firstToken = ERC20__factory.connect(swap.assetIn, provider)
const { pool, assetIn, assetOut } = swap
const [i, j,] = await registry.get_coin_indices(pool, assetIn, assetOut)
const executorAllowance = await firstToken.allowance(executorAddress, swap.pool)
const calls: BytesLike[] = []
if (executorAllowance.lt(amount)) {
const calldata = addCallParams(
executorInterface.encodeFunctionData("safeApprove", [
swap.assetIn,
swap.pool,
ethers.constants.MaxUint256
])
)
calls.push(calldata)
if (exchangeBalance == 0n) {
useExchangeBalance = false;
additionalTransferAmount = amount;
} else {
additionalTransferAmount = exchangeBalance >= amount ? 0n : amount - exchangeBalance;
if (srcToken !== ZeroAddress && additionalTransferAmount > exchangeAllowance) {
throw new Error(
`Not enough allowance to make swap, allowance - ${exchangeAllowance} needed allowance - ${additionalTransferAmount}`
);
}
}
let calldata = executorInterface.encodeFunctionData("curveSwapStableAmountIn", [
pool,
assetOut,
i,
j,
amount,
0,
exchangeContractAddress])
calldata = addCallParams(calldata)
calls.push(calldata)
return generateCalls(calls)
}
// Adds additional byte to single swap with settings
function addCallParams(
calldata: BytesLike,
callParams?: CallParams
) {
let firstByte = 0
if (callParams) {
if (callParams.value !== undefined) {
firstByte += 16 // 00010000
const encodedValue = ethers.utils.solidityPack(["uint128"], [callParams.value])
calldata = ethers.utils.hexlify(ethers.utils.concat([encodedValue, calldata]))
}
if (callParams.target !== undefined) {
firstByte += 32 // 00100000
const encodedAddress = ethers.utils.solidityPack(["address"], [callParams.target])
calldata = ethers.utils.hexlify(ethers.utils.concat([encodedAddress, calldata]))
}
if (callParams.gaslimit !== undefined) {
firstByte += 64 // 01000000
const encodedGaslimit = ethers.utils.solidityPack(["uint32"], [callParams.gaslimit])
calldata = ethers.utils.hexlify(ethers.utils.concat([encodedGaslimit, calldata]))
}
if (callParams.isMandatory !== undefined) firstByte += 128 // 10000000
}
const encodedFirstByte = ethers.utils.solidityPack(["uint8"], [firstByte])
calldata = ethers.utils.hexlify(ethers.utils.concat([encodedFirstByte, calldata]))
return calldata
}
async function generateCalls(calls: BytesLike[]) {
const executorInterface = SwapExecutor__factory.createInterface()
return "0x" + executorInterface.encodeFunctionData(EXECUTOR_SWAP_FUNCTION, [ethers.constants.AddressZero, calls]).slice(74)
return { useExchangeBalance, additionalTransferAmount };
}

View File

@@ -51,7 +51,7 @@ export default async function getSwapInfo({
const allPrices = await simpleFetch(blockchainService.getPricesWithQuoteAsset)();
const gasPriceWei = await simpleFetch(blockchainService.getGasPriceWei)();
const gasPriceGwei = ethers.utils.formatUnits(gasPriceWei, 'gwei').toString();
const gasPriceGwei = ethers.formatUnits(gasPriceWei, 'gwei').toString();
const assetInAddress = assetToAddress[assetIn];
if (assetInAddress === undefined) throw new Error(`Asset '${assetIn}' not found`);
@@ -101,15 +101,15 @@ export default async function getSwapInfo({
}
if (route === 'pool') {
const transactionCost = ethers.BigNumber.from(SWAP_THROUGH_ORION_POOL_GAS_LIMIT).mul(gasPriceWei);
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
const transactionCost = BigInt(SWAP_THROUGH_ORION_POOL_GAS_LIMIT) * BigInt(gasPriceWei);
const denormalizedTransactionCost = denormalizeNumber(transactionCost, BigInt(NATIVE_CURRENCY_PRECISION));
return {
route,
swapInfo,
fee: {
assetName: nativeCryptocurrencyName,
assetAddress: ethers.constants.AddressZero,
assetAddress: ethers.ZeroAddress,
networkFeeInFeeAsset: denormalizedTransactionCost.toString(),
protocolFeeInFeeAsset: undefined,
},
@@ -134,7 +134,7 @@ export default async function getSwapInfo({
gasPriceGwei,
feePercent,
baseAssetAddress,
ethers.constants.AddressZero,
ethers.ZeroAddress,
feeAssetAddress,
allPrices.prices
);

View File

@@ -1,17 +1,19 @@
import type Unit from '../index.js';
import deposit, { type DepositParams } from './deposit.js';
import getSwapInfo, { type GetSwapInfoParams } from './getSwapInfo.js';
import generateSwapCalldata, { type GenerateSwapCalldataParams } from './generateSwapCalldata.js';
import swapLimit, {type SwapLimitParams} from './swapLimit.js';
import swapMarket, { type SwapMarketParams } from './swapMarket.js';
import { generateSwapCalldataWithUnit, type GenerateSwapCalldataWithUnitParams } from './generateSwapCalldata.js';
import withdraw, { type WithdrawParams } from './withdraw.js';
import type { SwapLimitParams } from './swapLimit.js';
import swapLimit from './swapLimit.js';
import swapMarket from './swapMarket.js';
import type { SwapMarketParams } from './swapMarket.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'>
type PureGenerateSwapCalldataParams = Omit<GenerateSwapCalldataParams, 'unit'>
type PureGenerateSwapCalldataParams = Omit<GenerateSwapCalldataWithUnitParams, 'unit'>
type PureSwapLimitParams = Omit<SwapLimitParams, 'unit'>
type PureSwapMarketParams = Omit<SwapMarketParams, 'unit'>
export default class Exchange {
private readonly unit: Unit;
@@ -20,20 +22,6 @@ export default class Exchange {
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,
@@ -57,9 +45,23 @@ export default class Exchange {
}
public generateSwapCalldata(params: PureGenerateSwapCalldataParams) {
return generateSwapCalldata({
return generateSwapCalldataWithUnit({
...params,
unit: this.unit
})
}
public swapLimit(params: PureSwapLimitParams) {
return swapLimit({
...params,
unit: this.unit,
});
}
public swapMarket(params: PureSwapMarketParams) {
return swapMarket({
...params,
unit: this.unit,
});
}
}

View File

@@ -1,6 +1,6 @@
import { BigNumber } from 'bignumber.js';
import { ethers } from 'ethers';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v6/index.js';
import getBalances from '../../utils/getBalances.js';
import BalanceGuard from '../../BalanceGuard.js';
import getAvailableSources from '../../utils/getAvailableFundsSources.js';
@@ -12,6 +12,10 @@ 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';
import { generateSwapCalldata } from './generateSwapCalldata.js';
import isValidFactory from '../../utils/isValidFactory.js';
import type { SingleSwap } from '../../types.js';
import { must, safeGet } from '../../utils/safeGetters.js';
export type SwapLimitParams = {
type: 'exactSpend' | 'exactReceive'
@@ -44,11 +48,15 @@ type PoolSwap = {
amountOut: number
through: 'pool'
txHash: string
wait: (confirmations?: number | undefined) => Promise<ethers.providers.TransactionReceipt>
wait: (confirmations?: number | undefined) => Promise<ethers.TransactionReceipt | null>
}
export type Swap = AggregatorOrder | PoolSwap;
const isValidSingleSwap = (singleSwap: Omit<SingleSwap, 'factory'> & { factory: string }): singleSwap is SingleSwap => {
return isValidFactory(singleSwap.factory);
}
export default async function swapLimit({
type,
assetIn,
@@ -85,6 +93,7 @@ export default async function swapLimit({
exchangeContractAddress,
matcherAddress,
assetToAddress,
swapExecutorContractAddress,
} = await simpleFetch(blockchainService.getInfo)();
const nativeCryptocurrency = getNativeCryptocurrencyName(assetToAddress);
@@ -92,10 +101,11 @@ export default async function swapLimit({
const feeAssets = await simpleFetch(blockchainService.getPlatformFees)({ walletAddress, assetIn, assetOut });
const allPrices = await simpleFetch(blockchainService.getPricesWithQuoteAsset)();
const gasPriceWei = await simpleFetch(blockchainService.getGasPriceWei)();
const { factories } = await simpleFetch(blockchainService.getPoolsConfig)();
const { factories, WETHAddress } = await simpleFetch(blockchainService.getPoolsConfig)();
must(WETHAddress, 'WETHAddress is not defined');
const poolExchangesList = factories !== undefined ? Object.keys(factories) : [];
const gasPriceGwei = ethers.utils.formatUnits(gasPriceWei, 'gwei').toString();
const gasPriceGwei = ethers.formatUnits(gasPriceWei, 'gwei').toString();
const assetInAddress = assetToAddress[assetIn];
if (assetInAddress === undefined) throw new Error(`Asset '${assetIn}' not found`);
@@ -108,7 +118,7 @@ export default async function swapLimit({
{
[assetIn]: assetInAddress,
[feeAsset]: feeAssetAddress,
[nativeCryptocurrency]: ethers.constants.AddressZero,
[nativeCryptocurrency]: ethers.ZeroAddress,
},
aggregator,
walletAddress,
@@ -120,7 +130,7 @@ export default async function swapLimit({
balances,
{
name: nativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
provider,
signer,
@@ -138,7 +148,7 @@ export default async function swapLimit({
: undefined,
);
const { exchanges: swapExchanges } = swapInfo;
const { exchanges: swapExchanges, exchangeContractPath } = swapInfo;
const [firstSwapExchange] = swapExchanges;
@@ -236,12 +246,6 @@ export default async function swapLimit({
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)
@@ -271,45 +275,64 @@ export default async function swapLimit({
BigNumber.ROUND_FLOOR,
);
const unsignedSwapThroughOrionPoolTx = await exchangeContract.populateTransaction.swapThroughOrionPool(
amountSpendBlockchainParam,
amountReceiveBlockchainParam,
factoryAddress !== undefined
? [factoryAddress, ...pathAddresses]
: pathAddresses,
type === 'exactSpend',
const { calldata, swapDescription, value } = await generateSwapCalldata({
amount: amountSpendBlockchainParam,
minReturnAmount: amountReceiveBlockchainParam,
path: exchangeContractPath.filter(isValidSingleSwap),
initiatorAddress: walletAddress,
receiverAddress: walletAddress,
provider,
matcher: matcherAddress,
feeToken: feeAssetAddress,
fee: 0,
exchangeContractAddress,
swapExecutorContractAddress,
wethAddress: WETHAddress,
curveRegistryAddress: safeGet(unit.contracts, 'curveRegistry'),
})
const unsignedSwapThroughPoolsTx = await exchangeContract.swap.populateTransaction(
swapExecutorContractAddress,
swapDescription,
new Uint8Array(0),
calldata,
{
value
}
);
unsignedSwapThroughOrionPoolTx.chainId = parseInt(chainId, 10);
unsignedSwapThroughOrionPoolTx.gasPrice = ethers.BigNumber.from(gasPriceWei);
unsignedSwapThroughPoolsTx.chainId = BigInt(parseInt(chainId, 10));
unsignedSwapThroughPoolsTx.gasPrice = BigInt(gasPriceWei);
unsignedSwapThroughOrionPoolTx.from = walletAddress;
unsignedSwapThroughPoolsTx.from = walletAddress;
const amountSpendBN = new BigNumber(amountSpend);
let value = new BigNumber(0);
let txValue = 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);
txValue = amountSpendBN.minus(denormalizedAssetInExchangeBalance);
}
unsignedSwapThroughOrionPoolTx.value = normalizeNumber(
value.dp(INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_CEIL),
unsignedSwapThroughPoolsTx.value = normalizeNumber(
txValue.dp(INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_CEIL),
NATIVE_CURRENCY_PRECISION,
BigNumber.ROUND_CEIL,
);
unsignedSwapThroughOrionPoolTx.gasLimit = ethers.BigNumber.from(SWAP_THROUGH_ORION_POOL_GAS_LIMIT);
unsignedSwapThroughPoolsTx.gasLimit = BigInt(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);
const transactionCost = BigInt(SWAP_THROUGH_ORION_POOL_GAS_LIMIT) * BigInt(gasPriceWei);
const denormalizedTransactionCost = denormalizeNumber(transactionCost, BigInt(NATIVE_CURRENCY_PRECISION));
balanceGuard.registerRequirement({
reason: 'Network fee',
asset: {
name: nativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
amount: denormalizedTransactionCost.toString(),
sources: getAvailableSources('network_fee', ethers.constants.AddressZero, 'pool'),
sources: getAvailableSources('network_fee', ethers.ZeroAddress, 'pool'),
});
// if (value.gt(0)) {
@@ -317,24 +340,24 @@ export default async function swapLimit({
// reason: 'Transaction value (extra amount)',
// asset: {
// name: nativeCryptocurrency,
// address: ethers.constants.AddressZero,
// address: ethers.ZeroAddress,
// },
// amount: value.toString(),
// sources: getAvailableSources('amount', ethers.constants.AddressZero, 'pool'),
// sources: getAvailableSources('amount', ethers.ZeroAddress, 'pool'),
// });
// }
await balanceGuard.check(options?.autoApprove);
const nonce = await provider.getTransactionCount(walletAddress, 'pending');
unsignedSwapThroughOrionPoolTx.nonce = nonce;
unsignedSwapThroughPoolsTx.nonce = nonce;
options?.logger?.('Signing transaction...');
const swapThroughOrionPoolTxResponse = await signer.sendTransaction(unsignedSwapThroughOrionPoolTx);
const swapThroughOrionPoolTxResponse = await signer.sendTransaction(unsignedSwapThroughPoolsTx);
options?.logger?.(`Transaction sent. Tx hash: ${swapThroughOrionPoolTxResponse.hash}`);
return {
amountOut: swapInfo.amountOut,
wait: swapThroughOrionPoolTxResponse.wait,
wait: swapThroughOrionPoolTxResponse.wait.bind(swapThroughOrionPoolTxResponse),
through: 'pool',
txHash: swapThroughOrionPoolTxResponse.hash,
};
@@ -380,7 +403,7 @@ export default async function swapLimit({
gasPriceGwei,
feePercent,
baseAssetAddress,
ethers.constants.AddressZero,
ethers.ZeroAddress,
feeAssetAddress,
allPrices.prices,
);
@@ -426,7 +449,6 @@ export default async function swapLimit({
walletAddress,
matcherAddress,
feeAssetAddress,
false,
signer,
chainId,
);

View File

@@ -1,6 +1,6 @@
import { BigNumber } from 'bignumber.js';
import { ethers } from 'ethers';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v6/index.js';
import getBalances from '../../utils/getBalances.js';
import BalanceGuard from '../../BalanceGuard.js';
import getAvailableSources from '../../utils/getAvailableFundsSources.js';
@@ -12,6 +12,10 @@ 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';
import { generateSwapCalldata } from './generateSwapCalldata.js';
import isValidFactory from '../../utils/isValidFactory.js';
import type { SingleSwap } from '../../types.js';
import { must, safeGet } from '../../utils/safeGetters.js';
export type SwapMarketParams = Omit<SwapLimitParams, 'price'> & {
slippagePercent: BigNumber.Value
@@ -28,11 +32,16 @@ type PoolSwap = {
amountOut: number
through: 'pool'
txHash: string
wait: (confirmations?: number | undefined) => Promise<ethers.providers.TransactionReceipt>
wait: (confirmations?: number | undefined) => Promise<ethers.TransactionReceipt | null>
}
export type Swap = AggregatorOrder | PoolSwap;
const isValidSingleSwap = (singleSwap: Omit<SingleSwap, 'factory'> & { factory: string }): singleSwap is SingleSwap => {
return isValidFactory(singleSwap.factory);
}
export default async function swapMarket({
type,
assetIn,
@@ -45,6 +54,7 @@ export default async function swapMarket({
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');
@@ -70,6 +80,7 @@ export default async function swapMarket({
exchangeContractAddress,
matcherAddress,
assetToAddress,
swapExecutorContractAddress,
} = await simpleFetch(blockchainService.getInfo)();
const nativeCryptocurrency = getNativeCryptocurrencyName(assetToAddress);
@@ -77,10 +88,11 @@ export default async function swapMarket({
const feeAssets = await simpleFetch(blockchainService.getPlatformFees)({ walletAddress, assetIn, assetOut });
const allPrices = await simpleFetch(blockchainService.getPricesWithQuoteAsset)();
const gasPriceWei = await simpleFetch(blockchainService.getGasPriceWei)();
const { factories } = await simpleFetch(blockchainService.getPoolsConfig)();
const { factories, WETHAddress } = await simpleFetch(blockchainService.getPoolsConfig)();
must(WETHAddress, 'WETHAddress is not defined');
const poolExchangesList = factories !== undefined ? Object.keys(factories) : [];
const gasPriceGwei = ethers.utils.formatUnits(gasPriceWei, 'gwei').toString();
const gasPriceGwei = ethers.formatUnits(gasPriceWei, 'gwei').toString();
const assetInAddress = assetToAddress[assetIn];
if (assetInAddress === undefined) throw new Error(`Asset '${assetIn}' not found`);
@@ -93,7 +105,7 @@ export default async function swapMarket({
{
[assetIn]: assetInAddress,
[feeAsset]: feeAssetAddress,
[nativeCryptocurrency]: ethers.constants.AddressZero,
[nativeCryptocurrency]: ethers.ZeroAddress,
},
aggregator,
walletAddress,
@@ -105,7 +117,7 @@ export default async function swapMarket({
balances,
{
name: nativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
provider,
signer,
@@ -123,7 +135,7 @@ export default async function swapMarket({
: undefined,
);
const { exchanges: swapExchanges } = swapInfo;
const { exchanges: swapExchanges, exchangeContractPath } = swapInfo;
const [firstSwapExchange] = swapExchanges;
@@ -184,12 +196,6 @@ export default async function swapMarket({
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();
@@ -221,45 +227,64 @@ export default async function swapMarket({
INTERNAL_PROTOCOL_PRECISION,
BigNumber.ROUND_FLOOR,
);
const unsignedSwapThroughOrionPoolTx = await exchangeContract.populateTransaction.swapThroughOrionPool(
amountSpendBlockchainParam,
amountReceiveBlockchainParam,
factoryAddress !== undefined
? [factoryAddress, ...pathAddresses]
: pathAddresses,
type === 'exactSpend',
const { calldata, swapDescription, value } = await generateSwapCalldata({
amount: amountSpendBlockchainParam,
minReturnAmount: amountReceiveBlockchainParam,
path: exchangeContractPath.filter(isValidSingleSwap),
initiatorAddress: walletAddress,
receiverAddress: walletAddress,
provider,
matcher: matcherAddress,
feeToken: feeAssetAddress,
fee: 0,
exchangeContractAddress,
swapExecutorContractAddress,
wethAddress: WETHAddress,
curveRegistryAddress: safeGet(unit.contracts, 'curveRegistry'),
})
const unsignedSwapThroughPoolsTx = await exchangeContract.swap.populateTransaction(
swapExecutorContractAddress,
swapDescription,
new Uint8Array(0),
calldata,
{
value
}
);
unsignedSwapThroughOrionPoolTx.chainId = parseInt(chainId, 10);
unsignedSwapThroughOrionPoolTx.gasPrice = ethers.BigNumber.from(gasPriceWei);
unsignedSwapThroughPoolsTx.chainId = BigInt(parseInt(chainId, 10));
unsignedSwapThroughPoolsTx.gasPrice = BigInt(gasPriceWei);
unsignedSwapThroughOrionPoolTx.from = walletAddress;
unsignedSwapThroughPoolsTx.from = walletAddress;
const amountSpendBN = new BigNumber(amountSpend);
let value = new BigNumber(0);
let txValue = 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);
txValue = amountSpendBN.minus(denormalizedAssetInExchangeBalance);
}
unsignedSwapThroughOrionPoolTx.value = normalizeNumber(
value.dp(INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_CEIL),
unsignedSwapThroughPoolsTx.value = normalizeNumber(
txValue.dp(INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_CEIL),
NATIVE_CURRENCY_PRECISION,
BigNumber.ROUND_CEIL,
);
unsignedSwapThroughOrionPoolTx.gasLimit = ethers.BigNumber.from(SWAP_THROUGH_ORION_POOL_GAS_LIMIT);
unsignedSwapThroughPoolsTx.gasLimit = BigInt(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);
const transactionCost = BigInt(SWAP_THROUGH_ORION_POOL_GAS_LIMIT) * BigInt(gasPriceWei);
const denormalizedTransactionCost = denormalizeNumber(transactionCost, BigInt(NATIVE_CURRENCY_PRECISION));
balanceGuard.registerRequirement({
reason: 'Network fee',
asset: {
name: nativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
amount: denormalizedTransactionCost.toString(),
sources: getAvailableSources('network_fee', ethers.constants.AddressZero, 'pool'),
sources: getAvailableSources('network_fee', ethers.ZeroAddress, 'pool'),
});
// if (value.gt(0)) {
@@ -267,24 +292,24 @@ export default async function swapMarket({
// reason: 'Transaction value (extra amount)',
// asset: {
// name: nativeCryptocurrency,
// address: ethers.constants.AddressZero,
// address: ethers.ZeroAddress,
// },
// amount: value.toString(),
// sources: getAvailableSources('amount', ethers.constants.AddressZero, 'pool'),
// sources: getAvailableSources('amount', ethers.ZeroAddress, 'pool'),
// });
// }
await balanceGuard.check(options?.autoApprove);
const nonce = await provider.getTransactionCount(walletAddress, 'pending');
unsignedSwapThroughOrionPoolTx.nonce = nonce;
unsignedSwapThroughPoolsTx.nonce = nonce;
options?.logger?.('Signing transaction...');
const swapThroughOrionPoolTxResponse = await signer.sendTransaction(unsignedSwapThroughOrionPoolTx);
const swapThroughOrionPoolTxResponse = await signer.sendTransaction(unsignedSwapThroughPoolsTx);
options?.logger?.(`Transaction sent. Tx hash: ${swapThroughOrionPoolTxResponse.hash}`);
return {
amountOut: swapInfo.amountOut,
wait: swapThroughOrionPoolTxResponse.wait,
wait: swapThroughOrionPoolTxResponse.wait.bind(swapThroughOrionPoolTxResponse),
through: 'pool',
txHash: swapThroughOrionPoolTxResponse.hash,
};
@@ -338,7 +363,7 @@ export default async function swapMarket({
gasPriceGwei,
feePercent,
baseAssetAddress,
ethers.constants.AddressZero,
ethers.ZeroAddress,
feeAssetAddress,
allPrices.prices,
);
@@ -384,7 +409,6 @@ export default async function swapMarket({
walletAddress,
matcherAddress,
feeAssetAddress,
false,
signer,
chainId,
);

View File

@@ -1,6 +1,6 @@
import { BigNumber } from 'bignumber.js';
import { ethers } from 'ethers';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v6/index.js';
import getBalances from '../../utils/getBalances.js';
import BalanceGuard from '../../BalanceGuard.js';
import type Unit from '../index.js';
@@ -50,7 +50,7 @@ export default async function withdraw({
const balances = await getBalances(
{
[asset]: assetAddress,
[nativeCryptocurrency]: ethers.constants.AddressZero,
[nativeCryptocurrency]: ethers.ZeroAddress,
},
aggregator,
walletAddress,
@@ -62,7 +62,7 @@ export default async function withdraw({
balances,
{
name: nativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
provider,
signer,
@@ -78,27 +78,27 @@ export default async function withdraw({
sources: ['exchange'],
});
const unsignedTx = await exchangeContract.populateTransaction.withdraw(
const unsignedTx = await exchangeContract.withdraw.populateTransaction(
assetAddress,
normalizeNumber(amount, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR),
normalizeNumber(amount, INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR).toString(),
);
unsignedTx.gasLimit = ethers.BigNumber.from(WITHDRAW_GAS_LIMIT);
unsignedTx.gasLimit = BigInt(WITHDRAW_GAS_LIMIT);
const transactionCost = ethers.BigNumber.from(unsignedTx.gasLimit).mul(gasPriceWei);
const denormalizedTransactionCost = denormalizeNumber(transactionCost, NATIVE_CURRENCY_PRECISION);
const transactionCost = BigInt(unsignedTx.gasLimit) * BigInt(gasPriceWei);
const denormalizedTransactionCost = denormalizeNumber(transactionCost, BigInt(NATIVE_CURRENCY_PRECISION));
balanceGuard.registerRequirement({
reason: 'Network fee',
asset: {
name: nativeCryptocurrency,
address: ethers.constants.AddressZero,
address: ethers.ZeroAddress,
},
amount: denormalizedTransactionCost.toString(),
sources: ['wallet'],
});
unsignedTx.chainId = parseInt(chainId, 10);
unsignedTx.gasPrice = ethers.BigNumber.from(gasPriceWei);
unsignedTx.chainId = BigInt(chainId);
unsignedTx.gasPrice = BigInt(gasPriceWei);
unsignedTx.from = walletAddress;
await balanceGuard.check(true);
@@ -107,11 +107,11 @@ export default async function withdraw({
unsignedTx.nonce = nonce;
const signedTx = await signer.signTransaction(unsignedTx);
const txResponse = await provider.sendTransaction(signedTx);
const txResponse = await provider.broadcastTransaction(signedTx);
console.log(`Withdraw tx sent: ${txResponse.hash}. Waiting for confirmation...`);
try {
const txReceipt = await txResponse.wait();
if (txReceipt.status !== undefined) {
if (txReceipt?.status !== undefined) {
console.log('Withdraw tx confirmed');
} else {
console.log('Withdraw tx failed');

View File

@@ -1,410 +0,0 @@
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}`);
}
}
}

View File

@@ -1,35 +1,39 @@
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 { JsonRpcProvider } from 'ethers';
import { Aggregator } from '../services/Aggregator';
import { BlockchainService } from '../services/BlockchainService';
import { PriceFeed } from '../services/PriceFeed';
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 { chains, envs } from '../config';
import type { networkCodes } from '../constants/index.js';
import { IndexerService } from '../services/Indexer';
type KnownConfig = {
env: KnownEnv
chainId: SupportedChainId
}
};
export default class Unit {
public readonly networkCode: typeof networkCodes[number];
public readonly networkCode: (typeof networkCodes)[number];
public readonly chainId: SupportedChainId;
public readonly provider: ethers.providers.StaticJsonRpcProvider;
public readonly provider: JsonRpcProvider;
public readonly blockchainService: BlockchainService;
public readonly indexer: IndexerService | undefined;
public readonly aggregator: Aggregator;
public readonly priceFeed: PriceFeed;
public readonly exchange: Exchange;
public readonly farmingManager: FarmingManager;
public readonly config: VerboseUnitConfig;
public readonly contracts: Record<string, string>;
@@ -37,13 +41,33 @@ export default class Unit {
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(', ')}`);
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(', ')}`);
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(', ')}`);
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,
@@ -58,30 +82,45 @@ export default class Unit {
priceFeed: {
api: networkConfig.api + networkConfig.services.priceFeed.all,
},
indexer: {
api: networkConfig.api + networkConfig.services.indexer?.http,
},
},
}
};
} 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;
this.contracts = chainInfo.contracts
this.contracts = chainInfo.contracts;
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);
if (Number.isNaN(intNetwork)) {
throw new Error('Invalid chainId (not a number)' + this.chainId);
}
this.provider = new JsonRpcProvider(this.config.nodeJsonRpc, intNetwork);
this.provider.pollingInterval = 1000;
this.blockchainService = new BlockchainService(this.config.services.blockchainService.http, this.config.basicAuth);
this.blockchainService = new BlockchainService(
this.config.services.blockchainService.http,
this.config.basicAuth
);
this.indexer = this.config.services.indexer
? new IndexerService(
this.config.services.indexer.api,
intNetwork
)
: undefined;
this.aggregator = new Aggregator(
this.config.services.aggregator.http,
this.config.services.aggregator.ws,
this.config.basicAuth,
this.config.basicAuth
);
this.priceFeed = new PriceFeed(
this.config.services.priceFeed.api,
this.config.basicAuth
);
this.priceFeed = new PriceFeed(this.config.services.priceFeed.api, this.config.basicAuth);
this.exchange = new Exchange(this);
this.farmingManager = new FarmingManager(this);
}
}

View File

@@ -177,6 +177,9 @@ describe('Orion', () => {
priceFeed: {
api: orionPriceFeedAPI + '/price-feed',
},
indexer: {
api: '',
}
},
}
}
@@ -195,7 +198,7 @@ describe('Orion', () => {
expect(unit.aggregator.ws.api).toBe(`ws://localhost:${server1.port}/v1`);
expect(unit.blockchainService.api).toBe(blockchainServiceAPI);
expect(unit.priceFeed.api).toBe(orionPriceFeedAPI + '/price-feed');
expect(unit.provider.connection.url).toBe('https://cloudflare-eth.com/');
expect(unit.provider._getConnection().url).toBe('https://cloudflare-eth.com/');
const info = await simpleFetch(unit.blockchainService.getInfo)();
expect(info).toBeDefined();
@@ -246,7 +249,7 @@ describe('Orion', () => {
});
const bscUnit = orion.units[SupportedChainId.BSC_TESTNET]
expect(bscUnit?.provider.connection.url).toBe('https://data-seed-prebsc-1-s1.binance.org:8545/');
expect(bscUnit?.provider._getConnection().url).toBe('https://data-seed-prebsc-1-s1.binance.org:8545/');
expect(orion.referralSystem.api).toBe('https://zxczxc.orionprotocol.io');
});
@@ -312,7 +315,7 @@ describe('Orion', () => {
const network = await unitBSC.provider.getNetwork();
expect(network.chainId).toBe(97);
const zeroAddressWithout0x = ethers.constants.AddressZero.slice(2);
const zeroAddressWithout0x = ethers.ZeroAddress.slice(2);
expect(simpleFetch(orion.referralSystem.getMiniStats)(zeroAddressWithout0x))
.rejects
.toThrow('empty reward history');

View File

@@ -83,7 +83,7 @@ describe('Fee calculation', () => {
const gasPriceGwei = 3;
const feePercent = 0.2;
const baseAssetAddress = '0xcb2951e90d8dcf16e1fa84ac0c83f48906d6a744';
const baseCurrencyAddress = ethers.constants.AddressZero;
const baseCurrencyAddress = ethers.ZeroAddress;
const feeAssetAddress = '0xf223eca06261145b3287a0fefd8cfad371c7eb34';
const { totalFeeInFeeAsset: ornTotalFee } = calculateFeeInFeeAsset(
amount,

View File

@@ -1,7 +1,7 @@
import { ethers } from 'ethers';
import { z } from 'zod';
const addressSchema = z.string().refine(ethers.utils.isAddress, (value) => ({
const addressSchema = z.string().refine(ethers.isAddress, (value) => ({
message: `Should be an address, got ${value}`,
}));

View File

@@ -2,10 +2,10 @@
"1": {
"chainId": "1",
"explorer": "https://etherscan.io/",
"label": "Ethereum Mainnet",
"label": "Ethereum",
"shortName": "ETH",
"code": "eth",
"rpc": "https://trade.orionprotocol.io/eth-mainnet/rpc",
"rpc": "https://trade.orion.xyz/eth-mainnet/rpc",
"baseCurrencyName": "ETH",
"contracts": {
"WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
@@ -15,10 +15,10 @@
"56": {
"chainId": "56",
"explorer": "https://bscscan.com/",
"label": "Binance Smart Chain",
"label": "BNB Chain",
"shortName": "BSC",
"code": "bsc",
"rpc": "https://bsc-dataseed.binance.org/",
"rpc": "https://bsc-dataseed.bnbchain.org/",
"baseCurrencyName": "BNB",
"contracts": {
"WETH": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
@@ -28,14 +28,27 @@
"97": {
"chainId": "97",
"explorer": "https://testnet.bscscan.com/",
"label": "Binance Smart Chain Testnet",
"label": "BNB Chain Testnet",
"shortName": "BSC-Testnet",
"code": "bsc",
"rpc": "https://data-seed-prebsc-1-s1.bnbchain.org:8545/",
"baseCurrencyName": "BNB",
"contracts": {
"WETH": "0x23eE96bEaAB62abE126AA192e677c52bB7d274F0",
"curveRegistry": "0x8845b36C3DE93379F468590FFa102D4aF8C49A6c"
"curveRegistry": "0x93461072c00b2740c474a3d52c70be6249c802d8"
}
},
"204": {
"chainId": "204",
"explorer": "http://opbnbscan.com/",
"label": "opBNB Chain",
"shortName": "opBNB",
"code": "opbnb",
"rpc": "https://opbnb-mainnet-rpc.bnbchain.org",
"baseCurrencyName": "BNB",
"contracts": {
"WETH": "0x4200000000000000000000000000000000000006",
"curveRegistry": ""
}
},
"3": {
@@ -44,7 +57,7 @@
"label": "Ropsten",
"shortName": "ETH-Ropsten",
"code": "eth",
"rpc": "https://testing.orionprotocol.io/eth-ropsten/rpc",
"rpc": "https://testing.orion.xyz/eth-ropsten/rpc",
"baseCurrencyName": "ETH",
"contracts": {
"WETH": "",
@@ -57,7 +70,7 @@
"label": "Goerli",
"shortName": "ETH-Goerli",
"code": "eth",
"rpc": "https://testing.orionprotocol.io/eth-goerli/rpc",
"rpc": "https://testing.orion.xyz/eth-goerli/rpc",
"baseCurrencyName": "ETH",
"contracts": {
"WETH": "",
@@ -77,6 +90,19 @@
"curveRegistry": ""
}
},
"42161": {
"chainId": "42161",
"explorer": "https://arbiscan.io/",
"label": "Arbitrum",
"shortName": "Arbitrum",
"code": "arb",
"rpc": "https://arb1.arbitrum.io/rpc",
"baseCurrencyName": "ETH",
"contracts": {
"WETH": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"curveRegistry": "0x445FE580eF8d70FF569aB36e80c647af338db351"
}
},
"4002": {
"chainId": "4002",
"explorer": "https://testnet.ftmscan.com/",
@@ -93,27 +119,27 @@
"250": {
"chainId": "250",
"explorer": "https://ftmscan.com/",
"label": "Fantom Mainnet",
"label": "Fantom",
"shortName": "FTM",
"code": "ftm",
"rpc": "https://rpcapi.fantom.network/",
"baseCurrencyName": "FTM",
"contracts": {
"WETH": "",
"curveRegistry": ""
"WETH": "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83",
"curveRegistry": "0x0f854EA9F38ceA4B1c2FC79047E9D0134419D5d6"
}
},
"137": {
"chainId": "137",
"label": "Polygon Mainnet",
"label": "Polygon",
"shortName": "Polygon",
"code": "polygon",
"baseCurrencyName": "MATIC",
"rpc": "https://polygon-rpc.com/",
"explorer": "https://polygonscan.com/",
"contracts": {
"WETH": "",
"curveRegistry": ""
"WETH": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
"curveRegistry": "0x094d12e5b541784701FD8d65F11fc0598FBC6332"
}
},
"80001": {
@@ -132,13 +158,13 @@
"66": {
"chainId": "66",
"explorer": "https://www.oklink.com/okc/",
"label": "OKC Mainnet",
"label": "OKT Chain",
"shortName": "OKC",
"code": "okc",
"rpc": "https://exchainrpc.okex.org/",
"baseCurrencyName": "OKT",
"contracts": {
"WETH": "",
"WETH": "0x843340759bFCa4a3776F401cD2E03fE9bc0d838f",
"curveRegistry": ""
}
},
@@ -161,11 +187,37 @@
"shortName": "DRIP Chain",
"code": "drip",
"baseCurrencyName": "DRIP",
"rpc": "testnet.1d.rip",
"rpc": "https://testnet.1d.rip/",
"explorer": "https://explorer-testnet.1d.rip/",
"contracts": {
"WETH": "",
"curveRegistry": ""
}
},
"2525": {
"chainId": "2525",
"label": "inEVM",
"shortName": "inEVM",
"code": "inevm",
"baseCurrencyName": "INJ",
"rpc": "https://inevm.calderachain.xyz/http/",
"explorer": "https://explorer.injective.network/",
"contracts": {
"WETH": "0x4C3A213bd5e8c4BD70a8396d6F3C8302571598Cd",
"curveRegistry": ""
}
},
"59144": {
"chainId": "59144",
"label": "Linea",
"shortName": "Linea",
"code": "linea",
"baseCurrencyName": "ETH",
"rpc": "https://rpc.linea.build/",
"explorer": "https://lineascan.build/",
"contracts": {
"WETH": "0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f",
"curveRegistry": ""
}
}
}
}

View File

@@ -1,10 +1,10 @@
{
"production": {
"analyticsAPI": "https://trade.orionprotocol.io/api/stats",
"referralAPI": "https://trade.orionprotocol.io/referral-api",
"analyticsAPI": "https://trade.orion.xyz/api/stats",
"referralAPI": "https://trade.orion.xyz/referral-api",
"networks": {
"1": {
"api": "https://trade.orionprotocol.io/eth-mainnet",
"api": "https://trade.orion.xyz/eth-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -15,12 +15,15 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
},
"liquidityMigratorAddress": "0x23a1820a47BcD022E29f6058a5FD224242F50D1A"
},
"56": {
"api": "https://trade.orionprotocol.io/bsc-mainnet",
"api": "https://trade.orion.xyz/bsc-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -31,11 +34,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"250": {
"api": "https://trade.orionprotocol.io/ftm-mainnet",
"api": "https://trade.orion.xyz/ftm-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -46,11 +52,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"137": {
"api": "https://trade.orionprotocol.io/polygon-mainnet",
"api": "https://trade.orion.xyz/polygon-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -61,11 +70,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"66": {
"api": "https://trade.orionprotocol.io/okc-mainnet",
"api": "https://trade.orion.xyz/okc-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -76,17 +88,92 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"42161": {
"api": "https://trade.orion.xyz/arbitrum-one",
"services": {
"aggregator": {
"http": "/backend",
"ws": "/v1"
},
"blockchain": {
"http": ""
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"204": {
"api": "https://trade.orion.xyz/opbnb-mainnet",
"services": {
"aggregator": {
"http": "/backend",
"ws": "/v1"
},
"blockchain": {
"http": ""
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"2525": {
"api": "https://trade.orion.xyz/inevm-mainnet",
"services": {
"aggregator": {
"http": "/backend",
"ws": "/v1"
},
"blockchain": {
"http": ""
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"59144": {
"api": "https://trade.orion.xyz/linea-mainnet",
"services": {
"aggregator": {
"http": "/backend",
"ws": "/v1"
},
"blockchain": {
"http": ""
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
}
}
},
"testing": {
"analyticsAPI": "https://trade.orionprotocol.io/api/stats",
"referralAPI": "https://testing.orionprotocol.io/referral-api",
"analyticsAPI": "https://trade.orion.xyz/api/stats",
"referralAPI": "https://testing.orion.xyz/referral-api",
"networks": {
"97": {
"api": "https://testing.orionprotocol.io/bsc-testnet",
"api": "https://testing.orion.xyz/bsc-testnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -97,12 +184,15 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
},
"liquidityMigratorAddress": "0x01b10dds12478C88A5E18e2707E729906bC25CfF6"
},
"5": {
"api": "https://testing.orionprotocol.io/eth-goerli",
"api": "https://testing.orion.xyz/eth-goerli",
"services": {
"aggregator": {
"http": "/backend",
@@ -113,11 +203,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"421613": {
"api": "https://testing.orionprotocol.io/arbitrum-goerli",
"api": "https://testing.orion.xyz/arbitrum-goerli",
"services": {
"aggregator": {
"http": "/backend",
@@ -128,11 +221,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"4002": {
"api": "https://testing.orionprotocol.io/ftm-testnet",
"api": "https://testing.orion.xyz/ftm-testnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -143,11 +239,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"80001": {
"api": "https://testing.orionprotocol.io/polygon-mumbai",
"api": "https://testing.orion.xyz/polygon-mumbai",
"services": {
"aggregator": {
"http": "/backend",
@@ -158,11 +257,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"56303": {
"api": "https://testing.orionprotocol.io/drip-testnet",
"api": "https://testing.orion.xyz/drip-testnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -173,17 +275,20 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
}
}
},
"staging": {
"analyticsAPI": "https://trade.orionprotocol.io/api/stats",
"referralAPI": "https://staging.orionprotocol.io/referral-api",
"analyticsAPI": "https://trade.orion.xyz/api/stats",
"referralAPI": "https://staging.orion.xyz/referral-api",
"networks": {
"1": {
"api": "https://staging.orionprotocol.io/eth-mainnet",
"api": "https://staging.orion.xyz/eth-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -194,11 +299,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"56": {
"api": "https://staging.orionprotocol.io/bsc-mainnet",
"api": "https://staging.orion.xyz/bsc-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -209,11 +317,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"250": {
"api": "https://staging.orionprotocol.io/ftm-mainnet",
"api": "https://staging.orion.xyz/ftm-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -224,11 +335,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"137": {
"api": "https://staging.orionprotocol.io/polygon-mainnet",
"api": "https://staging.orion.xyz/polygon-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -239,11 +353,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"66": {
"api": "https://staging.orionprotocol.io/okc-mainnet",
"api": "https://staging.orion.xyz/okc-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -254,17 +371,92 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"42161": {
"api": "https://staging.orion.xyz/arbitrum-one",
"services": {
"aggregator": {
"http": "/backend",
"ws": "/v1"
},
"blockchain": {
"http": ""
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"204": {
"api": "https://staging.orion.xyz/opbnb-mainnet",
"services": {
"aggregator": {
"http": "/backend",
"ws": "/v1"
},
"blockchain": {
"http": ""
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"2525": {
"api": "https://trade.orion.xyz/inevm-mainnet",
"services": {
"aggregator": {
"http": "/backend",
"ws": "/v1"
},
"blockchain": {
"http": ""
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"59144": {
"api": "https://trade.orion.xyz/linea-mainnet",
"services": {
"aggregator": {
"http": "/backend",
"ws": "/v1"
},
"blockchain": {
"http": ""
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
}
}
},
"experimental": {
"analyticsAPI": "https://trade.orionprotocol.io/api/stats",
"referralAPI": "https://testing.orionprotocol.io/referral-api",
"analyticsAPI": "https://trade.orion.xyz/api/stats",
"referralAPI": "https://testing.orion.xyz/referral-api",
"networks": {
"97": {
"api": "https://dn-dev.orionprotocol.io/bsc-testnet",
"api": "https://dn-dev.orion.xyz/bsc-testnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -275,11 +467,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"3": {
"api": "https://dn-dev.orionprotocol.io/eth-ropsten",
"api": "https://dn-dev.orion.xyz/eth-ropsten",
"services": {
"aggregator": {
"http": "/backend",
@@ -290,17 +485,20 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
}
}
},
"kucoin-production": {
"analyticsAPI": "https://trade.orionprotocol.io/api/stats",
"referralAPI": "https://trade.orionprotocol.io/referral-api",
"analyticsAPI": "https://trade.orion.xyz/api/stats",
"referralAPI": "https://trade.orion.xyz/referral-api",
"networks": {
"1": {
"api": "https://trade.orionprotocol.io/eth-mainnet",
"api": "https://trade.orion.xyz/eth-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -311,12 +509,15 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
},
"liquidityMigratorAddress": "0x23a1820a47BcD022E29f6058a5FD224242F50D1A"
},
"56": {
"api": "https://trade.orionprotocol.io/bsc-mainnet",
"api": "https://trade.orion.xyz/bsc-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -327,11 +528,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"250": {
"api": "https://trade.orionprotocol.io/ftm-mainnet",
"api": "https://trade.orion.xyz/ftm-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -342,11 +546,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"137": {
"api": "https://trade.orionprotocol.io/polygon-mainnet",
"api": "https://trade.orion.xyz/polygon-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -357,11 +564,14 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
},
"66": {
"api": "https://trade.orionprotocol.io/okc-mainnet",
"api": "https://trade.orion.xyz/okc-mainnet",
"services": {
"aggregator": {
"http": "/backend",
@@ -372,9 +582,12 @@
},
"priceFeed": {
"all": "/price-feed"
},
"indexer": {
"http": "/orion-indexer/"
}
}
}
}
}
}
}

View File

@@ -14,6 +14,9 @@ export const pureEnvNetworksSchema = z.object({
priceFeed: z.object({
all: z.string(),
}),
indexer: z.object({
http: z.string(),
}).optional(),
}),
rpc: z.string().optional(),
liquidityMigratorAddress: z.string().optional(),

View File

@@ -15,4 +15,8 @@ export const productionChains = [
SupportedChainId.FANTOM_OPERA,
SupportedChainId.POLYGON,
SupportedChainId.OKC,
SupportedChainId.ARBITRUM,
SupportedChainId.OPBNB,
SupportedChainId.INEVM,
SupportedChainId.LINEA,
];

View File

@@ -23,4 +23,5 @@ export default [
'OKXSWAP',
'CURVE',
'CURVE_FACTORY',
'THENA_ALGEBRA_V1',
] as const;

View File

@@ -12,17 +12,14 @@ const mapping: Record<string, string> = {
PANCAKESWAP: 'PancakeSwap',
UNISWAP: 'Uniswap',
QUICKSWAP: 'QuickSwap',
ORION_POOL: 'Orion Pool',
INTERNAL_POOL_V2: 'Orion Pool V2',
INTERNAL_POOL_V3: 'Orion Pool V3',
INTERNAL_POOL_V3_0_01: 'Orion Pool V3',
INTERNAL_POOL_V3_0_05: 'Orion Pool V3',
INTERNAL_POOL_V3_0_3: 'Orion Pool V3',
INTERNAL_POOL_V3_1_0: 'Orion Pool V3',
ORION_POOL: 'Internal Pool',
INTERNAL_POOL_V2: 'Internal Pool V2',
INTERNAL_POOL_V3: 'Internal Pool V3',
CHERRYSWAP: 'CherrySwap',
OKXSWAP: 'OKXSwap',
CURVE: 'Curve',
CURVE_FACTORY: 'Curve Factory',
THENA_ALGEBRA_V1: 'Thena',
};
export default mapping;

View File

@@ -0,0 +1 @@
export default ["UniswapV2", "UniswapV3", "Curve", "OrionV2", "OrionV3"] as const

View File

@@ -9,5 +9,6 @@ export { default as exchangesMap } from './exchangesMap.js';
export * from './chains.js';
export * from './precisions.js';
export * from './gasLimits.js';
export * from './timings.js';
export const SERVICE_TOKEN = 'ORN';

View File

@@ -1 +1 @@
export default ['ftm', 'bsc', 'eth', 'polygon', 'okc', 'arb', 'drip'] as const;
export default ['ftm', 'bsc', 'eth', 'polygon', 'okc', 'arb', 'drip', 'opbnb', 'inevm', 'linea'] as const;

3
src/constants/timings.ts Normal file
View File

@@ -0,0 +1,3 @@
export const DAY = 86400
export const WEEK_DAYS = 7;
export const YEAR = 365 * DAY

View File

@@ -1 +1 @@
export default ['FTM', 'BSC', 'ETH', 'POLYGON', 'OKC', 'ARB'] as const;
export default ['FTM', 'BSC', 'ETH', 'POLYGON', 'OKC', 'ARB', 'OPBNB', 'INEVM', 'LINEA'] as const;

View File

@@ -1,35 +1,16 @@
import { ethers } from 'ethers';
import { ethers, keccak256 } from 'ethers';
import type { Order } from '../types.js';
const hashOrder = (order: Order) => ethers.utils.solidityKeccak256(
[
'uint8',
'address',
'address',
'address',
'address',
'address',
'uint64',
'uint64',
'uint64',
'uint64',
'uint64',
'uint8',
],
[
'0x03',
order.senderAddress,
order.matcherAddress,
order.baseAsset,
order.quoteAsset,
order.matcherFeeAsset,
order.amount,
order.price,
order.matcherFee,
order.nonce,
order.expiration,
order.buySide === 1 ? '0x01' : '0x00',
],
);
const ORDER_TYPEHASH = "0xb5132db62dfceb466f2f8aee7a039db36a99772e5a9771d28388a5f9baad7c54"
export default hashOrder;
export default function getOrderHash(order: Order) {
const abiCoder = ethers.AbiCoder.defaultAbiCoder()
const { senderAddress, matcherAddress, baseAsset, quoteAsset, matcherFeeAsset, amount, price, matcherFee, nonce, expiration, buySide } = order
const orderBytes = abiCoder.encode(
["bytes32", "address", "address", "address", "address", "address", "uint64", "uint64", "uint64", "uint64", "uint64", "uint8"],
[ORDER_TYPEHASH, senderAddress, matcherAddress, baseAsset, quoteAsset, matcherFeeAsset, amount, price, matcherFee, nonce, expiration, buySide]
)
return keccak256(orderBytes)
}

View File

@@ -1,4 +1,2 @@
export { default as signCancelOrder } from './signCancelOrder.js';
export { default as signCancelOrderPersonal } from './signCancelOrderPersonal.js';
export { default as signOrder } from './signOrder.js';
export { default as signOrderPersonal } from './signOrderPersonal.js';

View File

@@ -1,40 +1,33 @@
import type { TypedDataSigner } from '@ethersproject/abstract-signer';
import type { ethers } from 'ethers';
import { joinSignature, splitSignature } from 'ethers/lib/utils.js';
import { ethers } from 'ethers';
import CANCEL_ORDER_TYPES from '../constants/cancelOrderTypes.js';
import type { CancelOrderRequest, SignedCancelOrderRequest, SupportedChainId } from '../types.js';
import getDomainData from './getDomainData.js';
import signCancelOrderPersonal from './signCancelOrderPersonal.js';
type SignerWithTypedDataSign = ethers.Signer & TypedDataSigner;
const signCancelOrder = async (
senderAddress: string,
id: string,
usePersonalSign: boolean,
signer: ethers.Signer,
chainId: SupportedChainId,
) => {
const cancelOrderRequest: CancelOrderRequest = {
id,
senderAddress,
isPersonalSign: usePersonalSign,
};
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const typedDataSigner = signer as SignerWithTypedDataSign;
const signature = usePersonalSign
? await signCancelOrderPersonal(cancelOrderRequest, signer)
// https://docs.ethers.io/v5/api/signer/#Signer-signTypedData
: await typedDataSigner._signTypedData(
getDomainData(chainId),
CANCEL_ORDER_TYPES,
cancelOrderRequest,
);
const signature = await typedDataSigner.signTypedData(
getDomainData(chainId),
CANCEL_ORDER_TYPES,
cancelOrderRequest,
);
// https://github.com/poap-xyz/poap-fun/pull/62#issue-928290265
// "Signature's v was always send as 27 or 28, but from Ledger was 0 or 1"
const fixedSignature = joinSignature(splitSignature(signature));
const fixedSignature = ethers.Signature.from(signature).serialized;
// if (!fixedSignature) throw new Error("Can't sign order cancel");

View File

@@ -1,20 +0,0 @@
import { ethers } from 'ethers';
import { arrayify, joinSignature, splitSignature } from 'ethers/lib/utils.js';
import type { CancelOrderRequest } from '../types.js';
const signCancelOrderPersonal = async (
cancelOrderRequest: CancelOrderRequest,
signer: ethers.Signer,
) => {
const types = ['string', 'string', 'address'];
const message = ethers.utils.solidityKeccak256(
types,
['cancelOrder', cancelOrderRequest.id, cancelOrderRequest.senderAddress],
);
const signature = await signer.signMessage(arrayify(message));
// NOTE: metamask broke sig.v value and we fix it in next line
return joinSignature(splitSignature(signature));
};
export default signCancelOrderPersonal;

View File

@@ -1,14 +1,12 @@
import type { TypedDataSigner } from '@ethersproject/abstract-signer';
import { BigNumber } from 'bignumber.js';
import type { ethers } from 'ethers';
import { joinSignature, splitSignature } from 'ethers/lib/utils.js';
import { ethers } from 'ethers';
import { INTERNAL_PROTOCOL_PRECISION } from '../constants/index.js';
import ORDER_TYPES from '../constants/orderTypes.js';
import type { Order, SignedOrder, SupportedChainId } from '../types.js';
import normalizeNumber from '../utils/normalizeNumber.js';
import getDomainData from './getDomainData.js';
import hashOrder from './hashOrder.js';
import signOrderPersonal from './signOrderPersonal.js';
const DEFAULT_EXPIRATION = 29 * 24 * 60 * 60 * 1000; // 29 days
@@ -24,7 +22,6 @@ export const signOrder = async (
senderAddress: string,
matcherAddress: string,
serviceFeeAssetAddr: string,
usePersonalSign: boolean,
signer: ethers.Signer,
chainId: SupportedChainId,
) => {
@@ -37,41 +34,38 @@ export const signOrder = async (
baseAsset: baseAssetAddr,
quoteAsset: quoteAssetAddr,
matcherFeeAsset: serviceFeeAssetAddr,
amount: normalizeNumber(
amount: Number(normalizeNumber(
amount,
INTERNAL_PROTOCOL_PRECISION,
BigNumber.ROUND_FLOOR,
).toNumber(),
price: normalizeNumber(
)),
price: Number(normalizeNumber(
price,
INTERNAL_PROTOCOL_PRECISION,
BigNumber.ROUND_FLOOR,
).toNumber(),
matcherFee: normalizeNumber(
)),
matcherFee: Number(normalizeNumber(
matcherFee,
INTERNAL_PROTOCOL_PRECISION,
BigNumber.ROUND_CEIL, // ROUND_CEIL because we don't want get "not enough fee" error
).toNumber(),
)),
nonce,
expiration,
buySide: side === 'BUY' ? 1 : 0,
isPersonalSign: usePersonalSign,
};
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const typedDataSigner = signer as SignerWithTypedDataSign;
const signature = usePersonalSign
? await signOrderPersonal(order, signer)
: await typedDataSigner._signTypedData(
getDomainData(chainId),
ORDER_TYPES,
order,
);
const signature = await typedDataSigner.signTypedData(
getDomainData(chainId),
ORDER_TYPES,
order,
);
// https://github.com/poap-xyz/poap-fun/pull/62#issue-928290265
// "Signature's v was always send as 27 or 28, but from Ledger was 0 or 1"
const fixedSignature = joinSignature(splitSignature(signature));
const fixedSignature = ethers.Signature.from(signature).serialized;
// if (!fixedSignature) throw new Error("Can't sign order");

View File

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

View File

@@ -4,6 +4,8 @@ BigNumber.config({ EXPONENTIAL_AT: 1e+9 });
export * as config from './config/index.js';
export { default as Unit } from './Unit/index.js';
export { default as Orion } from './Orion/index.js';
export {generateSwapCalldata} from './Unit/Exchange/generateSwapCalldata.js';
export { default as factories} from './constants/factories.js';
export * as utils from './utils/index.js';
export * as services from './services/index.js';
export * as crypt from './crypt/index.js';

View File

@@ -8,7 +8,7 @@ import errorSchema from './schemas/errorSchema.js';
import placeAtomicSwapSchema from './schemas/placeAtomicSwapSchema.js';
import { AggregatorWS } from './ws/index.js';
import { atomicSwapHistorySchema } from './schemas/atomicSwapHistorySchema.js';
import type { BasicAuthCredentials, SignedCancelOrderRequest, SignedOrder } from '../../types.js';
import type { BasicAuthCredentials, OrderSource, SignedCancelOrderRequest, SignedOrder } from '../../types.js';
import {
pairConfigSchema, aggregatedOrderbookSchema,
exchangeOrderbookSchema, poolReservesSchema,
@@ -74,13 +74,13 @@ class Aggregator {
}
getOrder = (orderId: string, owner?: string) => {
if (!ethers.utils.isHexString(orderId)) {
if (!ethers.isHexString(orderId)) {
throw new Error(`Invalid order id: ${orderId}. Must be a hex string`);
}
const url = new URL(`${this.apiUrl}/api/v1/order`);
url.searchParams.append('orderId', orderId);
if (owner !== undefined) {
if (!ethers.utils.isAddress(owner)) {
if (!ethers.isAddress(owner)) {
throw new Error(`Invalid owner address: ${owner}`);
}
url.searchParams.append('owner', owner);
@@ -196,7 +196,8 @@ class Aggregator {
isCreateInternalOrder: boolean,
isReversedOrder?: boolean,
partnerId?: string,
fromWidget?: boolean,
source?: OrderSource,
rawExchangeRestrictions?: string | undefined,
) => {
const headers = {
'Content-Type': 'application/json',
@@ -205,7 +206,7 @@ class Aggregator {
'X-Reverse-Order': isReversedOrder ? 'true' : 'false',
},
...(partnerId !== undefined) && { 'X-Partner-Id': partnerId },
...(fromWidget !== undefined) && { 'X-From-Widget': fromWidget ? 'true' : 'false' },
...(source !== undefined) && { 'X-Source': source },
...this.basicAuthHeaders,
};
@@ -226,7 +227,7 @@ class Aggregator {
{
headers,
method: 'POST',
body: JSON.stringify(signedOrder),
body: JSON.stringify({ ...signedOrder, rawExchangeRestrictions }),
},
errorSchema,
);

View File

@@ -3,22 +3,22 @@ import { z } from 'zod';
import { exchanges, orderStatuses, subOrderStatuses } from '../../../constants/index.js';
const blockchainOrderSchema = z.object({
id: z.string().refine(ethers.utils.isHexString, (value) => ({
id: z.string().refine(ethers.isHexString, (value) => ({
message: `blockchainOrder.id must be a hex string, got ${value}`,
})),
senderAddress: z.string().refine(ethers.utils.isAddress, (value) => ({
senderAddress: z.string().refine(ethers.isAddress, (value) => ({
message: `blockchainOrder.senderAddress must be an address, got ${value}`,
})),
matcherAddress: z.string().refine(ethers.utils.isAddress, (value) => ({
matcherAddress: z.string().refine(ethers.isAddress, (value) => ({
message: `blockchainOrder.matcherAddress must be an address, got ${value}`,
})),
baseAsset: z.string().refine(ethers.utils.isAddress, (value) => ({
baseAsset: z.string().refine(ethers.isAddress, (value) => ({
message: `blockchainOrder.baseAsset must be an address, got ${value}`,
})),
quoteAsset: z.string().refine(ethers.utils.isAddress, (value) => ({
quoteAsset: z.string().refine(ethers.isAddress, (value) => ({
message: `blockchainOrder.quoteAsset must be an address, got ${value}`,
})),
matcherFeeAsset: z.string().refine(ethers.utils.isAddress, (value) => ({
matcherFeeAsset: z.string().refine(ethers.isAddress, (value) => ({
message: `blockchainOrder.matcherFeeAsset must be an address, got ${value}`,
})),
amount: z.number().int().nonnegative(),
@@ -27,7 +27,7 @@ const blockchainOrderSchema = z.object({
nonce: z.number(),
expiration: z.number(),
buySide: z.union([z.literal(1), z.literal(0)]),
signature: z.string().refine(ethers.utils.isHexString, (value) => ({
signature: z.string().refine(ethers.isHexString, (value) => ({
message: `blockchainOrder.signature must be a hex string, got ${value}`,
})).nullable(),
isPersonalSign: z.boolean(),
@@ -43,8 +43,6 @@ const tradeInfoSchema = z.object({
updateTime: z.number(),
matchedBlockchainOrder: blockchainOrderSchema.optional(),
matchedSubOrderId: z.number().int().nonnegative().optional(),
exchangeTradeInfo: z.boolean(),
poolTradeInfo: z.boolean(),
});
const baseOrderSchema = z.object({
@@ -53,7 +51,7 @@ const baseOrderSchema = z.object({
amount: z.number().nonnegative(),
remainingAmount: z.number().nonnegative(),
price: z.number().nonnegative(),
sender: z.string().refine(ethers.utils.isAddress, (value) => ({
sender: z.string().refine(ethers.isAddress, (value) => ({
message: `order.sender must be an address, got ${value}`,
})),
filledAmount: z.number().nonnegative(),
@@ -77,16 +75,17 @@ const brokerAddressSchema = z.enum([
'SELF_BROKER'
])
.or(selfBrokerSchema)
.or(z.string().refine(ethers.utils.isAddress, (value) => ({
.or(z.string().refine(ethers.isAddress, (value) => ({
message: `subOrder.subOrders.[n].brokerAddress must be an address, got ${value}`,
})));
const subOrderSchema = baseOrderSchema.extend({
price: z.number(),
id: z.number(),
parentOrderId: z.string().refine(ethers.utils.isHexString, (value) => ({
parentOrderId: z.string().refine(ethers.isHexString, (value) => ({
message: `subOrder.parentOrderId must be a hex string, got ${value}`,
})),
exchange: z.string(),
exchanges: z.string().array().optional(),
brokerAddress: brokerAddressSchema,
tradesInfo: z.record(
z.string().uuid(),
@@ -97,11 +96,11 @@ const subOrderSchema = baseOrderSchema.extend({
});
const orderSchema = z.object({
orderId: z.string().refine(ethers.utils.isHexString, (value) => ({
orderId: z.string().refine(ethers.isHexString, (value) => ({
message: `orderId must be a hex string, got ${value}`,
})),
order: baseOrderSchema.extend({
id: z.string().refine(ethers.utils.isHexString, (value) => ({
id: z.string().refine(ethers.isHexString, (value) => ({
message: `order.id must be a hex string, got ${value}`,
})),
fee: z.number().nonnegative(),

View File

@@ -7,6 +7,13 @@ const orderInfoSchema = z.object({
safePrice: z.number(),
}).nullable();
const exchangeContractStep = z.object({
pool: z.string(),
assetIn: z.string(),
assetOut: z.string(),
factory: z.string(),
});
const swapInfoBase = z.object({
id: z.string(),
amountIn: z.number(),
@@ -22,12 +29,7 @@ const swapInfoBase = z.object({
minAmountOut: z.number(),
minAmountIn: z.number(),
marketPrice: z.number().nullable(), // spending asset market price
exchangeContractPath: z.array(z.object({
pool: z.string(),
assetIn: z.string(),
assetOut: z.string(),
factory: z.string(),
})),
exchangeContractPath: z.array(exchangeContractStep),
alternatives: z.object({ // execution alternatives
exchanges: z.array(z.string()),
path: z.array(z.string()),
@@ -40,6 +42,13 @@ const swapInfoBase = z.object({
isThroughPoolOrCurve: z.boolean(),
}).array(),
assetNameMapping: z.record(z.string()).optional(), // address to ERC20 names
usd: z.object({ // USD info of this swap, nullable
aa: z.number().optional(), // available amount in, USD
aao: z.number().optional(), // available amount out, USD
mo: z.number().optional(), // market amount out, USD
mi: z.number().optional(), // market amount in, USD
d: z.string().optional(), // difference in available amount in/out (USD) and market amount out/in (USD) in percentage
}).optional(),
});
const swapInfoByAmountIn = swapInfoBase.extend({

View File

@@ -17,8 +17,6 @@ import unsubscriptionDoneSchema from './schemas/unsubscriptionDoneSchema.js';
import assetPairConfigSchema from './schemas/assetPairConfigSchema.js';
import type { fullOrderSchema, orderUpdateSchema } from './schemas/addressUpdateSchema.js';
import { objectKeys } from '../../../utils/objectKeys.js';
// import assertError from '../../../utils/assertError.js';
// import errorSchema from './schemas/errorSchema';
const UNSUBSCRIBE = 'u';
const SERVER_PING_INTERVAL = 30000;
@@ -42,7 +40,7 @@ type SwapInfoSubscriptionPayload = {
i: string // asset in
o: string // asset out
a: number // amount IN/OUT
es?: string[] | 'cex' | 'pools' // exchange list of all cex or all pools (ORION_POOL, UNISWAP, PANCAKESWAP etc)
es?: string // exchange list of all cex or all pools (ORION_POOL, UNISWAP, PANCAKESWAP etc)
e?: boolean // is amount IN? Value `false` means a = amount OUT, `true` if omitted
is?: boolean // instant settlement
}
@@ -528,6 +526,13 @@ class AggregatorWS {
availableAmountOut: item.aao,
})),
assetsNameMapping: json.anm,
usdInfo: json.usd && {
availableAmountIn: json.usd.aa,
availableAmountOut: json.usd.aao,
marketAmountOut: json.usd.mo,
marketAmountIn: json.usd.mi,
difference: json.usd.d,
},
};
switch (json.k) { // kind

View File

@@ -23,6 +23,7 @@ const subOrderSchema = z.object({
A: z.number(), // settled amount
p: z.number(), // avg weighed settlement price
e: z.string(), // exchange
es: z.string().array().optional(), // exchanges
b: z.string(), // broker address
S: z.enum(subOrderStatuses), // status
o: z.boolean(), // internal only
@@ -52,6 +53,7 @@ export const orderUpdateSchema = z.object({
subOrders: o.c.map((so) => ({
pair: so.P,
exchange: so.e,
exchanges: so.es,
id: so.i,
amount: so.a,
settledAmount: so.A,
@@ -106,6 +108,7 @@ export const fullOrderSchema = z.object({
subOrders: o.c.map((so) => ({
pair: so.P,
exchange: so.e,
exchanges: so.es,
id: so.i,
amount: so.a,
settledAmount: so.A,

View File

@@ -1,6 +1,7 @@
import { z } from 'zod';
import MessageType from '../MessageType.js';
import baseMessageSchema from './baseMessageSchema.js';
import factories from '../../../../constants/factories.js';
const alternativeSchema = z.object({ // execution alternatives
e: z.string().array(), // exchanges
@@ -11,6 +12,7 @@ const alternativeSchema = z.object({ // execution alternatives
aa: z.number().optional(), // available amount in
aao: z.number().optional(), // available amount out
});
const factorySchema = z.enum(factories);
const swapInfoSchemaBase = baseMessageSchema.extend({
T: z.literal(MessageType.SWAP_INFO),
S: z.string(), // swap request id
@@ -37,8 +39,15 @@ const swapInfoSchemaBase = baseMessageSchema.extend({
p: z.string(), // pool address
ai: z.string().toUpperCase(), // asset in
ao: z.string().toUpperCase(), // asset out
f: z.string().toUpperCase(), // factory
}))
f: factorySchema, // factory
})),
usd: z.object({ // USD info of this swap, nullable
aa: z.number().optional(), // available amount in, USD
aao: z.number().optional(), // available amount out, USD
mo: z.number().optional(), // market amount out, USD
mi: z.number().optional(), // market amount in, USD
d: z.string().optional(), // difference in available amount in/out (USD) and market amount out/in (USD) in percentage
}).optional(),
});
const swapInfoSchemaByAmountIn = swapInfoSchemaBase.extend({

View File

@@ -10,15 +10,12 @@ import {
userEarnedSchema,
type PairStatusEnum,
pairStatusSchema,
governanceContractsSchema,
governancePoolsSchema,
governancePoolSchema,
governanceChainsInfoSchema,
pricesWithQuoteAssetSchema,
referralDataSchema,
} from './schemas/index.js';
import type redeemOrderSchema from '../Aggregator/schemas/redeemOrderSchema.js';
import { sourceAtomicHistorySchema, targetAtomicHistorySchema } from './schemas/atomicHistorySchema.js';
import { makePartial } from '../../utils/index.js';
import { makePartial } from '../../utils';
import type { networkCodes } from '../../constants/index.js';
import { fetchWithValidation } from 'simple-typed-fetch';
import type { BasicAuthCredentials } from '../../types.js';
@@ -59,6 +56,14 @@ type AtomicSwapHistoryTargetQuery = AtomicSwapHistoryBaseQuery & {
expiredRedeem?: 0 | 1
state?: 'REDEEMED' | 'BEFORE-REDEEM'
}
type PlatformFees = {
assetIn: string
assetOut: string
walletAddress?: string | undefined
fromWidget?: string | undefined
}
class BlockchainService {
private readonly apiUrl: string;
@@ -106,10 +111,7 @@ class BlockchainService {
this.getBlockNumber = this.getBlockNumber.bind(this);
this.getRedeemOrderBySecretHash = this.getRedeemOrderBySecretHash.bind(this);
this.claimOrder = this.claimOrder.bind(this);
this.getGovernanceContracts = this.getGovernanceContracts.bind(this);
this.getGovernancePools = this.getGovernancePools.bind(this);
this.getGovernancePool = this.getGovernancePool.bind(this);
this.getGovernanceChainsInfo = this.getGovernanceChainsInfo.bind(this);
this.getGasLimits = this.getGasLimits.bind(this);
}
get basicAuthHeaders() {
@@ -223,31 +225,25 @@ class BlockchainService {
);
/**
* @deprecated In favor of getPlatformFees
*/
* @deprecated In favor of getPlatformFees
*/
getTokensFee = () => fetchWithValidation(
`${this.apiUrl}/api/tokensFee`,
z.record(z.string()).transform(makePartial),
{ headers: this.basicAuthHeaders }
);
getPlatformFees = (
{ assetIn, assetOut, walletAddress, fromWidget }: {
assetIn?: string | undefined,
assetOut?: string | undefined,
walletAddress?: string | undefined,
fromWidget?: string | undefined
}
getPlatformFees = ({
assetIn,
assetOut,
walletAddress,
fromWidget
}: PlatformFees
) => {
const url = new URL(`${this.apiUrl}/api/platform-fees`);
if (assetIn !== undefined) {
url.searchParams.append('assetIn', assetIn);
}
if (assetOut !== undefined) {
url.searchParams.append('assetOut', assetOut);
}
url.searchParams.append('assetIn', assetIn);
url.searchParams.append('assetOut', assetOut);
if (walletAddress !== undefined) {
url.searchParams.append('walletAddress', walletAddress);
@@ -264,6 +260,12 @@ class BlockchainService {
)
};
getReferralData = (walletAddress: string) => fetchWithValidation(
`${this.apiUrl}/api/referral-data/${walletAddress}`,
referralDataSchema,
{ headers: this.basicAuthHeaders }
);
getGasPriceWei = () => fetchWithValidation(
`${this.apiUrl}/api/gasPrice`,
z.string(),
@@ -427,9 +429,9 @@ class BlockchainService {
);
/**
* Sender is user address in source BlockchainService instance \
* Receiver is user address in target BlockchainService instance
*/
* Sender is user address in source BlockchainService instance \
* Receiver is user address in target BlockchainService instance
*/
getAtomicSwapHistory = (query: AtomicSwapHistorySourceQuery | AtomicSwapHistoryTargetQuery) => {
const url = new URL(`${this.apiUrl}/api/atomic/history/`);
@@ -484,28 +486,10 @@ class BlockchainService {
},
);
getGovernanceContracts = () => fetchWithValidation(
`${this.apiUrl}/api/governance/info`,
governanceContractsSchema,
{ headers: this.basicAuthHeaders },
);
getGovernancePools = () => fetchWithValidation(
`${this.apiUrl}/api/governance/pools`,
governancePoolsSchema,
{ headers: this.basicAuthHeaders },
);
getGovernancePool = (address: string) => fetchWithValidation(
`${this.apiUrl}/api/governance/pools/${address}`,
governancePoolSchema,
{ headers: this.basicAuthHeaders },
);
getGovernanceChainsInfo = () => fetchWithValidation(
`${this.apiUrl}/api/governance/chains-info`,
governanceChainsInfoSchema,
{ headers: this.basicAuthHeaders },
getGasLimits = () => fetchWithValidation(
`${this.apiUrl}/api/baseLimits`,
z.record(z.number()),
{ headers: this.basicAuthHeaders }
);
}

View File

@@ -16,9 +16,9 @@ const baseAtomicHistoryItem = z.object({
_id: z.string(),
__v: z.number(),
asset: z.string(),
sender: z.string().refine(ethers.utils.isAddress),
secretHash: z.string().refine(ethers.utils.isHexString),
receiver: z.string().refine(ethers.utils.isAddress).optional(),
sender: z.string().refine(ethers.isAddress),
secretHash: z.string().refine(ethers.isHexString),
receiver: z.string().refine(ethers.isAddress).optional(),
secret: z.string().optional(),
});

View File

@@ -1,7 +0,0 @@
import { z } from 'zod';
const governanceChainsInfoSchema = z.object({
isChainSupported: z.boolean(),
});
export default governanceChainsInfoSchema;

View File

@@ -1,19 +0,0 @@
import { z } from 'zod';
const governanceContractsSchema = z.object({
controllerAddress: z.string(),
veTOKENAddress: z.string(),
veTOKENYieldDistributorV4Address: z.string(),
time_total: z.string(),
absolute_ve_token_in_voting: z.string(),
info: z.record(
z.string(),
z.object({
gaugeAddress: z.string(),
gaugeType: z.number(),
gaugeName: z.string(),
})
),
});
export default governanceContractsSchema;

View File

@@ -1,18 +0,0 @@
import { z } from 'zod';
const governancePoolSchema = z.object({
min_apr: z.string(),
max_apr: z.string(),
tvl: z.string(),
lp_supply: z.string(),
lp_staked: z.string(),
lp_staked_with_boost: z.string(),
lp_price_in_usd: z.string(),
reward_per_period: z.string(),
lock_time_for_max_multiplier: z.string(),
lock_max_multiplier: z.string(),
veorn_max_multiplier: z.string(),
veorn_boost_scale_factor: z.string(),
});
export default governancePoolSchema;

View File

@@ -1,28 +0,0 @@
import { z } from 'zod';
const governancePoolsSchema = z.array(
z.object({
slug: z.string(),
identifier: z.string(),
chain: z.string(),
platform: z.string(),
logo: z.string(),
pair: z.string(),
lp_address: z.string(),
lp_staked: z.string(),
lp_staked_with_boost: z.string(),
lp_supply: z.string(),
lp_price_in_usd: z.string(),
farm_address: z.string(),
pool_tokens: z.tuple([z.string(), z.string()]),
pool_rewards: z.array(z.string()),
tvl: z.string(),
min_apr: z.string(),
max_apr: z.string(),
reward_per_period: z.array(z.string()),
weight: z.string(),
liquidity: z.string(),
})
);
export default governancePoolsSchema;

View File

@@ -12,9 +12,6 @@ export { default as atomicSummarySchema } from './atomicSummarySchema.js';
export { default as poolsLpAndStakedSchema } from './poolsLpAndStakedSchema.js';
export { default as userVotesSchema } from './userVotesSchema.js';
export { default as userEarnedSchema } from './userEarnedSchema.js';
export { default as governanceContractsSchema } from './governanceContractsSchema.js';
export { default as governancePoolsSchema } from './governancePoolsSchema.js';
export { default as governancePoolSchema } from './governancePoolSchema.js';
export { default as governanceChainsInfoSchema } from './governanceChainsInfoSchema.js';
export { default as poolsV3InfoSchema } from './poolsV3InfoSchema.js';
export { pricesWithQuoteAssetSchema } from './pricesWithQuoteAssetSchema.js';
export { referralDataSchema } from './referralDataSchema.js';

View File

@@ -11,7 +11,7 @@ const infoSchema = z.object({
chainId: z.number(),
chainName: z.string(),
exchangeContractAddress: z.string(),
swapExecutorContractAddress: z.string().optional(),
swapExecutorContractAddress: z.string(),
oracleContractAddress: z.string(),
matcherAddress: z.string(),
orderFeePercent: z.number(),

View File

@@ -0,0 +1,6 @@
import { z } from 'zod';
export const referralDataSchema = z.object({
referer: z.string().nullable(),
isReferral: z.boolean(),
});

View File

@@ -0,0 +1 @@
export const LOCK_START_TIME = 1690848000;// Aug 01 2023 00:00:00 UTC

View File

@@ -0,0 +1,266 @@
import {
environmentResponseSchema,
getPoolResponseSchema,
listAmountResponseSchema,
listNFTOrderResponseSchema,
listPoolResponseSchema,
listPoolV2ResponseSchema,
listPoolV3ResponseSchema,
PoolV2InfoResponseSchema,
testIncrementorSchema,
veORNInfoResponseSchema,
votingInfoResponseSchema
} from './schemas';
import { fetchWithValidation } from 'simple-typed-fetch';
import { BigNumber } from 'bignumber.js';
import { WEEK_DAYS, YEAR } from '../../constants';
import { LOCK_START_TIME } from './constants';
type BasePayload = {
chainId: number
jsonrpc: '1.0'
};
type GetEnvironmentPayload = BasePayload & {
model: 'Environment'
method: 'getEnvironment'
params: []
};
type ListNFTOrderPayload = BasePayload & {
model: 'OrionV3NFTManager'
method: 'listNFTOrder'
params: [string]
};
type GetPoolInfoPayload = BasePayload & {
model: 'OrionV3Factory' | 'OrionV2Factory'
method: 'getPoolInfo'
params: [string, string, string]
};
type ListPoolPayload = BasePayload & {
model: 'OrionFarmV3'
method: 'listPool'
params: [string]
};
type VeORNInfoPayload = BasePayload & {
model: 'veORN'
method: 'info'
params: [string]
};
type ListAmountPayload = BasePayload & {
model: string
method: 'listAmount'
params: []
};
type GetAmountByORNPayload = BasePayload & {
amountToken: number
timeLock: number
};
type Payload =
| GetEnvironmentPayload
| ListNFTOrderPayload
| GetPoolInfoPayload
| ListPoolPayload
| VeORNInfoPayload
| ListAmountPayload
| GetAmountByORNPayload;
class IndexerService {
private readonly apiUrl: string;
private readonly chainId: number;
get api() {
return this.apiUrl;
}
constructor(apiUrl: string, chainId: number) {
this.apiUrl = apiUrl;
this.chainId = chainId;
this.getEnvironment = this.getEnvironment.bind(this);
this.listNFTOrder = this.listNFTOrder.bind(this);
this.getPoolInfo = this.getPoolInfo.bind(this);
this.getListPool = this.getListPool.bind(this);
this.listPoolV2 = this.listPoolV2.bind(this);
this.poolV2Info = this.poolV2Info.bind(this);
this.listPoolV3 = this.listPoolV3.bind(this);
this.veORNInfo = this.veORNInfo.bind(this);
this.listAmount = this.listAmount.bind(this);
this.getAmountByORN = this.getAmountByORN.bind(this);
this.getAmountAtCurrent = this.getAmountAtCurrent.bind(this);
this.getVotingInfo = this.getVotingInfo.bind(this);
}
readonly makeRPCPayload = (payload: Omit<Payload, 'chainId' | 'jsonrpc'>) => {
return JSON.stringify({
...payload,
chainId: this.chainId,
jsonrpc: '1.0',
});
};
readonly veORNInfo = (address?: string) => {
return fetchWithValidation(this.apiUrl, veORNInfoResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'veORN',
method: 'info',
params: [address],
}),
});
};
readonly getAmountAtCurrent = (amount: number): BigNumber => {
const timestamp = Date.now() / 1000;
// sqrt
return BigNumber(amount).dividedBy(this.getK(timestamp));
};
readonly getAmountByORN = (amountToken: string, lockingDays: number) => {
const alpha = 730 / (30 - Math.sqrt(730 / 7)) ** (1 / 3);
const deltaDaysBN = BigNumber(lockingDays);
if (deltaDaysBN.lte(0)) return BigNumber(0);
const multSQRT = deltaDaysBN.dividedBy(WEEK_DAYS).sqrt();
const multCUBE = deltaDaysBN.dividedBy(alpha).pow(3);
return BigNumber(amountToken)
.multipliedBy(multSQRT.plus(multCUBE));
};
readonly getVotingInfo = (userAddress?: string) => {
return fetchWithValidation(this.apiUrl, votingInfoResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'OrionVoting',
method: 'info',
params: [userAddress],
}),
});
};
readonly getEnvironment = () => {
return fetchWithValidation(this.apiUrl, environmentResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'Environment',
method: 'getEnvironment',
params: [],
}),
});
};
readonly listNFTOrder = (address: string) => {
return fetchWithValidation(this.apiUrl, listNFTOrderResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'OrionV3NFTManager',
method: 'listNFTOrder',
params: [address],
}),
});
};
readonly getListPool = (userAddress?: string) => {
return fetchWithValidation(this.apiUrl, listPoolResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'OrionVoting',
method: 'listPool',
params: [userAddress],
}),
});
};
readonly getPoolInfo = (
token0: string,
token1: string,
poolAddress?: string
) => {
return fetchWithValidation(this.apiUrl, getPoolResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'OrionV3Factory',
method: 'getPoolInfo',
params: [token0, token1, poolAddress],
}),
});
};
readonly listPoolV2 = (address?: string) => {
return fetchWithValidation(this.apiUrl, listPoolV2ResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'OrionFarmV2',
method: 'listPool',
params: [address],
}),
});
};
readonly poolV2Info = (token0: string, token1: string, address: string | undefined) => {
return fetchWithValidation(this.apiUrl, PoolV2InfoResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'OrionV2Factory',
method: 'getPoolInfo',
params: [token0, token1, address],
}),
});
};
readonly listPoolV3 = (address?: string) => {
return fetchWithValidation(this.apiUrl, listPoolV3ResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'OrionFarmV3',
method: 'listPool',
params: [address],
}),
});
};
readonly listAmount = (poolKey: string) => {
return fetchWithValidation(this.apiUrl, listAmountResponseSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: poolKey,
method: 'listAmount',
params: [],
}),
});
};
readonly testRetrieve = () => {
return fetchWithValidation(this.apiUrl, testIncrementorSchema, {
method: 'POST',
body: this.makeRPCPayload({
model: 'Incrementer',
method: 'retrieve',
params: [],
}),
});
};
private readonly getK = (time: number) => {
const currentTime = time < LOCK_START_TIME ? LOCK_START_TIME : time;
const deltaYears = BigNumber(currentTime)
.minus(LOCK_START_TIME)
.dividedBy(YEAR);
return 2 ** BigNumber(deltaYears).multipliedBy(2).toNumber();
};
}
export * as schemas from './schemas/index.js';
export { IndexerService };

View File

@@ -0,0 +1,26 @@
import { z } from 'zod';
import { evmAddressSchema } from './util-schemas.js';
const basicPoolInfo = z.object({
poolAddress: evmAddressSchema,
isInitialized: z.boolean(),
liquidity: z.number().nonnegative(),
liquidityInUSD: z.number().nonnegative(),
liquidityShare: z.number().nonnegative(),
isFarming: z.boolean(),
rewardsTotal: z.number().nonnegative(),
rewardsPerPeriod: z.number().nonnegative(),
rewardsShare: z.number().nonnegative(),
feePerPeriod: z.number().nonnegative(),
feeTotal: z.number().nonnegative(),
feeShare: z.number().nonnegative(),
tickMultiplier: z.number().nonnegative(),
MAX_TICK: z.number().nonnegative().int(),
minAPR: z.number().nonnegative(),
maxAPR: z.number().nonnegative(),
avgAPR: z.number().nonnegative(),
maxBoost: z.number().nonnegative().int(),
feeRate: z.array(z.number().nonnegative()),
});
export default basicPoolInfo;

View File

@@ -0,0 +1,23 @@
import { z } from 'zod';
import { evmAddressSchema } from './util-schemas.js';
import infoSchema from './info-schema.js';
const environmentResponseSchema = z.object({
result: z.object({
chainId: z.number().int().nonnegative(),
nativeToken: z.string(),
ORN: evmAddressSchema,
WETH9: evmAddressSchema,
OrionV3Factory: evmAddressSchema.optional(),
OrionV2Factory: evmAddressSchema.optional(),
OrionV3NFTManager: evmAddressSchema.optional(),
SwapRouterV3: evmAddressSchema.optional(),
OrionFarmV3: evmAddressSchema.optional(),
OrionFarmV2: evmAddressSchema.optional(),
OrionVoting: evmAddressSchema.optional(),
veORN: evmAddressSchema.optional(),
}),
info: infoSchema,
});
export default environmentResponseSchema;

View File

@@ -0,0 +1,20 @@
import { z } from 'zod';
import { evmAddressSchema } from './util-schemas.js';
import basicPoolInfo from './basic-pool-info-schema.js';
import infoSchema from './info-schema.js';
const getPoolResponseSchema = z.object({
result: z.object({
token0: z.string().nonempty(),
token1: z.string().nonempty(),
token0Address: evmAddressSchema,
token1Address: evmAddressSchema,
totalLiquidity: z.number().nonnegative(),
WETH9: evmAddressSchema,
pools: z.record(z.number(), basicPoolInfo.nullable()),
}),
info: infoSchema,
});
export default getPoolResponseSchema;

View File

@@ -0,0 +1,11 @@
export { default as environmentResponseSchema } from './environment-response-schema';
export { default as listNFTOrderResponseSchema } from './list-nft-order-response-schema';
export { default as getPoolResponseSchema } from './get-pool-response-schema';
export { default as listPoolResponseSchema } from './list-pool-schema';
export { default as listPoolV2ResponseSchema } from './list-pool-v2-response-schema';
export { default as PoolV2InfoResponseSchema } from './pool-v2-info-schema';
export { default as listPoolV3ResponseSchema } from './list-pool-v3-response-schema';
export { default as veORNInfoResponseSchema } from './veORN-info-schema';
export { default as listAmountResponseSchema } from './list-amount-schema';
export { default as votingInfoResponseSchema } from './voting-info-schema';
export { default as testIncrementorSchema } from './test-incrementor-schema';

View File

@@ -0,0 +1,14 @@
import { z } from 'zod';
import { ethers } from 'ethers';
const infoSchema = z.object({
blockNumber: z.number().int().nonnegative(),
blockHash: z.string().refine((v) => v.length === 0 || ethers.isHexString(v), {
message: 'blockHash must be a valid hex string or empty',
}),
timeRequest: z.number().int().nonnegative(),
timeAnswer: z.number().int().nonnegative(),
changes: z.number().int().nonnegative(),
});
export default infoSchema;

View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
import infoSchema from './info-schema.js';
const listAmountSchema = z.object({
result: z.record(z.number()),
info: infoSchema,
});
export default listAmountSchema;

View File

@@ -0,0 +1,10 @@
import { z } from 'zod';
import poolSchema from './pool-schema.js';
import infoSchema from './info-schema.js';
const listNFTOrderResponseSchema = z.object({
result: z.array(poolSchema),
info: infoSchema,
});
export default listNFTOrderResponseSchema;

View File

@@ -0,0 +1,11 @@
import { z } from 'zod';
import infoSchema from './info-schema';
import { listPoolV2Schema } from './list-pool-v2-response-schema';
import { listPoolV3Schema } from './list-pool-v3-response-schema';
const listPoolResponseSchema = z.object({
result: z.array(listPoolV2Schema.or(listPoolV3Schema)),
info: infoSchema,
});
export default listPoolResponseSchema;

View File

@@ -0,0 +1,62 @@
import { z } from 'zod';
import { evmAddressSchema } from './util-schemas.js';
import basicPoolInfo from './basic-pool-info-schema.js';
import infoSchema from './info-schema.js';
export const listPoolV2Schema = z.object({
pair: z.string(),
token0: z.string().nonempty(),
token1: z.string().nonempty(),
name: z.string(),
name0: z.string(),
name1: z.string(),
token0Address: evmAddressSchema,
token1Address: evmAddressSchema,
token0Decimals: z.number().int().nonnegative().max(18),
token1Decimals: z.number().int().nonnegative().max(18),
WETH9: evmAddressSchema,
farmAddress: z.string().optional(),
weight: z.number(),
liquidity0: z.number(),
liquidity1: z.number(),
token0Price: z.number(),
token1Price: z.number(),
totalLPSupply: z.number(),
totalLPStake: z.number(),
totalLPStakeInUSD: z.number(),
userLPStaked: z.number(),
userLPStakedInUSD: z.number(),
lpPriceInUSD: z.number(),
lpPriceInORN: z.number(),
userReward: z.number(),
weeklyReward: z.number(),
userAPR: z.number(),
lockMaxMultiplier: z.number(),
veornMaxMultiplier: z.number(),
veornBoostScaleFactor: z.number(),
lockTimeForMaxMultiplier: z.number(),
userBoost: z.number(),
userTimeDeposit: z.number(),
userLockTimeStart: z.number(),
userLockTimePeriod: z.number(),
userVeORN: z.number(),
userORN: z.number(),
userRewardToPool: z.number(),
boostTotalVeORN: z.number(),
boostCurrentPoolReward: z.number(),
boostTotalLiquidity: z.number(),
boostCurrentLiquidity: z.number(),
boostCurrentVeORN: z.number(),
boostTotalReward: z.number(),
...basicPoolInfo.shape,
type: z.string().nonempty(),
});
const listPoolV2ResponseSchema = z.object({
result: z.array(listPoolV2Schema),
info: infoSchema,
});
export default listPoolV2ResponseSchema;

View File

@@ -0,0 +1,32 @@
import { z } from 'zod';
import { evmAddressSchema } from './util-schemas.js';
import basicPoolInfo from './basic-pool-info-schema.js';
import infoSchema from './info-schema.js';
export const listPoolV3Schema = z.object({
token0: z.string().nonempty(),
token1: z.string().nonempty(),
name: z.string(),
name0: z.string(),
name1: z.string(),
token0Address: evmAddressSchema,
token1Address: evmAddressSchema,
token0Decimals: z.number().int().nonnegative().max(18),
token1Decimals: z.number().int().nonnegative().max(18),
WETH9: evmAddressSchema,
poolFee: z.number(),
weeklyReward: z.number(),
weight: z.number(),
totalLPStakeInUSD: z.number(),
...basicPoolInfo.shape,
type: z.literal('v3'),
});
const listPoolV3ResponseSchema = z.object({
result: z.array(listPoolV3Schema),
info: infoSchema,
});
export default listPoolV3ResponseSchema;

View File

@@ -0,0 +1,33 @@
import { z } from 'zod';
import { evmAddressSchema } from './util-schemas.js';
const poolSchema = z.object({
tokenId: evmAddressSchema,
token0: z.string().nonempty(),
token1: z.string().nonempty(),
token0Address: evmAddressSchema,
token1Address: evmAddressSchema,
token0Decimals: z.number().int().nonnegative().max(18),
token1Decimals: z.number().int().nonnegative().max(18),
amount: z.number().nonnegative(),
amount0: z.number().nonnegative(),
amount1: z.number().nonnegative(),
from: z.number().nonnegative(),
to: z.number().nonnegative(),
fee: z.number().nonnegative(),
collectFee: z.number().nonnegative(),
reward: z.number().nonnegative(),
apr: z.number().nonnegative(),
boost: z.number().int().nonnegative(),
isStaked: z.boolean(),
poolFee: z.number().nonnegative(),
poolAddress: evmAddressSchema,
veOrnForMaxBoost: z.number().nonnegative(),
veOrnMaxBoost: z.number().nonnegative(),
veORNCurrent: z.number().nonnegative(),
time: z.number().int().nonnegative(), // tim
});
export default poolSchema;

View File

@@ -0,0 +1,64 @@
import { z } from 'zod';
import { evmAddressSchema } from './util-schemas.js';
import basicPoolInfo from './basic-pool-info-schema';
import infoSchema from './info-schema.js';
const poolInfoSchema = z.object({
pair: z.string(),
name: z.string(),
token0: z.string(),
token1: z.string(),
name0: z.string(),
name1: z.string(),
token0Address: evmAddressSchema,
token1Address: evmAddressSchema,
token0Decimals: z.number().int().nonnegative().max(18),
token1Decimals: z.number().int().nonnegative().max(18),
WETH9: z.string(),
farmAddress: z.string().optional(),
weight: z.number(),
liquidity0: z.number(),
liquidity1: z.number(),
token0Price: z.number(),
token1Price: z.number(),
userLPBalance: z.number(),
userLPBalanceStr: z.string(),
totalLPSupply: z.number(),
totalLPStake: z.number(),
totalLPStakeInUSD: z.number(),
userLPStaked: z.number(),
userLPStakedInUSD: z.number(),
lpPriceInUSD: z.number(),
lpPriceInORN: z.number(),
userReward: z.number(),
userWeeklyReward: z.number(),
userRewardToPool: z.number(),
weeklyReward: z.number(),
userAPR: z.number(),
lockMaxMultiplier: z.number(),
veornMaxMultiplier: z.number(),
veornBoostScaleFactor: z.number(),
lockTimeForMaxMultiplier: z.number(),
userBoost: z.number(),
userTimeDeposit: z.number(),
userLockTimeStart: z.number(),
userLockTimePeriod: z.number(),
userVeORN: z.number(),
userORN: z.number(),
boostTotalVeORN: z.number(),
boostCurrentPoolReward: z.number(),
boostTotalLiquidity: z.number(),
boostCurrentLiquidity: z.number(),
boostCurrentVeORN: z.number(),
boostTotalReward: z.number(),
type: z.literal('v2'),
...basicPoolInfo.shape,
});
const PoolV2InfoResponseSchema = z.object({
result: poolInfoSchema,
info: infoSchema,
});
export default PoolV2InfoResponseSchema;

View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
import infoSchema from './info-schema.js';
const testIncrementorSchema = z.object({
result: z.number().int(),
info: infoSchema,
});
export default testIncrementorSchema;

View File

@@ -0,0 +1,14 @@
import { ethers } from 'ethers';
import { z } from 'zod';
export const evmAddressSchema = z
.string()
.refine(ethers.isAddress, (v) => ({
message: `${v} is not a valid address`,
}));
export const hexStringSchema = z
.string()
.refine(ethers.isHexString, (v) => ({
message: `${v} is not a valid hex string`,
}));

View File

@@ -0,0 +1,27 @@
import { z } from 'zod';
import { evmAddressSchema } from './util-schemas.js';
import infoSchema from './info-schema.js';
const veORNResultSchema = z.object({
avgAPR: z.number(),
minAPR: z.number(),
maxAPR: z.number(),
veTokenAddress: evmAddressSchema,
totalORNLocked: z.number(),
totalVeORN: z.number(),
weeklyReward: z.number(),
userAPR: z.number(),
userVeORN: z.number(),
userORNLocked: z.number(),
userLockEndDate: z.number(),
userReward: z.number(),
userWeeklyReward: z.number(),
userMinLockPeriod: z.number(),
});
const veORNInfoSchema = z.object({
result: veORNResultSchema,
info: infoSchema,
});
export default veORNInfoSchema;

View File

@@ -0,0 +1,34 @@
import { z } from 'zod';
import infoSchema from './info-schema.js';
const poolSchema = z.object({
allVote: z.number(),
name: z.string(),
poolAddress: z.string(),
type: z.string(),
userVote: z.number(),
token0: z.string(), // deprecated
token1: z.string(), // deprecated
name0: z.string(),
name1: z.string(),
poolFee: z.number(),
userWeight: z.number(),
weight: z.number(),
});
const votingResultSchema = z.object({
absoluteVeTokenInVoting: z.number(),
pools: z.array(poolSchema),
userVeTokenBalance: z.number(),
userVeTokenInVoting: z.number(),
veTokenAddress: z.string(),
votingAddress: z.string(),
weeklyReward: z.number(),
});
const votingInfoSchema = z.object({
result: votingResultSchema,
info: infoSchema,
});
export default votingInfoSchema;

View File

@@ -13,6 +13,7 @@ import {
aggregatedHistorySchema,
inviteCodeLinkSchema,
contractsAddressesSchema,
allTimeLeadersSchema,
} from './schemas/index.js';
import type { SupportedChainId } from '../../types.js';
@@ -69,8 +70,9 @@ class ReferralSystem {
this.getMiniStats = this.getMiniStats.bind(this);
this.getRewardsMapping = this.getRewardsMapping.bind(this);
this.claimRewards = this.claimRewards.bind(this);
this.getRating = this.getRating.bind(this);
this.getRating = this.getRating.bind(this);
this.getLeaderboard = this.getLeaderboard.bind(this);
this.getLeaderboardSingleChain = this.getLeaderboardSingleChain.bind(this);
this.getAllTimeLeaders = this.getAllTimeLeaders.bind(this);
this.getContractsAddresses = this.getContractsAddresses.bind(this);
this.getClaimInfo = this.getClaimInfo.bind(this);
this.getAggregatedHistory = this.getAggregatedHistory.bind(this);
@@ -205,7 +207,20 @@ class ReferralSystem {
errorSchema
);
getRating = (refererAddress: string | undefined, chainId: SupportedChainId) =>
getLeaderboard = (refererAddress: string | undefined) =>
fetchWithValidation(
`${this.apiUrl}/referer/ve/rating-table-leaderboard?tag=aggregated`,
ratingSchema,
{
headers:
refererAddress !== undefined
? { 'referer-address': refererAddress }
: {},
},
errorSchema
);
getLeaderboardSingleChain = (refererAddress: string | undefined, chainId: SupportedChainId) =>
fetchWithValidation(
`${this.apiUrl}/referer/ve/rating-table-leaderboard?chain_id=${chainId}`,
ratingSchema,
@@ -218,6 +233,19 @@ class ReferralSystem {
errorSchema
);
getAllTimeLeaders = (refererAddress: string | undefined) =>
fetchWithValidation(
`${this.apiUrl}/referer/ve/leaderboard-lifetime`,
allTimeLeadersSchema,
{
headers:
refererAddress !== undefined
? { 'referer-address': refererAddress }
: {},
},
errorSchema
);
getContractsAddresses = () =>
fetchWithValidation(
`${this.apiUrl}/referer/view/contracts`,

View File

@@ -0,0 +1,11 @@
import { z } from 'zod';
const allTimeLeadersSchema = z.array(z.object({
wallet: z.string(),
total_earnings_fmt: z.number(),
referrals_count_fmt: z.number(),
total_trades_fmt: z.number(),
weekly_earnings_fmt: z.number(),
}));
export default allTimeLeadersSchema;

View File

@@ -1,10 +1,10 @@
import { z } from 'zod';
import { SupportedChainId } from '../../../types.js';
import { isAddress } from 'ethers/lib/utils.js';
import { ethers } from 'ethers';
const contractsAddressesSchema = z.record(
z.nativeEnum(SupportedChainId),
z.string().refine(isAddress)
z.string().refine(ethers.isAddress)
);
export default contractsAddressesSchema;

View File

@@ -15,8 +15,16 @@ const distinctAnalyticsSchema = z.object({
latest_block: z.number(),
}),
),
total_earned: z.number(),
total_sent_to_governance: z.number(),
total_earned: z.number(),
total_volume: z.number(),
total_trades: z.number(),
all_time_earnings_boost_only: z.number(),
all_time_earnings_boost_only_usd: z.number(),
all_time_earnings: z.number(),
all_time_earnings_usd: z.number(),
all_weekly_earnings: z.number(),
all_weekly_earnings_usd: z.number(),
});
export default distinctAnalyticsSchema;

View File

@@ -10,3 +10,4 @@ export { default as claimInfoSchema } from './claimInfoSchema.js';
export { default as aggregatedHistorySchema } from './aggregatedHistorySchema.js';
export { default as contractsAddressesSchema } from './contractsAddressesSchema.js';
export { default as inviteCodeLinkSchema } from './inviteCodeLinkSchema.js';
export { default as allTimeLeadersSchema } from './allTimeLeadersSchema.js';

View File

@@ -1,8 +1,8 @@
import { z } from 'zod';
const linkSchema = z.object({
status: z.string(),
referer: z.string(),
ref_link: z.string(),
});
export default linkSchema;

View File

@@ -4,6 +4,9 @@ const ratingSchema = z.object({
info: z.object({
weekly_boost_budget: z.string(),
weekly_boost_budget_fmt: z.number(),
monthly_boost_budget: z.string(),
monthly_boost_budget_fmt: z.number(),
displayed_boost_budget_fmt: z.number(),
time_left_for_the_reward: z.number(),
time_left_for_the_reward_local: z.string(),
time_left_for_the_reward_utc: z.string(),
@@ -33,6 +36,11 @@ const ratingSchema = z.object({
weighted_volume_fmt: z.number(),
total_weight: z.string(),
total_weight_fmt: z.number(),
total_volume_fmt: z.number(),
weekly_earnings_fmt: z.number(),
total_earnings_fmt: z.number(),
referrals_count_fmt: z.number(),
total_trades_fmt: z.number(),
reward: z.string(),
reward_fmt: z.number()
})),

View File

@@ -2,3 +2,4 @@ export * as aggregator from './Aggregator/index.js';
export * as blockchainService from './BlockchainService/index.js';
export * as priceFeed from './PriceFeed/index.js';
export * as referralSystem from './ReferralSystem/index.js';
export * as indexer from './Indexer/index.js';

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import type factories from './constants/factories.js';
import type { BigNumber } from 'bignumber.js';
import type subOrderStatuses from './constants/subOrderStatuses.js';
import type positionStatuses from './constants/positionStatuses.js';
@@ -47,7 +48,6 @@ export type Order = {
nonce: number // uint64
expiration: number // uint64
buySide: 0 | 1 // uint8, 1=buy, 0=sell
isPersonalSign: boolean // bool
}
export type SignedOrder = {
@@ -59,7 +59,6 @@ export type SignedOrder = {
export type CancelOrderRequest = {
id: number | string
senderAddress: string
isPersonalSign: boolean
}
export type SignedCancelOrderRequest = {
@@ -84,10 +83,13 @@ export enum SupportedChainId {
MAINNET = '1',
ROPSTEN = '3',
GOERLI = '5',
ARBITRUM_GOERLI = '421613',
ARBITRUM = '42161',
FANTOM_OPERA = '250',
POLYGON = '137',
OKC = '66',
OPBNB = '204',
INEVM = '2525',
LINEA = '59144',
POLYGON_TESTNET = '80001',
FANTOM_TESTNET = '4002',
@@ -95,6 +97,7 @@ export enum SupportedChainId {
BSC_TESTNET = '97',
OKC_TESTNET = '65',
DRIP_TESTNET = '56303',
ARBITRUM_GOERLI = '421613',
// For testing and debug purpose
// BROKEN = '0',
@@ -165,11 +168,13 @@ export type SwapInfoAlternative = {
availableAmountOut?: number | undefined
}
type ExchangeContractPath = {
export type Factory = typeof factories[number]
export type SingleSwap = {
pool: string
assetIn: string
assetOut: string
factory: string
factory: Factory
}
export type SwapInfoBase = {
@@ -182,7 +187,7 @@ export type SwapInfoBase = {
minAmountOut: number
path: string[]
exchangeContractPath: ExchangeContractPath[]
exchangeContractPath: SingleSwap[]
exchanges?: string[] | undefined
poolOptimal: boolean
@@ -196,6 +201,13 @@ export type SwapInfoBase = {
} | undefined
alternatives: SwapInfoAlternative[]
assetsNameMapping?: Partial<Record<string, string>> | undefined
usdInfo: {
availableAmountIn: number | undefined
availableAmountOut: number | undefined
marketAmountOut: number | undefined
marketAmountIn: number | undefined
difference: string | undefined
} | undefined
}
export type SwapInfoByAmountIn = SwapInfoBase & {
@@ -252,6 +264,12 @@ export type VerboseUnitConfig = {
// http://10.23.5.11:3003/,
// https://price-feed:3003/
}
indexer?: {
api: string
// For example:
// http://localhost:3004/,
// http://
} | undefined
}
basicAuth?: BasicAuthCredentials
}
@@ -438,3 +456,5 @@ export type AtomicSwap = Partial<
refundTx?: TransactionInfo | undefined
liquidityMigrationTx?: TransactionInfo | undefined
}
export type OrderSource = 'TERMINAL_MARKET' | 'TERMINAL_LIMIT' | 'SWAP_UI' | 'WIDGET';

View File

@@ -0,0 +1,9 @@
import type { AddressLike } from "ethers";
export async function addressLikeToString(address: AddressLike): Promise<string> {
address = await address
if (typeof address !== 'string') {
address = await address.getAddress()
}
return address.toLowerCase()
}

View File

@@ -8,6 +8,6 @@ export default function calculateNetworkFee(
) {
const networkFeeGwei = new BigNumber(gasPriceGwei).multipliedBy(gasLimit);
const bn = new BigNumber(ethers.utils.parseUnits(networkFeeGwei.toString(), 'gwei').toString());
const bn = new BigNumber(ethers.parseUnits(networkFeeGwei.toString(), 'gwei').toString());
return bn.div(new BigNumber(10).pow(NATIVE_CURRENCY_PRECISION)).toString();
}

View File

@@ -1,8 +1,8 @@
import { ERC20__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { ERC20__factory } from '@orionprotocol/contracts/lib/ethers-v6/index.js';
import { ethers } from 'ethers';
import invariant from 'tiny-invariant';
const checkIsToken = async (address: string, provider?: ethers.providers.Provider) => {
const checkIsToken = async (address: string, provider?: ethers.Provider) => {
invariant(provider, 'No provider for token checking');
const tokenContract = ERC20__factory.connect(address, provider);
try {
@@ -12,7 +12,7 @@ const checkIsToken = async (address: string, provider?: ethers.providers.Provide
tokenContract.symbol(),
tokenContract.decimals(),
tokenContract.totalSupply(),
tokenContract.balanceOf(ethers.constants.AddressZero),
tokenContract.balanceOf(ethers.ZeroAddress),
],
);

View File

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

View File

@@ -45,7 +45,7 @@ function isomorphicCryptoRandomBytes(size: number): Uint8Array {
const generateSecret = () => {
const RANDOM_BITS = 256;
const rand = isomorphicCryptoRandomBytes(RANDOM_BITS);
const secret = ethers.utils.keccak256(rand);
const secret = ethers.keccak256(rand);
return secret;
};

View File

@@ -8,7 +8,7 @@ export default function getAvailableFundsSources(
): Source[] {
switch (route) {
case 'aggregator':
if (assetAddress === ethers.constants.AddressZero) return ['exchange']; // We can't take native crypto from wallet
if (assetAddress === ethers.ZeroAddress) return ['exchange']; // We can't take native crypto from wallet
return ['exchange', 'wallet']; // We can take any token amount from exchange + wallet. Order is important!
case 'pool':
if (expenseType === 'network_fee') return ['wallet']; // Network fee is always taken from wallet

View File

@@ -1,10 +1,11 @@
import type { Exchange } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { ERC20__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import type { BigNumber } from 'bignumber.js';
import { ethers } from 'ethers';
import { INTERNAL_PROTOCOL_PRECISION, NATIVE_CURRENCY_PRECISION } from '../constants/index.js';
import type { Aggregator } from '../services/Aggregator/index.js';
import denormalizeNumber from './denormalizeNumber.js';
import { ERC20__factory, Exchange__factory, type Exchange } from "@orionprotocol/contracts/lib/ethers-v6/index.js";
import type { BigNumber } from "bignumber.js";
import { ZeroAddress, ethers } from "ethers";
import { INTERNAL_PROTOCOL_PRECISION, NATIVE_CURRENCY_PRECISION } from "../constants/index.js";
import type { Aggregator } from "../services/Aggregator/index.js";
import denormalizeNumber from "./denormalizeNumber.js";
import type { AddressLike } from "ethers";
import { addressLikeToString } from "./addressLikeToString.js";
export default async function getBalance(
aggregator: Aggregator,
@@ -12,11 +13,11 @@ export default async function getBalance(
assetAddress: string,
walletAddress: string,
exchangeContract: Exchange,
provider: ethers.providers.Provider,
provider: ethers.Provider
) {
const assetIsNativeCryptocurrency = assetAddress === ethers.constants.AddressZero;
const assetIsNativeCryptocurrency = assetAddress === ethers.ZeroAddress;
let assetWalletBalance: ethers.BigNumber | undefined;
let assetWalletBalance: bigint | undefined;
let denormalizedAssetInWalletBalance: BigNumber | undefined;
@@ -28,10 +29,13 @@ export default async function getBalance(
denormalizedAssetInWalletBalance = denormalizeNumber(assetWalletBalance, assetDecimals);
} else {
assetWalletBalance = await provider.getBalance(walletAddress);
denormalizedAssetInWalletBalance = denormalizeNumber(assetWalletBalance, NATIVE_CURRENCY_PRECISION);
denormalizedAssetInWalletBalance = denormalizeNumber(assetWalletBalance, BigInt(NATIVE_CURRENCY_PRECISION));
}
const assetContractBalance = await exchangeContract.getBalance(assetAddress, walletAddress);
const denormalizedAssetInContractBalance = denormalizeNumber(assetContractBalance, INTERNAL_PROTOCOL_PRECISION);
const denormalizedAssetInContractBalance = denormalizeNumber(
assetContractBalance,
BigInt(INTERNAL_PROTOCOL_PRECISION)
);
const denormalizedAssetLockedBalanceResult = await aggregator.getLockedBalance(walletAddress, assetName);
if (denormalizedAssetLockedBalanceResult.isErr()) {
throw new Error(denormalizedAssetLockedBalanceResult.error.message);
@@ -42,3 +46,154 @@ export default async function getBalance(
wallet: denormalizedAssetInWalletBalance,
};
}
async function getExchangeBalanceERC20(
tokenAddress: AddressLike,
walletAddress: AddressLike,
exchangeAddress: AddressLike,
provider: ethers.Provider,
convertToNativeDecimals = true
) {
walletAddress = await addressLikeToString(walletAddress);
exchangeAddress = await addressLikeToString(exchangeAddress);
tokenAddress = await addressLikeToString(tokenAddress);
const exchange = <Exchange>Exchange__factory.connect(exchangeAddress, provider);
const exchangeBalance = await exchange.getBalance(tokenAddress, walletAddress);
if (convertToNativeDecimals) {
const tokenContract = ERC20__factory.connect(tokenAddress, provider);
const decimals = await tokenContract.decimals();
const convertedExchangeBalance = (exchangeBalance * BigInt(10) ** decimals) / BigInt(10) ** 8n;
return convertedExchangeBalance;
}
return exchangeBalance;
}
async function getExchangeBalanceNative(
walletAddress: AddressLike,
exchangeAddress: AddressLike,
provider: ethers.Provider,
convertToNativeDecimals = true
) {
walletAddress = await addressLikeToString(walletAddress);
exchangeAddress = await addressLikeToString(exchangeAddress);
const exchange = <Exchange>Exchange__factory.connect(exchangeAddress, provider);
const exchangeBalance = await exchange.getBalance(ZeroAddress, walletAddress);
if (convertToNativeDecimals) {
const convertedExchangeBalance = (exchangeBalance * BigInt(10) ** 18n) / BigInt(10) ** 8n;
return convertedExchangeBalance;
}
return exchangeBalance;
}
export async function getExchangeBalance(
tokenAddress: AddressLike,
walletAddress: AddressLike,
exchangeAddress: AddressLike,
provider: ethers.Provider,
convertToNativeDecimals = true
) {
walletAddress = await addressLikeToString(walletAddress);
tokenAddress = await addressLikeToString(tokenAddress);
if (typeof tokenAddress === "string" && tokenAddress === ZeroAddress) {
return getExchangeBalanceNative(walletAddress, exchangeAddress, provider, convertToNativeDecimals);
} else {
return getExchangeBalanceERC20(tokenAddress, walletAddress, exchangeAddress, provider, convertToNativeDecimals);
}
}
export async function getExchangeAllowance(
tokenAddress: AddressLike,
walletAddress: AddressLike,
exchangeAddress: AddressLike,
provider: ethers.Provider
) {
if (typeof tokenAddress === "string" && tokenAddress === ZeroAddress) {
return 0n;
} else {
walletAddress = await addressLikeToString(walletAddress);
tokenAddress = await addressLikeToString(tokenAddress);
const tokenContract = ERC20__factory.connect(tokenAddress, provider);
let allowance = await tokenContract.allowance(walletAddress, exchangeAddress);
return allowance;
}
}
async function getWalletBalanceERC20(
tokenAddress: AddressLike,
walletAddress: AddressLike,
provider: ethers.Provider,
convertToExchangeDecimals = false
) {
walletAddress = await addressLikeToString(walletAddress);
tokenAddress = await addressLikeToString(tokenAddress);
const tokenContract = ERC20__factory.connect(tokenAddress, provider);
let walletBalance = await tokenContract.balanceOf(walletAddress);
if (convertToExchangeDecimals) {
const tokenContract = ERC20__factory.connect(tokenAddress, provider);
const decimals = await tokenContract.decimals();
const convertedNativeBalance = (walletBalance * BigInt(10) ** 8n) / BigInt(10) ** decimals;
return convertedNativeBalance;
}
return walletBalance;
}
async function getWalletBalanceNative(
walletAddress: AddressLike,
provider: ethers.Provider,
convertToExchangeDecimals = false
) {
walletAddress = await addressLikeToString(walletAddress);
const nativeBalance = await provider.getBalance(walletAddress);
if (convertToExchangeDecimals) {
const convertedNativeBalance = (nativeBalance * BigInt(10) ** 8n) / BigInt(10) ** 18n;
return convertedNativeBalance;
}
return nativeBalance;
}
export async function getWalletBalance(
tokenAddress: AddressLike,
walletAddress: AddressLike,
provider: ethers.Provider,
convertToExchangeDecimals = false
) {
if (typeof tokenAddress === "string" && tokenAddress === ZeroAddress) {
return getWalletBalanceNative(walletAddress, provider, convertToExchangeDecimals);
} else {
return getWalletBalanceERC20(tokenAddress, walletAddress, provider, convertToExchangeDecimals);
}
}
export async function getTotalBalance(
tokenAddress: AddressLike,
walletAddress: AddressLike,
exchangeAddress: AddressLike,
provider: ethers.Provider,
convertToNativeDecimals = true
) {
const walletBalance = await getWalletBalance(tokenAddress, walletAddress, provider, !convertToNativeDecimals);
const exchangeBalance = await getExchangeBalance(
tokenAddress,
walletAddress,
exchangeAddress,
provider,
convertToNativeDecimals
);
return {
walletBalance,
exchangeBalance,
totalBalance: walletBalance + exchangeBalance
}
}

View File

@@ -1,4 +1,4 @@
import type { Exchange } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import type { Exchange } from '@orionprotocol/contracts/lib/ethers-v6/index.js';
import type { BigNumber } from 'bignumber.js';
import type { ethers } from 'ethers';
import type { Aggregator } from '../services/Aggregator/index.js';
@@ -9,7 +9,7 @@ export default async (
aggregator: Aggregator,
walletAddress: string,
exchangeContract: Exchange,
provider: ethers.providers.Provider,
provider: ethers.Provider,
) => {
const balances = await Promise.all(
Object.entries(balancesRequired)

View File

@@ -11,7 +11,7 @@ const getNativeCryptocurrencyName = (assetToAddress: Partial<Record<string, stri
};
}, {});
const nativeCryptocurrencyName = addressToAssetName[ethers.constants.AddressZero];
const nativeCryptocurrencyName = addressToAssetName[ethers.ZeroAddress];
if (nativeCryptocurrencyName === undefined) {
throw new Error('Native cryptocurrency asset name is not found');
}

View File

@@ -0,0 +1,8 @@
import { type Factory } from '../types.js';
import { factories } from '../index.js';
const isValidFactory = (factory: string): factory is Factory => {
return factories.some((f) => f === factory);
};
export default isValidFactory;

View File

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

View File

@@ -1,13 +1,13 @@
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v5/index.js';
import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v6/index.js';
import { ethers } from 'ethers';
import { z } from 'zod';
const swapThroughOrionPoolSchema = z.object({
name: z.literal('swapThroughOrionPool'),
args: z.tuple([
z.instanceof(ethers.BigNumber), // amount_spend
z.instanceof(ethers.BigNumber), // amount_receive
z.string().refine(ethers.utils.isAddress).array().nonempty(), // path
z.bigint(), // amount_spend
z.bigint(), // amount_receive
z.string().refine(ethers.isAddress).array().nonempty(), // path
z.boolean(), // is_exact_spend
]),
}).transform((data) => ({
@@ -21,34 +21,34 @@ const swapThroughOrionPoolSchema = z.object({
}));
const buyOrderSchema = z.tuple([ // buy order
z.string().refine(ethers.utils.isAddress), // senderAddress
z.string().refine(ethers.utils.isAddress), // matcherAddress
z.string().refine(ethers.utils.isAddress), // baseAsset
z.string().refine(ethers.utils.isAddress), // quoteAsset
z.string().refine(ethers.utils.isAddress), // matcherFeeAsset
z.instanceof(ethers.BigNumber), // amount
z.instanceof(ethers.BigNumber), // price
z.instanceof(ethers.BigNumber), // matcherFee
z.instanceof(ethers.BigNumber), // nonce
z.instanceof(ethers.BigNumber), // expiration
z.string().refine(ethers.isAddress), // senderAddress
z.string().refine(ethers.isAddress), // matcherAddress
z.string().refine(ethers.isAddress), // baseAsset
z.string().refine(ethers.isAddress), // quoteAsset
z.string().refine(ethers.isAddress), // matcherFeeAsset
z.bigint(), // amount
z.bigint(), // price
z.bigint(), // matcherFee
z.bigint(), // nonce
z.bigint(), // expiration
z.literal(1), // buySide
z.boolean(), // isPersonalSign
z.string().refine(ethers.utils.isHexString), // signature
z.string().refine(ethers.isHexString), // signature
]);
const sellOrderSchema = z.tuple([ // sell orer
z.string().refine(ethers.utils.isAddress), // senderAddress
z.string().refine(ethers.utils.isAddress), // matcherAddress
z.string().refine(ethers.utils.isAddress), // baseAsset
z.string().refine(ethers.utils.isAddress), // quoteAsset
z.string().refine(ethers.utils.isAddress), // matcherFeeAsset
z.instanceof(ethers.BigNumber), // amount
z.instanceof(ethers.BigNumber), // price
z.instanceof(ethers.BigNumber), // matcherFee
z.instanceof(ethers.BigNumber), // nonce
z.instanceof(ethers.BigNumber), // expiration
z.string().refine(ethers.isAddress), // senderAddress
z.string().refine(ethers.isAddress), // matcherAddress
z.string().refine(ethers.isAddress), // baseAsset
z.string().refine(ethers.isAddress), // quoteAsset
z.string().refine(ethers.isAddress), // matcherFeeAsset
z.bigint(), // amount
z.bigint(), // price
z.bigint(), // matcherFee
z.bigint(), // nonce
z.bigint(), // expiration
z.literal(0), // buySide
z.boolean(), // isPersonalSign
z.string().refine(ethers.utils.isHexString), // signature
z.string().refine(ethers.isHexString), // signature
]);
const toOrder = <T extends z.infer<typeof buyOrderSchema> | z.infer<typeof sellOrderSchema>>(data: T) => ({
@@ -71,9 +71,9 @@ const fillThroughOrionPoolSchema = z.object({
name: z.literal('fillThroughOrionPool'),
args: z.tuple([
sellOrderSchema,
z.instanceof(ethers.BigNumber), // filled amount
z.instanceof(ethers.BigNumber), // blockchainFee
z.string().refine(ethers.utils.isAddress).array().nonempty(), // path
z.bigint(), // filled amount
z.bigint(), // blockchainFee
z.string().refine(ethers.isAddress).array().nonempty(), // path
]),
}).transform((data) => ({
name: data.name,
@@ -90,8 +90,8 @@ const fillOrdersSchema = z.object({
args: z.tuple([
buyOrderSchema,
sellOrderSchema,
z.instanceof(ethers.BigNumber), // filledPrice
z.instanceof(ethers.BigNumber), // filledAmount
z.bigint(), // filledPrice
z.bigint(), // filledAmount
]),
}).transform((data) => ({
name: data.name,

View File

@@ -1,5 +1,4 @@
export class SafeArray<T> extends Array<T> {
public static override from<T>(array: ArrayLike<T>): SafeArray<T> {
return new SafeArray(array);
}
@@ -9,9 +8,10 @@ export class SafeArray<T> extends Array<T> {
for (const index in array) {
const value = array[index]
if (value === undefined) {
throw new Error("Array passed to constructor has undefined values")
throw new Error('Array passed to constructor has undefined values')
}
this[index] = value
this[index] = value;
}
}
@@ -19,11 +19,11 @@ export class SafeArray<T> extends Array<T> {
return [...this];
}
public override map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): SafeArray<U> {
public override map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: unknown): SafeArray<U> {
return new SafeArray(super.map(callbackfn, thisArg));
}
public override filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): SafeArray<T> {
public override filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: unknown): SafeArray<T> {
return new SafeArray(super.filter(callbackfn, thisArg));
}
@@ -53,8 +53,8 @@ export function safeGet<V>(obj: Partial<Record<string, V>>, key: string, errorMe
const prefix = 'Requirement not met';
export function must(condition: unknown, message?: string | (() => string)): asserts condition {
if (condition) return;
const provided = typeof message === 'function' ? message() : message;
const value = provided ? `${prefix}: ${provided}` : prefix;
throw new Error(value);
}
if (condition) return;
const provided = typeof message === 'function' ? message() : message;
const value = provided ? `${prefix}: ${provided}` : prefix;
throw new Error(value);
}