diff --git a/package-lock.json b/package-lock.json index 97e5759..2671eeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@orionprotocol/sdk", - "version": "0.20.38", + "version": "0.20.39", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@orionprotocol/sdk", - "version": "0.20.38", + "version": "0.20.39", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/src/Unit/Exchange/swapLimit.ts b/src/Unit/Exchange/swapLimit.ts index 8ea85c2..6c9f4c8 100644 --- a/src/Unit/Exchange/swapLimit.ts +++ b/src/Unit/Exchange/swapLimit.ts @@ -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' @@ -49,6 +53,10 @@ type PoolSwap = { export type Swap = AggregatorOrder | PoolSwap; +const isValidSingleSwap = (singleSwap: Omit & { 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,7 +101,8 @@ 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.formatUnits(gasPriceWei, 'gwei').toString(); @@ -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,33 +275,52 @@ export default async function swapLimit({ BigNumber.ROUND_FLOOR, ); - const unsignedSwapThroughOrionPoolTx = await exchangeContract.swapThroughOrionPool.populateTransaction( - 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 = BigInt(parseInt(chainId, 10)); - unsignedSwapThroughOrionPoolTx.gasPrice = BigInt(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 = BigInt(SWAP_THROUGH_ORION_POOL_GAS_LIMIT); + unsignedSwapThroughPoolsTx.gasLimit = BigInt(SWAP_THROUGH_ORION_POOL_GAS_LIMIT); const transactionCost = BigInt(SWAP_THROUGH_ORION_POOL_GAS_LIMIT) * BigInt(gasPriceWei); const denormalizedTransactionCost = denormalizeNumber(transactionCost, BigInt(NATIVE_CURRENCY_PRECISION)); @@ -327,10 +350,10 @@ export default async function swapLimit({ 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, diff --git a/src/Unit/Exchange/swapMarket.ts b/src/Unit/Exchange/swapMarket.ts index afcbaf0..c4c0b05 100644 --- a/src/Unit/Exchange/swapMarket.ts +++ b/src/Unit/Exchange/swapMarket.ts @@ -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 & { slippagePercent: BigNumber.Value @@ -33,6 +37,11 @@ type PoolSwap = { export type Swap = AggregatorOrder | PoolSwap; + +const isValidSingleSwap = (singleSwap: Omit & { factory: string }): singleSwap is SingleSwap => { + return isValidFactory(singleSwap.factory); +} + export default async function swapMarket({ type, assetIn, @@ -71,6 +80,7 @@ export default async function swapMarket({ exchangeContractAddress, matcherAddress, assetToAddress, + swapExecutorContractAddress, } = await simpleFetch(blockchainService.getInfo)(); const nativeCryptocurrency = getNativeCryptocurrencyName(assetToAddress); @@ -78,7 +88,8 @@ 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.formatUnits(gasPriceWei, 'gwei').toString(); @@ -124,7 +135,7 @@ export default async function swapMarket({ : undefined, ); - const { exchanges: swapExchanges } = swapInfo; + const { exchanges: swapExchanges, exchangeContractPath } = swapInfo; const [firstSwapExchange] = swapExchanges; @@ -185,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(); @@ -222,33 +227,52 @@ export default async function swapMarket({ INTERNAL_PROTOCOL_PRECISION, BigNumber.ROUND_FLOOR, ); - const unsignedSwapThroughOrionPoolTx = await exchangeContract.swapThroughOrionPool.populateTransaction( - amountSpendBlockchainParam.toString(), - amountReceiveBlockchainParam.toString(), - 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 = BigInt(chainId); - unsignedSwapThroughOrionPoolTx.gasPrice = BigInt(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 = BigInt(SWAP_THROUGH_ORION_POOL_GAS_LIMIT); + unsignedSwapThroughPoolsTx.gasLimit = BigInt(SWAP_THROUGH_ORION_POOL_GAS_LIMIT); const transactionCost = BigInt(SWAP_THROUGH_ORION_POOL_GAS_LIMIT) * BigInt(gasPriceWei); const denormalizedTransactionCost = denormalizeNumber(transactionCost, BigInt(NATIVE_CURRENCY_PRECISION)); @@ -278,10 +302,10 @@ export default async function swapMarket({ 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, diff --git a/src/services/Aggregator/schemas/swapInfoSchema.ts b/src/services/Aggregator/schemas/swapInfoSchema.ts index 049c8bc..ecbd440 100644 --- a/src/services/Aggregator/schemas/swapInfoSchema.ts +++ b/src/services/Aggregator/schemas/swapInfoSchema.ts @@ -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()), diff --git a/src/utils/isValidFactory.ts b/src/utils/isValidFactory.ts new file mode 100644 index 0000000..16fa2a3 --- /dev/null +++ b/src/utils/isValidFactory.ts @@ -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;