diff --git a/package-lock.json b/package-lock.json index 3e5fae5..24ddf6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "@orionprotocol/sdk", - "version": "0.12.4", + "version": "0.12.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@orionprotocol/sdk", - "version": "0.12.4", + "version": "0.12.12", "license": "ISC", "dependencies": { "@ethersproject/abstract-signer": "^5.6.0", "@ethersproject/providers": "^5.6.2", "@lukeed/csprng": "^1.0.1", - "@orionprotocol/contracts": "0.0.8", + "@orionprotocol/contracts": "0.0.9", "bignumber.js": "^9.0.2", "buffer": "^6.0.3", "crypto-browserify": "^3.12.0", @@ -2485,9 +2485,9 @@ } }, "node_modules/@orionprotocol/contracts": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@orionprotocol/contracts/-/contracts-0.0.8.tgz", - "integrity": "sha512-dgfowYXTf2nu/o9wcQbnLZg+kF7mJusP62unGSxhRKjquVrF1qids+lpSyjvbA9KT/XYKbXg61tXOZ9pOdrBTw==" + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@orionprotocol/contracts/-/contracts-0.0.9.tgz", + "integrity": "sha512-mMnds/0clCcGDr7+LW6SyTKzrxm1o6Hkksi6m6XBeIXkMGjnfMTApoC3b+/sniHuXTgplkb08ecSyz6VPgII2g==" }, "node_modules/@sinonjs/commons": { "version": "1.8.3", @@ -12131,9 +12131,9 @@ } }, "@orionprotocol/contracts": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@orionprotocol/contracts/-/contracts-0.0.8.tgz", - "integrity": "sha512-dgfowYXTf2nu/o9wcQbnLZg+kF7mJusP62unGSxhRKjquVrF1qids+lpSyjvbA9KT/XYKbXg61tXOZ9pOdrBTw==" + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@orionprotocol/contracts/-/contracts-0.0.9.tgz", + "integrity": "sha512-mMnds/0clCcGDr7+LW6SyTKzrxm1o6Hkksi6m6XBeIXkMGjnfMTApoC3b+/sniHuXTgplkb08ecSyz6VPgII2g==" }, "@sinonjs/commons": { "version": "1.8.3", diff --git a/package.json b/package.json index 312b14b..21abd76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@orionprotocol/sdk", - "version": "0.12.12", + "version": "0.12.13", "description": "Orion Protocol SDK", "main": "./lib/esm/index.js", "module": "./lib/esm/index.js", @@ -60,7 +60,7 @@ "@ethersproject/abstract-signer": "^5.6.0", "@ethersproject/providers": "^5.6.2", "@lukeed/csprng": "^1.0.1", - "@orionprotocol/contracts": "0.0.8", + "@orionprotocol/contracts": "0.0.9", "bignumber.js": "^9.0.2", "buffer": "^6.0.3", "crypto-browserify": "^3.12.0", diff --git a/src/OrionUnit/Exchange/getSwapMarketFeeInfo.ts b/src/OrionUnit/Exchange/getSwapMarketFeeInfo.ts index 39c75ee..4e26f06 100644 --- a/src/OrionUnit/Exchange/getSwapMarketFeeInfo.ts +++ b/src/OrionUnit/Exchange/getSwapMarketFeeInfo.ts @@ -94,6 +94,7 @@ export default async function getSwapMarketFeeInfo({ } const [baseAssetName] = swapInfo.orderInfo.assetPair.split('-'); + if (baseAssetName === undefined) throw new Error('Base asset name is undefined'); const baseAssetAddress = assetToAddress[baseAssetName]; if (!baseAssetAddress) throw new Error(`No asset address for ${baseAssetName}`); diff --git a/src/OrionUnit/Exchange/swapMarket.ts b/src/OrionUnit/Exchange/swapMarket.ts index 1c00b29..4195bba 100644 --- a/src/OrionUnit/Exchange/swapMarket.ts +++ b/src/OrionUnit/Exchange/swapMarket.ts @@ -86,6 +86,8 @@ export default async function swapMarket({ const feeAssets = await simpleFetch(orionBlockchain.getTokensFee)(); const pricesInOrn = await simpleFetch(orionBlockchain.getPrices)(); const gasPriceWei = await simpleFetch(orionBlockchain.getGasPriceWei)(); + const { factories } = await simpleFetch(orionBlockchain.getPoolsConfig)(); + const poolExchangesList = factories !== undefined ? Object.keys(factories) : []; const gasPriceGwei = ethers.utils.formatUnits(gasPriceWei, 'gwei').toString(); @@ -125,6 +127,12 @@ export default async function swapMarket({ options?.poolOnly ? ['ORION_POOL'] : undefined, ); + const { exchanges: swapExchanges } = swapInfo; + + const firstSwapExchange = swapExchanges?.[0]; + + if (swapExchanges) options?.logger?.(`Swap exchanges: ${swapExchanges.join(', ')}`); + if (swapInfo.orderInfo !== null && options?.poolOnly === true && options.poolOnly !== swapInfo.isThroughPoolOptimal) { throw new Error(`Unexpected Orion Aggregator response. Please, contact support. Report swap request id: ${swapInfo.id}`); } @@ -139,16 +147,49 @@ export default async function swapMarket({ if (swapInfo.orderInfo === null) throw new Error(swapInfo.executionInfo); + const [baseAssetName, quoteAssetName] = swapInfo.orderInfo.assetPair.split('-'); + if (baseAssetName === undefined) throw new Error('Base asset name is undefined'); + if (quoteAssetName === undefined) throw new Error('Quote asset name is undefined'); + + const pairConfig = await simpleFetch(orionAggregator.getPairConfig)(`${baseAssetName}-${quoteAssetName}`); + if (!pairConfig) throw new Error(`Pair config ${baseAssetName}-${quoteAssetName} not found`); + + const qtyPrecisionBN = new BigNumber(pairConfig.qtyPrecision); + const qtyDecimalPlaces = amountBN.dp(); + + if (qtyPrecisionBN.lt(qtyDecimalPlaces)) throw new Error(`Actual amount decimal places (${qtyDecimalPlaces}) is greater than max allowed decimal places (${qtyPrecisionBN.toString()}) on pair ${baseAssetName}-${quoteAssetName}`); + const percent = new BigNumber(slippagePercent).div(100); let isThroughPoolOptimal: boolean; + if (options?.developer?.route !== undefined) { isThroughPoolOptimal = options.developer.route === 'pool'; - } else if (options?.poolOnly) isThroughPoolOptimal = true; - else isThroughPoolOptimal = swapInfo.isThroughPoolOptimal; + options?.logger?.('Swap is through pool (because route forced to pool)'); + } else if (options?.poolOnly) { + options?.logger?.('Swap is through pool (because "poolOnly" option is true)'); + isThroughPoolOptimal = true; + } else if ( + swapExchanges !== undefined + && poolExchangesList.length > 0 + && swapExchanges.length === 1 + && firstSwapExchange + && poolExchangesList.some((poolExchange) => poolExchange === firstSwapExchange) + ) { + options?.logger?.(`Swap is through pool [via ${firstSwapExchange}] (detected by "exchanges" field)`); + isThroughPoolOptimal = true; + } else { + if (swapInfo.isThroughPoolOptimal) options?.logger?.('Swap is through pool (detected by "isThroughPoolOptimal" field)'); + isThroughPoolOptimal = swapInfo.isThroughPoolOptimal; + } if (isThroughPoolOptimal) { - options?.logger?.('Swap through pool'); + let factoryAddress: string | undefined; + if (factories && firstSwapExchange) { + factoryAddress = factories?.[firstSwapExchange]; + if (factoryAddress) options?.logger?.(`Factory address is ${factoryAddress}. Exchange is ${firstSwapExchange}`); + } + const pathAddresses = swapInfo.path.map((name) => { const assetAddress = assetToAddress?.[name]; if (!assetAddress) throw new Error(`No asset address for ${name}`); @@ -189,7 +230,7 @@ export default async function swapMarket({ const unsignedSwapThroughOrionPoolTx = await exchangeContract.populateTransaction.swapThroughOrionPool( amountSpendBlockchainParam, amountReceiveBlockchainParam, - pathAddresses, + factoryAddress ? [factoryAddress, ...pathAddresses] : pathAddresses, type === 'exactSpend', ); @@ -244,6 +285,7 @@ export default async function swapMarket({ options?.logger?.('Signing transaction...'); const swapThroughOrionPoolTxResponse = await signer.sendTransaction(unsignedSwapThroughOrionPoolTx); + options?.logger?.(`Transaction sent. Tx hash: ${swapThroughOrionPoolTxResponse.hash}`); return { through: 'orion_pool', txHash: swapThroughOrionPoolTxResponse.hash, @@ -263,10 +305,6 @@ export default async function swapMarket({ .multipliedBy(slippageMultiplier) .toString(); - const [baseAssetName, quoteAssetName] = swapInfo.orderInfo.assetPair.split('-'); - const pairConfig = await simpleFetch(orionAggregator.getPairConfig)(`${baseAssetName}-${quoteAssetName}`); - if (!pairConfig) throw new Error(`Pair config ${baseAssetName}-${quoteAssetName} not found`); - const baseAssetAddress = assetToAddress[baseAssetName]; if (!baseAssetAddress) throw new Error(`No asset address for ${baseAssetName}`); const quoteAssetAddress = assetToAddress[quoteAssetName]; @@ -361,6 +399,8 @@ export default async function swapMarket({ if (!orderIsOk) throw new Error('Order is not valid'); const { orderId } = await simpleFetch(orionAggregator.placeOrder)(signedOrder, false); + options?.logger?.(`Order placed. Order id: ${orderId}`); + return { through: 'aggregator', id: orderId, diff --git a/src/OrionUnit/FarmingManager/index.ts b/src/OrionUnit/FarmingManager/index.ts index 506c1cc..a94f6e7 100644 --- a/src/OrionUnit/FarmingManager/index.ts +++ b/src/OrionUnit/FarmingManager/index.ts @@ -41,6 +41,8 @@ export default class FarmingManager { 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 { @@ -219,6 +221,8 @@ export default class FarmingManager { }: 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, diff --git a/src/OrionUnit/index.ts b/src/OrionUnit/index.ts index 3aa12b2..b163b92 100644 --- a/src/OrionUnit/index.ts +++ b/src/OrionUnit/index.ts @@ -74,6 +74,7 @@ export default class OrionUnit { } else { const envInfo = envs[env]; const envNetworks = envInfo?.networks; + if (envNetworks === undefined) throw new Error('Env networks is undefined (constructor)'); if (isValidChainId(chain)) chainId = chain; else { @@ -94,7 +95,9 @@ export default class OrionUnit { : `Chains not found for chain name '${chain}' in env '${env}'.`, ); } - [chainId] = targetChains; + const firstTargetChain = targetChains[0]; + if (firstTargetChain === undefined) throw new Error('First target chain is undefined'); + chainId = firstTargetChain; } if (!(chainId in envNetworks)) { @@ -145,6 +148,8 @@ export default class OrionUnit { const envInfo = envs[this.env]; const envNetworks = envInfo?.networks; + if (envNetworks === undefined) throw new Error('Env networks is undefined (siblings)'); + const siblingsNetworks = Object .keys(envNetworks) .filter(isValidChainId) diff --git a/src/index.ts b/src/index.ts index 13e183e..d476de2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import BigNumber from 'bignumber.js'; -BigNumber.config({ EXPONENTIAL_AT: 1e9 }); +BigNumber.config({ EXPONENTIAL_AT: 1e+9 }); export * as config from './config'; export { default as OrionUnit } from './OrionUnit'; diff --git a/src/services/OrionAggregator/schemas/swapInfoSchema.ts b/src/services/OrionAggregator/schemas/swapInfoSchema.ts index 846f18a..3863539 100644 --- a/src/services/OrionAggregator/schemas/swapInfoSchema.ts +++ b/src/services/OrionAggregator/schemas/swapInfoSchema.ts @@ -15,6 +15,7 @@ const swapInfoBase = z.object({ amount: z.number(), safePrice: z.number(), }).nullable(), + exchanges: z.array(z.string()).optional(), price: z.number().nullable(), // spending asset price minAmountOut: z.number(), minAmountIn: z.number(), diff --git a/src/services/OrionAggregator/ws/index.ts b/src/services/OrionAggregator/ws/index.ts index faff4f4..e064054 100644 --- a/src/services/OrionAggregator/ws/index.ts +++ b/src/services/OrionAggregator/ws/index.ts @@ -329,6 +329,7 @@ class OrionAggregatorWS { minAmounOut: json.mao, minAmounIn: json.ma, path: json.ps, + exchanges: json.e, poolOptimal: json.po, ...json.oi && { orderInfo: { @@ -399,13 +400,13 @@ class OrionAggregatorWS { break; case MessageType.ASSET_PAIRS_CONFIG_UPDATE: { const pairs = json; - let priceUpdates: Partial> = {}; + const priceUpdates: Partial> = {}; pairs.u.forEach(([pairName, minQty, pricePrecision]) => { priceUpdates[pairName] = { minQty, pricePrecision, - } + }; }); this.subscriptions[ @@ -425,7 +426,7 @@ class OrionAggregatorWS { prev[asset] = { tradable, reserved, contract, wallet, allowance, - } + }; return prev; }, {}) @@ -437,7 +438,7 @@ class OrionAggregatorWS { const fullOrder = mapFullOrder(o); prev.push(fullOrder); - + return prev; }, []) : undefined; @@ -475,10 +476,10 @@ class OrionAggregatorWS { } break; case MessageType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATE: { - let brokerBalances: Partial> = {}; + const brokerBalances: Partial> = {}; json.bb.forEach(([asset, balance]) => { - brokerBalances[asset] = balance + brokerBalances[asset] = balance; }); this.subscriptions[ diff --git a/src/services/OrionAggregator/ws/schemas/swapInfoSchema.ts b/src/services/OrionAggregator/ws/schemas/swapInfoSchema.ts index e1c0f5d..0343db4 100644 --- a/src/services/OrionAggregator/ws/schemas/swapInfoSchema.ts +++ b/src/services/OrionAggregator/ws/schemas/swapInfoSchema.ts @@ -13,7 +13,7 @@ const swapInfoSchemaBase = baseMessageSchema.extend({ mao: z.number(), // min amount out ps: z.string().array(), // path po: z.boolean(), // is swap through pool optimal - + e: z.string().array().optional(), // Exchanges p: z.number().optional(), // price mp: z.number().optional(), // market price oi: z.object({ // info about order equivalent to this swap diff --git a/src/services/OrionBlockchain/schemas/poolsConfigSchema.ts b/src/services/OrionBlockchain/schemas/poolsConfigSchema.ts index 258fc7c..16aaf92 100644 --- a/src/services/OrionBlockchain/schemas/poolsConfigSchema.ts +++ b/src/services/OrionBlockchain/schemas/poolsConfigSchema.ts @@ -1,3 +1,4 @@ +import { ethers } from 'ethers'; import { z } from 'zod'; import { makePartial } from '../../../utils'; @@ -7,6 +8,12 @@ const poolsConfigSchema = z.object({ governanceAddress: z.string(), routerAddress: z.string(), votingAddress: z.string(), + factories: z.record( + z.string(), + z.string().refine(ethers.utils.isAddress, 'Factory should be an address'), + ) + .transform(makePartial) + .optional(), pools: z.record( z.string(), z.object({ diff --git a/src/types.ts b/src/types.ts index 37aa007..5d62a31 100644 --- a/src/types.ts +++ b/src/types.ts @@ -112,6 +112,7 @@ export type SwapInfoBase = { minAmounOut: number, path: string[], + exchanges?: string[], poolOptimal: boolean, price?: number, diff --git a/src/utils/isNetworkCodeInEnvironment.ts b/src/utils/isNetworkCodeInEnvironment.ts index e223023..e26f956 100644 --- a/src/utils/isNetworkCodeInEnvironment.ts +++ b/src/utils/isNetworkCodeInEnvironment.ts @@ -6,6 +6,8 @@ export default function isNetworkCodeInEnvironment(networkCode: string, env: str } const envInfo = envs[env]; const envNetworks = envInfo?.networks; + if (envNetworks === undefined) throw new Error('Env networks is undefined (isNetworkCodeInEnvironment)'); + return Object.values(chains) .some((chain) => chain.code.toLowerCase() === networkCode.toLowerCase() && chain.chainId in envNetworks); diff --git a/tsconfig.json b/tsconfig.json index 2aeb43b..14fa2ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -100,6 +100,7 @@ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "noUncheckedIndexedAccess": true, "skipLibCheck": true /* Skip type checking all .d.ts files. */ } } \ No newline at end of file